package LATMOS::Accounts::Task::Buildnet;

use strict;
use warnings;
use base qw(LATMOS::Accounts::Task);
use LATMOS::Accounts::Log;
use LATMOS::Accounts::Utils;
use FindBin qw($Bin);
use POSIX qw(strftime);
use Net::IP;
use File::Path;
use File::Temp qw(tempfile);
use Net::IPv4Addr;
use Net::IPv6Addr;

=head1 NAME

LATMOS::Accounts::Task::Buildnet - Task to generate network configuration files

=head1 DESCRIPTION

This contains functions to generate network config file from C<Netzone> and
C<Nethost> object.

These config file can be:

=over 4

=item DNS zone files

A standard DNS zone generated from a header and entries found in bases

=item DNS reverse zone files

A reverse DNS zone genarated from a header and entries found in bases

=item A DHCP host list

A file well formated host list to be included in dhcpd config file.

=back

=cut

# Always try because depend also on files:

sub runDelay { 10 * 60 }

sub init {
    my ($self) = @_;
    my $LA = LATMOS::Accounts->new($self->{config}, noacl => 1);

    $self->{_base} = $LA->base();
    $self->{_base} && $self->{_base} or die "Cannot load base";
    $self->{_base}->type eq 'sql' or die "This module work only with SQL base type\n";
    $self->{_la} = $LA;
}

sub run {
    my ($self) = @_;

    if (my $cmd = $self->_la->val('_network_', 'pre')) {
        exec_command(
            $cmd,
            {
                TEMPLATE_DIR => $self->_la->val('_network_', 'template_dir', ''),
                OUTPUT_DIR => $self->_la->val('_network_', 'output_dir', ''),
                DIRECTORY => $self->_la->val('_network_', 'output_dir', ''),
                HOOK_TYPE => 'PRE',
            },
        );
    }

    my %headers;
    foreach my $zone ($self->_base->search_objects('netzone', 'exported=*')) {
        my $ozone = $self->_base->get_object('netzone', $zone)
            or next;
        # check file need regeneration:
        $self->_check_zone_need_update($ozone) or do {
            la_log(LA_DEBUG, "No need to rebuild %s", $ozone->id);
            next;
        };
        my $header = $self->_pre_zone($ozone) or next;
        $headers{$zone} = $header;
    }
    $self->_base->commit;

    foreach (keys %headers) {
        $self->gen_zone($_, $headers{$_}) or return;
    }
    $self->_base->rollback;

    if (my $cmd = $self->_la->val('_network_', 'post')) {
        exec_command(
            $cmd,
            {
                TEMPLATE_DIR => $self->_la->val('_network_', 'template_dir', ''),
                OUTPUT_DIR => $self->_la->val('_network_', 'output_dir', ''),
                DIRECTORY => $self->_la->val('_network_', 'output_dir', ''),
                HOOK_TYPE => 'POST',
            },
        );
    }

    1;
}

sub _la {
    $_[0]->{_la}
}

sub _base {
    my ($self) = @_;
    return $self->{_base} if ($self->{_base});
    my $base = $self->SUPER::base;
    return $self->{_base} = $base
}

sub _bnet_state {
    my ($self) = @_;
    return $self->{_bnet_state} if ($self->{_bnet_state});
    # where trace goes:
    my $state_file =  $self->_la->val('_default_', 'state_dir', '/');
    $state_file .= '/buildnet_state.ini';
    la_log(LA_DEBUG, "Status file is %s", $state_file);
    if ($state_file && ! -w $state_file) {
        # don't exists, we have to create it
        open(my $handle, '>', $state_file) or do {
            la_log(LA_ERR, "Cannot open build net status file %s",
                $state_file);
            return;
        };
        print $handle "[_default_]\n";
        close($handle);
    }
    $self->{_bnet_state} = Config::IniFiles->new(
        -file => $state_file
    );
}

