source: LATMOS-Accounts/lib/LATMOS/Accounts/BuildNet.pm @ 863

Last change on this file since 863 was 863, checked in by nanardon, 13 years ago
  • support IPv6 adresses
File size: 21.6 KB
Line 
1package LATMOS::Accounts::BuildNet;
2
3# $Id: BuildNet.pm 6283 2011-05-20 10:16:51Z nanardon $
4
5use strict;
6use warnings;
7use base qw(LATMOS::Accounts);
8use LATMOS::Accounts::Log;
9use LATMOS::Accounts::Utils;
10use FindBin qw($Bin);
11use POSIX qw(strftime);
12use Net::IP;
13use File::Path;
14use File::Temp qw(tempfile);
15
16sub _base {
17    my ($self) = @_;
18    return $self->{_maintenance_base} if ($self->{_maintenance_base});
19    my $base = $self->SUPER::default_base;
20    $base->type eq 'sql' or die "This module work only with SQL base type\n";
21    return $self->{_maintenance_base} = $base
22}
23
24sub _bnet_state {
25    my ($self) = @_;
26    return $self->{_bnet_state} if ($self->{_bnet_state});
27    # where trace goes:
28    my $state_file =  $self->val('_default_', 'state_dir', '/');
29    $state_file .= '/buildnet_state.ini';
30    la_log(LA_DEBUG, "Status file is %s", $state_file);
31    if ($state_file && ! -w $state_file) {
32        # don't exists, we have to create it
33        open(my $handle, '>', $state_file) or do {
34            la_log(LA_ERR, "Cannot open build net status file %s",
35                $state_file);
36            return;
37        };
38        print $handle "[_default_]\n";
39        close($handle);
40    }
41    $self->{_bnet_state} = Config::IniFiles->new(
42        -file => $state_file
43    );
44}
45
46sub write_state_file {
47    la_log(LA_DEBUG, "Writting status file");
48    $_[0]->_bnet_state->RewriteConfig;
49}
50
51sub gen_all {
52    my ($self) = @_;
53    if (my $cmd = $self->val('_network_', 'pre')) {
54        exec_command(
55            $cmd,
56            {
57                TEMPLATE_DIR => $self->val('_network_', 'template_dir', ''),
58                OUTPUT_DIR => $self->val('_network_', 'output_dir', ''),
59                DIRECTORY => $self->val('_network_', 'output_dir', ''),
60                HOOK_TYPE => 'PRE',
61            },
62        );
63    }
64
65    my %headers;
66    foreach my $zone ($self->_base->search_objects('netzone')) {
67        my $ozone = $self->_base->get_object('netzone', $zone)
68            or next;
69        # check file need regeneration:
70        $self->_check_zone_need_update($ozone) or do {
71            la_log(LA_DEBUG, "No need to rebuild %s", $ozone->id);
72            next;
73        };
74        my $header = $self->_pre_zone($ozone) or next;
75        $headers{$zone} = $header;
76    }
77    $self->_base->commit;
78
79    foreach (keys %headers) {
80        $self->gen_zone($_, $headers{$_}) or return;
81    }
82    $self->_base->rollback;
83
84    if (my $cmd = $self->val('_network_', 'post')) {
85        exec_command(
86            $cmd,
87            {
88                TEMPLATE_DIR => $self->val('_network_', 'template_dir', ''),
89                OUTPUT_DIR => $self->val('_network_', 'output_dir', ''),
90                DIRECTORY => $self->val('_network_', 'output_dir', ''),
91                HOOK_TYPE => 'POST',
92            },
93        );
94    }
95}
96
97sub _template_file {
98    my ($self, $ozone) = @_;
99
100    my $template =  join('/', grep { $_ } $self->val('_network_', 'template_dir'),
101        $ozone->get_attributes('templateD'));
102    la_log(LA_DEBUG, "Template for %s is %s", $ozone->id, $template);
103    $template;
104}
105
106sub _output_file {
107    my ($self, $ozone) = @_;
108
109    my $path = join(
110        '/',
111        $self->val('_network_', 'output_dir', 
112            ($self->val('_default_', 'state_dir'), $ozone->get_attributes('type'))
113        )
114    );
115
116    if (! -d $path) {
117        la_log(LA_INFO, 'Creating directory %s', $path);
118        mkpath($path) or return;
119    }
120    my $output = join('/', $path, $ozone->get_attributes('outputD'));
121    la_log(LA_DEBUG, 'output file for %s is %s', $ozone->id, $output);
122    $output;
123}
124
125sub get_zone_rev {
126    my ($self, $ozone) = @_;
127    my $date = strftime('%Y%m%d01', localtime);
128    my $oldrev = $ozone->get_attributes('zoneRevision') || 0;
129    my $rev;
130    if ($oldrev >= $date) {
131        # same date, increment subrev
132        $rev = $oldrev + 1;
133    } else {
134        # date has changed, subrev is 1
135        $rev = $date;
136    }
137    la_log(LA_DEBUG, 'new dns revision for %s is %s', $ozone->id, $rev);
138    $ozone->set_c_fields(zoneRevision => $rev) or do {
139        return;
140    };
141    $rev
142}
143
144
145sub _check_zone_need_update {
146    my ($self, $ozone) = @_;
147
148    # If env var is set, do it anyway
149    if ($ENV{LA_BNET_FORCE}) { return 1 }
150
151    if ($ozone->get_attributes('rev') >
152        $self->_bnet_state->val($ozone->id, 'dbrev', 0)) {
153        return 1;
154    }
155
156    return 1 if (! -f $self->_output_file($ozone));
157       
158
159    if ($ozone->get_attributes('type') ne 'dhcp' && $ozone->get_attributes('templateD')) {
160        my $template = $self->_template_file($ozone);
161        my $output = $self->_output_file($ozone);
162        my @tstat = stat($template);
163        my @ostat = stat($output);
164        if (($ostat[9] || 0) <= ($tstat[9] || 0)) {
165            return 1;
166        }
167    }
168
169    return;
170}
171
172sub _set_last_build {
173    my ($self, $ozone) = @_;
174
175    my $lctime = scalar(localtime);
176    la_log(LA_DEBUG, 'Update last build for zone %s (%s)', $ozone->id, $lctime);
177    $ozone->set_c_fields('lastBuild' => $lctime);
178}
179
180sub _pre_zone {
181    my ($self, $ozone) = @_;
182     
183    if (!$ozone->get_attributes('templateD')) {
184        la_log(LA_ERR, "No template file for zone %s, aborting", $ozone->id);
185        return;
186    }
187
188    my $textzone = $self->_comment_zone($ozone);
189    if ($ozone->get_attributes('type') =~ /^(dns|reverse)$/) {
190        my $tzone = $self->_read_template($ozone) or return;
191        $textzone .= $tzone;
192    }
193    $self->_set_last_build($ozone);
194
195    return $textzone;
196}
197
198sub gen_zone {
199    my ($self, $zone, $header) = @_;
200   
201    my $ozone = $self->_base->get_object('netzone', $zone)
202        or return;
203 
204    la_log(LA_DEBUG, "Start building zone %s (%s)", $zone,
205        $ozone->get_attributes('type'));
206   
207    $header ||= $self->_pre_zone($ozone) or return;
208
209    my $type = $ozone->get_attributes('type');
210    my $res =
211        $type eq 'dns'     ? $self->_gen_dns_zone($ozone, $header) :
212        $type eq 'reverse' ? $self->_gen_reverse_zone($ozone, $header) :
213        $type eq 'dhcp'    ? $self->_gen_dhcp_zone($ozone, $header) :
214        undef;
215       
216    if ($res) {
217
218        if (my $cmd = $self->val('_network_', 'post_file',
219                $self->val('_network_', 'post_zone'))) {
220            exec_command(
221                $cmd,
222                {
223                TEMPLATE_DIR => $self->val('_network_', 'template_dir', ''),
224                OUTPUT_DIR => $self->val('_network_', 'output_dir', ''),
225                DIRECTORY => $self->val('_network_', 'output_dir', ''),
226                TEMPLATE_FILE => $ozone->get_attributes('templateD'),
227                OUTPUT_FILE => $ozone->get_attributes('outputD'),
228                HOOK_TYPE => 'POSTFILE',
229                },
230            );
231        }
232
233        $self->_bnet_state->newval($ozone->id, 'dbrev',
234            $ozone->get_attributes('rev'));
235        la_log LA_DEBUG, "Zone rev build point is %d for %s",
236            $ozone->get_attributes('rev'),
237            $ozone->id;
238        $self->_bnet_state->SetParameterComment(
239            $ozone->id, 'dbrev',
240            scalar(localtime));
241        $self->write_state_file;
242
243    } else {
244        $self->_base->rollback;
245    }
246    $res
247}
248
249sub _checkzone_output {
250    my ($self, $ozone, $output) = @_;
251
252    my ($fh, $filename) = tempfile();
253
254    print $fh $output;
255    close($fh);
256
257    my $msg;
258    my $res = exec_command(sprintf(
259            "%s -k fail '%s' '%s'",
260            '/usr/sbin/named-checkzone',
261            $ozone->id,
262            $filename,
263        ), undef, $msg);
264    if (!$res) {
265        la_log(LA_ERR, "Error on zone %s: ", $ozone->id);
266        la_log(LA_ERR, "  msg: $_") foreach (split(/\n/, $msg));
267    } else {
268        unlink($filename);
269    }
270    $res
271}
272
273sub _comment_zone {
274    my ($self, $ozone) = @_;
275
276    my @output = ();
277    my $com_prefix = 
278        $ozone->get_attributes('type') eq 'dhcp' ? '# ' : '; ';
279    push @output, sprintf('Zone %s, type %s', $ozone->id,
280        $ozone->get_attributes('type'));
281    push @output, $ozone->get_attributes('description')
282        if ($ozone->get_attributes('description'));
283    push @output, sprintf('Generated by %s', q$Id: BuildNet.pm 6283 2011-05-20 10:16:51Z nanardon $ );
284    push @output, sprintf('Network: %s', join(', ', $ozone->get_attributes('net')))
285        if ($ozone->get_attributes('net'));
286    push @output, sprintf('Exclude Network: %s', join(', ',
287            $ozone->get_attributes('netExclude')))
288        if ($ozone->get_attributes('netExclude'));
289    if ($ozone->get_attributes('type') eq 'dhcp') {
290        push(@output, 'This dhcp zone include dynamic IP address')
291            if ($ozone->get_attributes('allow_dyn'));
292    }
293
294    return to_ascii(join('', map { $com_prefix . $_ . "\n" } @output) . "\n");
295}
296
297sub _comment_nethost {
298    my ($self, $nethost) = @_;
299
300    my @desc;
301    if (my $owner = $nethost->get_attributes('owner')) {
302        if (my $user = $self->_base->get_object('user', $owner)) {
303            push(@desc, $user->get_attributes('displayName'));
304        }
305    }
306    push(@desc, $nethost->get_attributes('description'));
307
308    return to_ascii(join(', ', grep { $_ } @desc) || '');
309}
310
311sub _read_template {
312    my ($self, $ozone) = @_;
313
314    my $revision = $self->get_zone_rev($ozone) or return;
315    my $textzone = '';
316    if (open(my $handle, '<', $self->_template_file($ozone))) {
317        while (my $line = <$handle>) {
318            $line =~ s/(\d+\s*;\s*)?\@REVISION@/$revision/;
319            $textzone .= $line;
320        }
321        close($handle);
322    } else {
323        la_log(LA_ERR, "Can't open template file for zone %s", $ozone->id);
324        return;
325    }
326    return $textzone;
327}
328
329sub _gen_dns_zone {
330    my ($self, $ozone, $textzone) = @_;
331   
332    my $dbzone = "\n; Comming from database:\n";
333    if ($ozone->get_attributes('net')) {
334        my $findhost = $self->_base->db->prepare_cached(q{
335            select name, value::inet as value from nethost join nethost_attributes_ips on
336            nethost.ikey = nethost_attributes_ips.okey
337            where value::inet <<= any(?) and exported = true
338            except
339            select name, value::inet from nethost join nethost_attributes_ips on
340            nethost.ikey = nethost_attributes_ips.okey
341            where value::inet <<= any(?)
342            order by value, name
343        });
344        $findhost->execute(
345            [ $ozone->get_attributes('net') ],
346            [ $ozone->get_attributes('netExclude') ],
347        ) or do {
348            la_log LA_ERR, "Cannot fetch host list: %s",
349                $self->_base->db->errstr;
350            return;
351        };
352        my %lists;
353        my %names;
354        # Storing all name in %names to check later if CNAME does not conflict
355        while (my $res = $findhost->fetchrow_hashref) {
356            $lists{$res->{name}} ||= {};
357            push(@{$lists{$res->{name}}{ip}}, $res->{value});
358            my $host_o = $self->_base->get_object('nethost', $res->{name});
359            foreach (grep { $_ } $host_o->get_attributes('otherName')) {
360                $names{$_} = 1;
361            }
362        }
363
364        foreach my $res (sort keys %lists) {
365            my $host_o = $self->_base->get_object('nethost', $res) or do {
366                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
367                return;
368            };
369            my $desc = $self->_comment_nethost($host_o);
370            $dbzone .= $desc
371                ? '; ' . $desc . "\n"
372                : '';
373            foreach my $ip (@{$lists{$res}{ip}}) {
374                $dbzone .= sprintf(
375                    "%-30s IN    %-4s     %s\n",
376                    $res,
377                    ($ip =~ /:/ ? 'AAAA' : 'A'),
378                    $ip
379                );
380            }
381            foreach (grep { $_ } $host_o->get_attributes('otherName')) {
382                foreach my $ip (@{$lists{$res}{ip}}) {
383                    $dbzone .= sprintf(
384                        "%-30s IN    %-4s     %s\n",
385                        $_,
386                        ($ip =~ /:/ ? 'AAAA' : 'A'),
387                        $ip
388                    );
389                }
390            }
391            foreach (grep { $_ } $host_o->get_attributes('cname')) {
392                # It is deny to have both:
393                # foo IN A
394                # foo IN CNAME
395                if ($names{$_}) {
396                    my $msg .= sprintf(
397                        'Cname %s to %s exclude because %s is already an A record',
398                        $_, $res->{name}, $_
399                    );
400                    la_log(LA_ERR, sprintf("$msg (zone %s)", $ozone->id));
401                    $dbzone .= "; $msg\n";
402                } else {
403                    $dbzone .= sprintf("%-30s IN    CNAME    %s\n", $_, $res,);
404                }
405            }
406        }
407    }
408
409    $dbzone .= "; End of data from database\n";
410
411    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
412        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
413            $ozone->id);
414        return;
415    }
416
417    if (open(my $handle, '>', $self->_output_file($ozone))) {
418        print $handle $textzone;
419        print $handle $dbzone;
420        close($handle);
421        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
422            $self->_output_file($ozone));
423    } else {
424       la_log(LA_ERR, "Can't open output file for zone %s", $ozone->id);
425       return;
426   } 
427   1;
428}
429
430
431sub _gen_reverse_zone {
432    my ($self, $ozone, $textzone) = @_;
433
434    my $domain = $ozone->get_attributes('domain') || '';
435    my $dbzone = "\n; Comming from database:\n";
436    if ($ozone->get_attributes('net')) {
437        my $findhost = $self->_base->db->prepare_cached(q{
438            select * from (
439            select * from nethost join nethost_attributes_ips on
440            nethost.ikey = nethost_attributes_ips.okey
441            where value::inet <<= ? and exported = true
442            except
443            select * from nethost join nethost_attributes_ips on
444            nethost.ikey = nethost_attributes_ips.okey
445            where value::inet <<= any(?)
446            ) as q
447            order by value::inet
448
449        });
450        $findhost->execute(
451            $ozone->get_attributes('net'),
452            [ $ozone->get_attributes('netExclude') ],
453        ) or do {
454            la_log LA_ERR, "Cannot fetch host list: %s",
455                $self->_base->db->errstr;
456            return;
457        };
458
459        # reverse is complicated:
460        my ($mask) = ($ozone->get_attributes('net') =~ m:/(\d+)$:);
461
462        while (my $res = $findhost->fetchrow_hashref) {
463            my $host_o = $self->_base->get_object('nethost', $res->{name}) or do {
464                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
465                return;
466            };
467            my $desc = $self->_comment_nethost($host_o);
468            my $reverse = $host_o->get_attributes('reverse');
469            $dbzone .= $desc
470                ? '; ' . $desc . "\n"
471                : '';
472            my @ippart = split(/\./, $res->{value});
473            splice(@ippart, 0, $mask/8); # get rid of start of ip
474            my @nippart;
475            while (@ippart) { unshift(@nippart, shift(@ippart)) }
476            $dbzone .= sprintf("%-12s IN    PTR    %s%s\n", join('.', @nippart),
477                $reverse 
478                    ? ($reverse, '.')
479                    : ($res->{name}, ($domain ? ".$domain." : '')));
480        }
481    }
482
483    $dbzone .= "; End of data from database\n";
484   
485    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
486        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
487            $ozone->id);
488        return;
489    }
490
491    if (open(my $handle, '>', $self->_output_file($ozone))) {
492        print $handle $textzone;
493        print $handle $dbzone;
494        close($handle);
495        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
496            $self->_output_file($ozone));
497    } else {
498       la_log(LA_ERR, "can't open output file %s (%s)",
499           $self->_output_file($ozone), $!);
500       return;
501   } 
502   1;
503}
504
505sub _gen_dhcp_zone {
506    my ($self, $ozone, $output) = @_;
507
508    my $outzone = $ozone;
509
510    my @net;
511    if ($outzone->get_attributes('net')) {
512        @net = (map { Net::IP->new($_) } $outzone->get_attributes('net')) or do {
513            la_log(LA_DEBUG, 'Cannot get Net::IP for zone %s (ip: %s)', $outzone->id,
514                join(', ', $outzone->get_attributes('net')));
515            next;
516        };
517    }
518
519    {
520        my $find = $self->_base->db->prepare(q{
521            select * from nethost where exported = true and ikey in(
522            select okey from nethost_attributes where attr = 'macaddr'
523            intersect (
524                select nethost_attributes_ips.okey from nethost_attributes_ips join
525                netzone_attributes
526                on netzone_attributes.attr = 'net' and
527                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
528                join netzone on netzone.ikey = netzone_attributes.okey
529                where netzone.name = $1
530               
531                except
532                select nethost_attributes_ips.okey from nethost_attributes_ips join
533                netzone_attributes
534                on netzone_attributes.attr = 'netExclude' and
535                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
536                join netzone on netzone.ikey = netzone_attributes.okey
537                where netzone.name = $1
538                )
539            )
540            order by name
541
542            });
543        $find->execute($ozone->id) or do {
544            la_log LA_ERR, "Cannot fetch host list: %s",
545                $self->_base->db->errstr;
546            return;
547        };
548        while (my $res = $find->fetchrow_hashref) {
549            my $nethost = $res->{name}; 
550
551            my $obj = $self->_base->get_object('nethost', $nethost) or do {
552                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
553                return;
554            };
555
556            my $retainip; 
557            if (@net) {
558                foreach my $inet (@net) {
559                    ($retainip) = grep { $_ && $inet->overlaps(Net::IP->new($_)) } $obj->get_attributes('ip')
560                        and last;
561                }
562            }
563
564            $obj->get_attributes('noDynamic') && !$retainip and next;
565
566            my $desc = $self->_comment_nethost($obj);
567            foreach my $mac (sort grep { $_ } $obj->get_attributes('macaddr')) {
568                $output .= $desc
569                ? '# ' . $desc . "\n"
570                : '';
571                my $fmac = $mac;
572                $fmac =~ s/://g;
573                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
574                $output .= sprintf("    hardware ethernet %s;\n", $mac);
575                $output .= sprintf("    fixed-address %s;\n", $retainip)
576                if ($retainip);
577                $output .= "}\n\n";
578            }
579        }
580    }
581    if ($ozone->get_attributes('allow_dyn')) {
582        $output .= "\n# Host without IP:\n";
583        my @dynfrom = grep { $_ } $ozone->get_attributes('dynFrom');
584        my $find = $self->_base->db->prepare(q{
585            select * from nethost where exported = true and ikey in(
586            select okey from nethost_attributes where attr = 'macaddr'
587            } . (@dynfrom ? q{
588            intersect
589            (
590                select ikey from nethost where ikey not in
591                    (select okey from nethost_attributes_ips)
592                union
593
594                (
595                select nethost_attributes_ips.okey from nethost_attributes_ips join
596                netzone_attributes
597                on netzone_attributes.attr = 'net' and
598                   netzone_attributes.value::inet >>=
599                   nethost_attributes_ips.value::inet
600                   join netzone on netzone.ikey = netzone_attributes.okey
601                   where netzone.name = any(?)
602                except
603                select nethost_attributes_ips.okey from nethost_attributes_ips join
604                netzone_attributes
605                on netzone_attributes.attr = 'netExclude' and
606                   netzone_attributes.value::inet >>=
607                   nethost_attributes_ips.value::inet
608                   join netzone on netzone.ikey = netzone_attributes.okey
609                   where netzone.name = any(?)
610                )
611            )} : '') . q{
612            except
613            select nethost_attributes_ips.okey from nethost_attributes_ips join
614            netzone_attributes
615            on netzone_attributes.attr = 'net' and
616            netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
617            join netzone on netzone.ikey = netzone_attributes.okey
618            where netzone.name = ?
619            )
620            order by name
621
622            });
623        $find->execute((@dynfrom ? ([ @dynfrom ], [ @dynfrom ]) : ()), $ozone->id) or do {
624            la_log LA_ERR, "Cannot fetch host list: %s",
625                $self->_base->db->errstr;
626            return;
627        };
628        while (my $res = $find->fetchrow_hashref) {
629            my $nethost = $res->{name}; 
630
631            my $obj = $self->_base->get_object('nethost', $nethost);
632
633            $obj->get_attributes('noDynamic') and next;
634
635            my $desc = $self->_comment_nethost($obj);
636            foreach my $mac (grep { $_ } $obj->get_attributes('macaddr')) {
637                $output .= $desc
638                ? '# ' . $desc . "\n"
639                : '';
640                my $fmac = $mac;
641                $fmac =~ s/://g;
642                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
643                $output .= sprintf("    hardware ethernet %s;\n", $mac);
644                $output .= "}\n\n";
645            }
646        }
647    }
648
649    $output .= "# End of data from database\n";
650    if (open(my $handle, '>', $self->_output_file($outzone))) {
651        print $handle $output;
652        close($handle);
653        la_log(LA_INFO, "zone %s written into %s", $outzone->id,
654            $self->_output_file($outzone));
655    } else {
656        la_log(LA_ERR, "Can't open output file for dhcp zone %s (%s)",
657            $outzone, $!);
658        return;
659    }
660    1;
661}
662
6631;
Note: See TracBrowser for help on using the repository browser.