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

Last change on this file since 861 was 861, checked in by nanardon, 13 years ago
  • reimport missing files from previous svn
File size: 21.2 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        make_path($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            push(@lists, $res);
357            $names{$res->{name}} = 1;
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 (@lists) {
365            my $host_o = $self->_base->get_object('nethost', $res->{name}) 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            $dbzone .= sprintf("%-30s IN    A     %s\n", $res->{name}, $res->{value});
374            foreach (grep { $_ } $host_o->get_attributes('otherName')) {
375                $dbzone .= sprintf("%-30s IN    A     %s\n", $_, $res->{value});
376            }
377            foreach (grep { $_ } $host_o->get_attributes('cname')) {
378                # It is deny to have both:
379                # foo IN A
380                # foo IN CNAME
381                if ($names{$_}) {
382                    my $msg .= sprintf(
383                        'Cname %s to %s exclude because %s is already an A record',
384                        $_, $res->{name}, $_
385                    );
386                    la_log(LA_ERR, sprintf("$msg (zone %s)", $ozone->id));
387                    $dbzone .= "; $msg\n";
388                } else {
389                    $dbzone .= sprintf("%-30s IN    CNAME %s\n", $_, $res->{name},);
390                }
391            }
392        }
393    }
394
395    $dbzone .= "; End of data from database\n";
396
397    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
398        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
399            $ozone->id);
400        return;
401    }
402
403    if (open(my $handle, '>', $self->_output_file($ozone))) {
404        print $handle $textzone;
405        print $handle $dbzone;
406        close($handle);
407        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
408            $self->_output_file($ozone));
409    } else {
410       la_log(LA_ERR, "Can't open output file for zone %s", $ozone->id);
411       return;
412   } 
413   1;
414}
415
416
417sub _gen_reverse_zone {
418    my ($self, $ozone, $textzone) = @_;
419
420    my $domain = $ozone->get_attributes('domain') || '';
421    my $dbzone = "\n; Comming from database:\n";
422    if ($ozone->get_attributes('net')) {
423        my $findhost = $self->_base->db->prepare_cached(q{
424            select * from (
425            select * from nethost join nethost_attributes_ips on
426            nethost.ikey = nethost_attributes_ips.okey
427            where value::inet <<= ? and exported = true
428            except
429            select * from nethost join nethost_attributes_ips on
430            nethost.ikey = nethost_attributes_ips.okey
431            where value::inet <<= any(?)
432            ) as q
433            order by value::inet
434
435        });
436        $findhost->execute(
437            $ozone->get_attributes('net'),
438            [ $ozone->get_attributes('netExclude') ],
439        ) or do {
440            la_log LA_ERR, "Cannot fetch host list: %s",
441                $self->_base->db->errstr;
442            return;
443        };
444
445        # reverse is complicated:
446        my ($mask) = ($ozone->get_attributes('net') =~ m:/(\d+)$:);
447
448        while (my $res = $findhost->fetchrow_hashref) {
449            my $host_o = $self->_base->get_object('nethost', $res->{name}) or do {
450                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
451                return;
452            };
453            my $desc = $self->_comment_nethost($host_o);
454            my $reverse = $host_o->get_attributes('reverse');
455            $dbzone .= $desc
456                ? '; ' . $desc . "\n"
457                : '';
458            my @ippart = split(/\./, $res->{value});
459            splice(@ippart, 0, $mask/8); # get rid of start of ip
460            my @nippart;
461            while (@ippart) { unshift(@nippart, shift(@ippart)) }
462            $dbzone .= sprintf("%-12s IN    PTR    %s%s\n", join('.', @nippart),
463                $reverse 
464                    ? ($reverse, '.')
465                    : ($res->{name}, ($domain ? ".$domain." : '')));
466        }
467    }
468
469    $dbzone .= "; End of data from database\n";
470   
471    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
472        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
473            $ozone->id);
474        return;
475    }
476
477    if (open(my $handle, '>', $self->_output_file($ozone))) {
478        print $handle $textzone;
479        print $handle $dbzone;
480        close($handle);
481        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
482            $self->_output_file($ozone));
483    } else {
484       la_log(LA_ERR, "can't open output file %s (%s)",
485           $self->_output_file($ozone), $!);
486       return;
487   } 
488   1;
489}
490
491sub _gen_dhcp_zone {
492    my ($self, $ozone, $output) = @_;
493
494    my $outzone = $ozone;
495
496    my @net;
497    if ($outzone->get_attributes('net')) {
498        @net = (map { Net::IP->new($_) } $outzone->get_attributes('net')) or do {
499            la_log(LA_DEBUG, 'Cannot get Net::IP for zone %s (ip: %s)', $outzone->id,
500                join(', ', $outzone->get_attributes('net')));
501            next;
502        };
503    }
504
505    {
506        my $find = $self->_base->db->prepare(q{
507            select * from nethost where exported = true and ikey in(
508            select okey from nethost_attributes where attr = 'macaddr'
509            intersect (
510                select nethost_attributes_ips.okey from nethost_attributes_ips join
511                netzone_attributes
512                on netzone_attributes.attr = 'net' and
513                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
514                join netzone on netzone.ikey = netzone_attributes.okey
515                where netzone.name = $1
516               
517                except
518                select nethost_attributes_ips.okey from nethost_attributes_ips join
519                netzone_attributes
520                on netzone_attributes.attr = 'netExclude' and
521                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
522                join netzone on netzone.ikey = netzone_attributes.okey
523                where netzone.name = $1
524                )
525            )
526            order by name
527
528            });
529        $find->execute($ozone->id) or do {
530            la_log LA_ERR, "Cannot fetch host list: %s",
531                $self->_base->db->errstr;
532            return;
533        };
534        while (my $res = $find->fetchrow_hashref) {
535            my $nethost = $res->{name}; 
536
537            my $obj = $self->_base->get_object('nethost', $nethost) or do {
538                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
539                return;
540            };
541
542            my $retainip; 
543            if (@net) {
544                foreach my $inet (@net) {
545                    ($retainip) = grep { $_ && $inet->overlaps(Net::IP->new($_)) } $obj->get_attributes('ip')
546                        and last;
547                }
548            }
549
550            $obj->get_attributes('noDynamic') && !$retainip and next;
551
552            my $desc = $self->_comment_nethost($obj);
553            foreach my $mac (sort grep { $_ } $obj->get_attributes('macaddr')) {
554                $output .= $desc
555                ? '# ' . $desc . "\n"
556                : '';
557                my $fmac = $mac;
558                $fmac =~ s/://g;
559                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
560                $output .= sprintf("    hardware ethernet %s;\n", $mac);
561                $output .= sprintf("    fixed-address %s;\n", $retainip)
562                if ($retainip);
563                $output .= "}\n\n";
564            }
565        }
566    }
567    if ($ozone->get_attributes('allow_dyn')) {
568        $output .= "\n# Host without IP:\n";
569        my @dynfrom = grep { $_ } $ozone->get_attributes('dynFrom');
570        my $find = $self->_base->db->prepare(q{
571            select * from nethost where exported = true and ikey in(
572            select okey from nethost_attributes where attr = 'macaddr'
573            } . (@dynfrom ? q{
574            intersect
575            (
576                select ikey from nethost where ikey not in
577                    (select okey from nethost_attributes_ips)
578                union
579
580                (
581                select nethost_attributes_ips.okey from nethost_attributes_ips join
582                netzone_attributes
583                on netzone_attributes.attr = 'net' and
584                   netzone_attributes.value::inet >>=
585                   nethost_attributes_ips.value::inet
586                   join netzone on netzone.ikey = netzone_attributes.okey
587                   where netzone.name = any(?)
588                except
589                select nethost_attributes_ips.okey from nethost_attributes_ips join
590                netzone_attributes
591                on netzone_attributes.attr = 'netExclude' and
592                   netzone_attributes.value::inet >>=
593                   nethost_attributes_ips.value::inet
594                   join netzone on netzone.ikey = netzone_attributes.okey
595                   where netzone.name = any(?)
596                )
597            )} : '') . q{
598            except
599            select nethost_attributes_ips.okey from nethost_attributes_ips join
600            netzone_attributes
601            on netzone_attributes.attr = 'net' and
602            netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
603            join netzone on netzone.ikey = netzone_attributes.okey
604            where netzone.name = ?
605            )
606            order by name
607
608            });
609        $find->execute((@dynfrom ? ([ @dynfrom ], [ @dynfrom ]) : ()), $ozone->id) or do {
610            la_log LA_ERR, "Cannot fetch host list: %s",
611                $self->_base->db->errstr;
612            return;
613        };
614        while (my $res = $find->fetchrow_hashref) {
615            my $nethost = $res->{name}; 
616
617            my $obj = $self->_base->get_object('nethost', $nethost);
618
619            $obj->get_attributes('noDynamic') and next;
620
621            my $desc = $self->_comment_nethost($obj);
622            foreach my $mac (grep { $_ } $obj->get_attributes('macaddr')) {
623                $output .= $desc
624                ? '# ' . $desc . "\n"
625                : '';
626                my $fmac = $mac;
627                $fmac =~ s/://g;
628                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
629                $output .= sprintf("    hardware ethernet %s;\n", $mac);
630                $output .= "}\n\n";
631            }
632        }
633    }
634
635    $output .= "# End of data from database\n";
636    if (open(my $handle, '>', $self->_output_file($outzone))) {
637        print $handle $output;
638        close($handle);
639        la_log(LA_INFO, "zone %s written into %s", $outzone->id,
640            $self->_output_file($outzone));
641    } else {
642        la_log(LA_ERR, "Can't open output file for dhcp zone %s (%s)",
643            $outzone, $!);
644        return;
645    }
646    1;
647}
648
6491;
Note: See TracBrowser for help on using the repository browser.