sub _write_state_file {
    la_log(LA_DEBUG, "Writting status file");
    $_[0]->_bnet_state->RewriteConfig;
}

sub _template_file {
    my ($self, $ozone) = @_;

    my $template =  join('/', grep { $_ } $self->_la->val('_network_', 'template_dir'),
        $ozone->get_attributes('templateD'));
    la_log(LA_DEBUG, "Template for %s is %s", $ozone->id, $template);
    $template;
}

sub _output_file {
    my ($self, $ozone) = @_;

    my $path = join(
        '/',
        $self->_la->val('_network_', 'output_dir',
            ($self->_la->val('_default_', 'state_dir'), $ozone->get_attributes('type'))
        )
    );

    if (! -d $path) {
        la_log(LA_INFO, 'Creating directory %s', $path);
        mkpath($path) or return;
    }
    my $output = join('/', $path, $ozone->get_attributes('outputD'));
    la_log(LA_DEBUG, 'output file for %s is %s', $ozone->id, $output);
    $output;
}

=head2 get_zone_rev ($zone)

Return next zone revision (DNS).

This revision is formated from date + incremental serial number. If day change,
serial start to 1. If serial goes goes over 99, head build from date is
increment.

The code ensure returned number is always highter that current one.

=cut

sub get_zone_rev {
    my ($self, $ozone) = @_;
    my $date = strftime('%Y%m%d01', localtime);
    my $oldrev = $ozone->get_attributes('zoneRevision') || 0;
    my $rev;
    if ($oldrev >= $date) {
        # same date, increment subrev
        $rev = $oldrev + 1;
    } else {
        # date has changed, subrev is 1
        $rev = $date;
    }
    la_log(LA_DEBUG, 'new dns revision for %s is %s', $ozone->id, $rev);
    $ozone->set_c_fields(zoneRevision => $rev) or do {
        return;
    };
    $rev
}


sub _check_zone_need_update {
    my ($self, $ozone) = @_;

    # If env var is set, do it anyway
    if ($ENV{LA_BNET_FORCE}) { return 1 }

    if ($ozone->get_attributes('rev') >
        $self->_bnet_state->val($ozone->id, 'dbrev', 0)) {
        return 1;
    }

    return 1 if (! -f $self->_output_file($ozone));


    if ($ozone->get_attributes('type') ne 'dhcp' && $ozone->get_attributes('templateD')) {
        my $template = $self->_template_file($ozone);
        my $output = $self->_output_file($ozone);
        my @tstat = stat($template);
        my @ostat = stat($output);
        if (($ostat[9] || 0) <= ($tstat[9] || 0)) {
            return 1;
        }
    }

    return;
}

sub _set_last_build {
    my ($self, $ozone) = @_;

    my $lctime = scalar(localtime);
    la_log(LA_DEBUG, 'Update last build for zone %s (%s)', $ozone->id, $lctime);
    $ozone->set_c_fields('lastBuild' => $lctime);
}

sub _pre_zone {
    my ($self, $ozone) = @_;

    if (!$ozone->get_attributes('templateD')) {
        la_log(LA_ERR, "No template file for zone %s, aborting", $ozone->id);
        return;
    }

    my $textzone = $self->_comment_zone($ozone);
    if ($ozone->get_attributes('type') =~ /^(dns|reverse)$/) {
        my $tzone = $self->_read_template($ozone) or return;
        $textzone .= $tzone;
    }
    $self->_set_last_build($ozone);

    return $textzone;
}

=head2 gen_zone($zone, $header)

Generate zone file C<$zone> with header C<$header>

=cut

# TODO what is $header

