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

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