sub gen_zone {
    my ($self, $zone, $header) = @_;

    my $ozone = $self->_base->get_object('netzone', $zone)
        or return;

    la_log(LA_DEBUG, "Start building zone %s (%s)", $zone,
        $ozone->get_attributes('type'));

    $header ||= $self->_pre_zone($ozone) or return;

    my $type = $ozone->get_attributes('type');
    my $res =
        $type eq 'dns'     ? $self->_gen_dns_zone($ozone, $header) :
        $type eq 'reverse' ? $self->_gen_reverse_zone($ozone, $header) :
        $type eq 'dhcp'    ? $self->_gen_dhcp_zone($ozone, $header) :
        $type eq 'puppet'  ? $self->_gen_puppet_zone($ozone, $header) :
        undef;

    if ($res) {

        if (my $cmd = $self->_la->val('_network_', 'post_file',
                $self->_la->val('_network_', 'post_zone'))) {
            exec_command(
                $cmd,
                {
                TEMPLATE_DIR => $self->_la->val('_network_', 'template_dir', ''),
                OUTPUT_DIR => $self->_la->val('_network_', 'output_dir', ''),
                DIRECTORY => $self->_la->val('_network_', 'output_dir', ''),
                TEMPLATE_FILE => $ozone->get_attributes('templateD'),
                OUTPUT_FILE => $ozone->get_attributes('outputD'),
                HOOK_TYPE => 'POSTFILE',
                },
            );
        }

        $self->_bnet_state->newval($ozone->id, 'dbrev',
            $ozone->get_attributes('rev'));
        la_log LA_DEBUG, "Zone rev build point is %d for %s",
            $ozone->get_attributes('rev'),
            $ozone->id;
        $self->_bnet_state->SetParameterComment(
            $ozone->id, 'dbrev',
            scalar(localtime));
        $self->_write_state_file;

    } else {
        $self->_base->rollback;
    }
    $res
}

sub _checkzone_output {
    my ($self, $ozone, $output) = @_;

    if (!$self->_la->val('_network_', 'checkzone')) {
        return 1;
    }

    my ($fh, $filename) = tempfile();

    print $fh $output;
    close($fh);

    my $named_checkzone = $self->_la->val('_network_', 'named-checkzone',
        '/usr/sbin/named-checkzone');

    my $msg;
    my $res = exec_command(sprintf(
            "%s -k fail '%s' '%s'",
            $named_checkzone,
            $ozone->id,
            $filename,
        ), undef, $msg);
    if (!$res) {
        la_log(LA_ERR, "Error on zone %s: ", $ozone->id);
        la_log(LA_ERR, "  msg: $_") foreach (split(/\n/, $msg));
    } else {
        unlink($filename);
    }
    $res
}

sub _comment_zone {
    my ($self, $ozone) = @_;

    my @output = ();
    my $com_prefix =
        $ozone->get_attributes('type') =~ /^(dhcp|puppet)$/ ? '# ' : '; ';
    push @output, sprintf('Zone %s, type %s', $ozone->id,
        $ozone->get_attributes('type'));
    push @output, $ozone->get_attributes('description')
        if ($ozone->get_attributes('description'));
    push @output, sprintf('Generated by %s', q$Id: BuildNet.pm 6283 2011-05-20 10:16:51Z nanardon $ );
    push @output, sprintf('          the %s', scalar(localtime) );
    push @output, sprintf('Network: %s', join(', ', $ozone->get_attributes('net')))
        if ($ozone->get_attributes('net'));
    push @output, sprintf('Exclude Network: %s', join(', ',
            $ozone->get_attributes('netExclude')))
        if ($ozone->get_attributes('netExclude'));
    if ($ozone->get_attributes('type') eq 'dhcp') {
        push(@output, 'This dhcp zone include dynamic IP address')
            if ($ozone->get_attributes('allow_dyn'));
    }

    return to_ascii(join('', map { $com_prefix . $_ . "\n" } @output) . "\n");
}

sub _comment_nethost {
    my ($self, $nethost) = @_;

    my $displayeduser;
    if (my $owner = $nethost->get_attributes('user')) {
        if (my $user = $self->_base->get_object('user', $owner)) {
            $displayeduser = $user->get_attributes('displayName');
        }
    }
    if ((!$displayeduser) && (my $owner = $nethost->get_attributes('owner'))) {
        if (my $user = $self->_base->get_object('user', $owner)) {
            $displayeduser = $user->get_attributes('displayName');
        }
    }
    my @desc = ($displayeduser, $nethost->get_attributes('description'));

    return to_ascii(join(', ', grep { $_ } @desc) || '');
}

sub _read_template {
    my ($self, $ozone) = @_;

    my $revision = $self->get_zone_rev($ozone) or return;
    my $textzone = '';
    if (open(my $handle, '<', $self->_template_file($ozone))) {
        while (my $line = <$handle>) {
            $line =~ s/(\d+\s*;\s*)?\@REVISION@/$revision/;
            $textzone .= $line;
        }
        close($handle);
    } else {
        la_log(LA_ERR, "Can't open template file for zone %s", $ozone->id);
        return;
    }
    return $textzone;
}

sub _gen_dns_zone {
    my ($self, $ozone, $textzone) = @_;

    my $dbzone = "\n; Comming from database:\n";
    if ($ozone->get_attributes('net')) {
        my $findhost = $self->_base->db->prepare_cached(q{
            select name, value::inet as value from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= any(?) and exported = true
            except
            select name, value::inet from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= any(?)
            order by value, name
        });
        $findhost->execute(
            [ $ozone->get_attributes('net') ],
            [ $ozone->get_attributes('netExclude') ],
        ) or do {
            la_log LA_ERR, "Cannot fetch host list: %s",
                $self->_base->db->errstr;
            return;
        };
        my %lists;
        my %names;
        # Storing all name in %names to check later if CNAME does not conflict
        while (my $res = $findhost->fetchrow_hashref) {
            $lists{$res->{name}} ||= {};
            push(@{$lists{$res->{name}}{ip}}, $res->{value});
            my $host_o = $self->_base->get_object('nethost', $res->{name});
            foreach (grep { $_ } $host_o->get_attributes('otherName')) {
                $names{$_} = 1;
            }
        }

        foreach my $res (sort keys %lists) {
            my $host_o = $self->_base->get_object('nethost', $res) or do {
                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
                return;
            };
            my $desc = $self->_comment_nethost($host_o);
            $dbzone .= $desc
                ? '; ' . $desc . "\n"
                : '';
            foreach my $ip (@{$lists{$res}{ip}}) {
                $dbzone .= sprintf(
                    "%-30s IN    %-4s     %s\n",
                    $res,
                    ($ip =~ /:/ ? 'AAAA' : 'A'),
                    $ip
                );
            }
            foreach (grep { $_ } $host_o->get_attributes('otherName')) {
                foreach my $ip (@{$lists{$res}{ip}}) {
                    $dbzone .= sprintf(
                        "%-30s IN    %-4s     %s\n",
                        $_,
                        ($ip =~ /:/ ? 'AAAA' : 'A'),
                        $ip
                    );
                }
            }
            foreach (grep { $_ } $host_o->get_attributes('cname')) {
                # It is deny to have both:
                # foo IN A
                # foo IN CNAME
                if ($names{$_}) {
                    my $msg .= sprintf(
                        'Cname %s to %s exclude because %s is already an A record',
                        $_, $res, $_
                    );
                    la_log(LA_ERR, sprintf("$msg (zone %s)", $ozone->id));
                    $dbzone .= "; $msg\n";
                } else {
                    $dbzone .= sprintf("%-30s IN    CNAME    %s\n", $_, $res,);
                }
            }
            # SSH finger print
            foreach my $name (grep { $_ } ($res, $host_o->get_attributes('otherName'))) {
                foreach my $sshfp (grep { $_ } $host_o->get_attributes('sshfp')) {
                    $dbzone .= sprintf(
                        "%-30s IN    SSHFP    %s\n",
                        $name,
                        $sshfp,
                    );
                }
            }
        }
    }

    $dbzone .= "; End of data from database\n";

    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
            $ozone->id);
        return;
    }

    if (open(my $handle, '>', $self->_output_file($ozone))) {
        print $handle $textzone;
        print $handle $dbzone;
        close($handle);
        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
            $self->_output_file($ozone));
    } else {
       la_log(LA_ERR, "Can't open output file for zone %s", $ozone->id);
       return;
   }
   1;
}

sub _gen_reverse_zone {
    my ($self, $ozone, $textzone) = @_;

    my $domain = $ozone->get_attributes('domain') || '';
    my $dbzone = "\n; Comming from database:\n";
    if ($ozone->get_attributes('net')) {
        my $findhost = $self->_base->db->prepare_cached(q{
            select * from (
            select * from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= ? and exported = true
            except
            select * from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= any(?)
            ) as q
            order by value::inet

        });
        $findhost->execute(
            $ozone->get_attributes('net'),
            [ $ozone->get_attributes('netExclude') ],
        ) or do {
            la_log LA_ERR, "Cannot fetch host list: %s",
                $self->_base->db->errstr;
            return;
        };

        # reverse is complicated:
	my ($net) = $ozone->get_attributes('net') or do {
        $self->base->log('Cannot fetch attribute "net" for zone %s', $ozone->id);
        return;
    };
	my $netip = Net::IP->new($net) or do {
		$self->base->log(LA_ERR, "Cannot build reverse zone %s: wrong net %s", $ozone->id, $net);
		return;
	};
	my $mask = $netip->prefixlen;

        while (my $res = $findhost->fetchrow_hashref) {
            my $host_o = $self->_base->get_object('nethost', $res->{name}) or do {
                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
                return;
            };
            my $desc = $self->_comment_nethost($host_o);
            my $reverse = $host_o->get_attributes('reverse') || ($res->{name} . '.');
            $dbzone .= $desc
                ? '; ' . $desc . "\n"
                : '';
            my $revip;
            my $fmt;
            if ($res->{value} =~ /:/) {
                # IPv6
                my $m = $mask/4;
                $revip = Net::IPv6Addr->new($res->{value})->to_string_ip6_int;
                $revip =~ s/\.([0-9,a-f]\.?){$m}\.IP6\.INT\.$//i;
                $fmt = "%-72s IN    PTR    %s%s\n";
            } else {
                # ipv4
                if ($mask > 24) {
                    $self->base->log(LA_ERR, 'Mask for zone %s cannot be %d', $ozone->id, $mask);
                    return;
                }
                my @ippart = split(/\./, $res->{value});
                splice(@ippart, 0, $mask/8); # get rid of start of ip
                my @nippart;
                while (@ippart) { unshift(@nippart, shift(@ippart)) }
                $revip = join('.', @nippart);
                $fmt = "%-12s IN    PTR    %s%s\n";
            }
 
            $dbzone .= sprintf($fmt, $revip,
                $reverse =~ /\.$/
                    ? ($reverse, ($domain ? "$domain." : '.'))
                    : ($reverse, '.'));
        }
    }

    $dbzone .= "; End of data from database\n";

    if (!$self->_checkzone_output($ozone, $textzone . $dbzone)) {
        la_log(LA_ERR, "Output of DNS zone %s not ok, not updating this zone",
            $ozone->id);
        return;
    }

    if (open(my $handle, '>', $self->_output_file($ozone))) {
        print $handle $textzone;
        print $handle $dbzone;
        close($handle);
        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
            $self->_output_file($ozone));
    } else {
       la_log(LA_ERR, "can't open output file %s (%s)",
           $self->_output_file($ozone), $!);
       return;
   }
   1;
}

sub _gen_dhcp_zone {
    my ($self, $ozone, $output) = @_;

    my $outzone = $ozone;

    my @net;
    if ($outzone->get_attributes('net')) {
        @net = (map { Net::IP->new($_) } $outzone->get_attributes('net')) or do {
            la_log(LA_DEBUG, 'Cannot get Net::IP for zone %s (ip: %s)', $outzone->id,
                join(', ', $outzone->get_attributes('net')));
            next;
        };
    }

    {
        my $find = $self->_base->db->prepare(q{
            select * from nethost where exported = true and ikey in(
            select okey from nethost_attributes where attr = 'macaddr'
            intersect (
                select nethost_attributes_ips.okey from nethost_attributes_ips join
                netzone_attributes
                on netzone_attributes.attr = 'net' and
                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
                join netzone on netzone.ikey = netzone_attributes.okey
                where netzone.name = $1

                except
                select nethost_attributes_ips.okey from nethost_attributes_ips join
                netzone_attributes
                on netzone_attributes.attr = 'netExclude' and
                netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
                join netzone on netzone.ikey = netzone_attributes.okey
                where netzone.name = $1
                )
            )
            order by name

            });
        $find->execute($ozone->id) or do {
            la_log LA_ERR, "Cannot fetch host list: %s",
                $self->_base->db->errstr;
            return;
        };
        while (my $res = $find->fetchrow_hashref) {
            my $nethost = $res->{name};

            my $obj = $self->_base->get_object('nethost', $nethost) or do {
                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
                return;
            };

            my $retainip;
            if (@net) {
                foreach my $inet (@net) {
                    ($retainip) = grep { $_ && $inet->overlaps(Net::IP->new($_)) } $obj->get_attributes('ip')
                        and last;
                }
            }

            $obj->get_attributes('noDynamic') && !$retainip and next;

            my $desc = $self->_comment_nethost($obj);
            foreach my $mac (sort grep { $_ } $obj->get_attributes('macaddr')) {
                $output .= $desc
                ? '# ' . $desc . "\n"
                : '';
                my $fmac = $mac;
                $fmac =~ s/://g;
                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
                $output .= sprintf("    hardware ethernet %s;\n", $mac);
                $output .= sprintf("    fixed-address %s;\n", $retainip)
                if ($retainip);
                $output .= "}\n\n";
            }
        }
    }
    if ($ozone->get_attributes('allow_dyn')) {
        $output .= "\n# Host without IP:\n";
        my @dynfrom = grep { $_ } $ozone->get_attributes('dynFrom');
        my $find = $self->_base->db->prepare(q{
            select * from nethost where exported = true and ikey in(
            select okey from nethost_attributes where attr = 'macaddr'
            } . (@dynfrom ? q{
            intersect
            (
                select ikey from nethost where ikey not in
                    (select okey from nethost_attributes_ips)
                union

                (
                select nethost_attributes_ips.okey from nethost_attributes_ips join
                netzone_attributes
                on netzone_attributes.attr = 'net' and
                   netzone_attributes.value::inet >>=
                   nethost_attributes_ips.value::inet
                   join netzone on netzone.ikey = netzone_attributes.okey
                   where netzone.name = any(?)
                except
                select nethost_attributes_ips.okey from nethost_attributes_ips join
                netzone_attributes
                on netzone_attributes.attr = 'netExclude' and
                   netzone_attributes.value::inet >>=
                   nethost_attributes_ips.value::inet
                   join netzone on netzone.ikey = netzone_attributes.okey
                   where netzone.name = any(?)
                )
            )} : '') . q{
            except
            select nethost_attributes_ips.okey from nethost_attributes_ips join
            netzone_attributes
            on netzone_attributes.attr = 'net' and
            netzone_attributes.value::inet >>= nethost_attributes_ips.value::inet
            join netzone on netzone.ikey = netzone_attributes.okey
            where netzone.name = ?
            )
            order by name

            });
        $find->execute((@dynfrom ? ([ @dynfrom ], [ @dynfrom ]) : ()), $ozone->id) or do {
            la_log LA_ERR, "Cannot fetch host list: %s",
                $self->_base->db->errstr;
            return;
        };
        while (my $res = $find->fetchrow_hashref) {
            my $nethost = $res->{name};

            my $obj = $self->_base->get_object('nethost', $nethost);

            $obj->get_attributes('noDynamic') and next;

            my $desc = $self->_comment_nethost($obj);
            foreach my $mac (grep { $_ } $obj->get_attributes('macaddr')) {
                $output .= $desc
                ? '# ' . $desc . "\n"
                : '';
                my $fmac = $mac;
                $fmac =~ s/://g;
                $output .= sprintf("host %s-%s {\n", $nethost, lc($fmac));
                $output .= sprintf("    hardware ethernet %s;\n", $mac);
                $output .= "}\n\n";
            }
        }
    }

    $output .= "# End of data from database\n";
    if (open(my $handle, '>', $self->_output_file($outzone))) {
        print $handle $output;
        close($handle);
        la_log(LA_INFO, "zone %s written into %s", $outzone->id,
            $self->_output_file($outzone));
    } else {
        la_log(LA_ERR, "Can't open output file for dhcp zone %s (%s)",
            $outzone->id, $!);
        return;
    }
    1;
}

sub _gen_puppet_zone {
    my ($self, $ozone, $output) = @_;

    if ($ozone->get_attributes('net')) {
        my $findhost = $self->_base->db->prepare_cached(q{
            select * from (
            select * from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= ? and exported = true
            except
            select * from nethost join nethost_attributes_ips on
            nethost.ikey = nethost_attributes_ips.okey
            where value::inet <<= any(?)
            ) as q
            order by value::inet

        });
        $findhost->execute(
            $ozone->get_attributes('net'),
            [ $ozone->get_attributes('netExclude') ],
        ) or do {
            la_log LA_ERR, "Cannot fetch host list: %s",
                $self->_base->db->errstr;
            return;
        };

        my @puppetclasses = $ozone->get_attributes('puppetClass');
        while (my $res = $findhost->fetchrow_hashref) {
            my $nethost = $res->{name};

            my $obj = $self->_base->get_object('nethost', $nethost) or do {
                la_log LA_ERR, "Cannot fetch host %s", $res->{name};
                return;
            };

            # merging classes from host and zone
            my %classes = map { $_ => 1 } grep { $_ } (
                $obj->get_attributes('puppetClass'),
                ($obj->get_attributes('noInheritPuppet') ? () : @puppetclasses),
            );
            my $desc = $self->_comment_nethost($obj);
            $output .= sprintf("node '%s' {\n", $obj->id);
            $output .= $desc
                ? sprintf("    # %s\n", $desc)
                : '';
            $output .= join('', map { "    class '$_'\n" } sort keys %classes);
            $output .= "}\n\n";
        }
    }

    $output .= "# End of data from database\n";
    my $file = $self->_output_file($ozone);
    if (open(my $handle, '>', $file)) {
        print $handle $output;
        close($handle);
        la_log(LA_INFO, "zone %s written into %s", $ozone->id,
            $self->_output_file($ozone));
    } else {
        la_log(LA_ERR, "Can't open output file for puppet zone %s (%s)",
            $ozone->id, $!);
        return;
    }
    1;
}

sub reset_savepoint {
    my ($self) = @_;
    foreach my $zone ($self->_base->search_objects('netzone')) {
        my $ozone = $self->_base->get_object('netzone', $zone)
            or next;

        $self->_bnet_state->newval($ozone->id, 'dbrev', 0);
        la_log LA_DEBUG, "Zone savepoint reset for %s", $ozone->id;
        $self->_bnet_state->SetParameterComment(
            $ozone->id, 'dbrev',
            'Reset savepoint the ' . scalar(localtime));
    }

    $self->_write_state_file;
}

1;

__END__

=head1 AUTHOR

Olivier Thauvin, E<lt>olivier.thauvin@latmos.ipsl.frE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008, 2009, 2010, 2011, 2012 CNRS SA/CETP/LATMOS

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.

=cut
