The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package RackMan::Device::PDU::APC_RackPDU;

use Carp;
use Config::IniFiles;
use Cwd         qw< getcwd >;
use File::Path;
use File::Temp  qw< tempdir >;
use Moose::Role;
use Net::FTP;
use Net::SNMP;
use RackMan;
use RackMan::File;
use RackMan::Utils;
use namespace::autoclean;


use constant {
    CONFIG_SECTION  => "device:pdu:apc_rackpdu",
    INTERFACE_NAME  => "AC-out",
    CONFIG_FILENAME => "config.ini",
    EMPTY_VALUE     => '""',
    PDU_RESTART_OID => ".1.3.6.1.4.1.318.2.2.1",
};


#
# write_config()
# ------------
sub write_config {
    my ($self, $args) = @_;

    my $rackman = $args->{rackman};
    my $path    = $rackman->config->val(general => "path");

    # generate the configuration file
    my $file = $self->fetch_from_database($args);

    # write the configuration file on disk, to its final path
    $file->path($path);
    print "  + writing ", $file->fullpath, $/ if $args->{verbose};
    my $scm = $rackman->get_scm({ path => $path });
    $scm->update;
    $file->write;
    $scm->add($file->name);
    $scm->commit($file->name, "generated by ".__PACKAGE__
        . " / $::PROGRAM v$::VERSION");
}


#
# diff_config()
# -----------
sub diff_config {
    my ($self, $args) = @_;

    # fetch config from the device
    my $actual   = $self->fetch_from_device($args);

    # generate the config from the database
    my $expected = $self->fetch_from_database($args);

    # prepare the files
    my @cfg_actual    = split $/, $actual->content;
    my @cfg_expected  = split $/, $expected->content;

    # .. remove bits here and there that will generate useless diffs
    s|generated on \S+ at \S+|generated|,
    s|; Rack PDU Feature Not Supported *||
        for @cfg_actual, @cfg_expected;

    # computes the differences between the two files
    print "  - computing differences..." if $args->{verbose};
    my @diff = diff_lines(\@cfg_actual, \@cfg_expected);
    print " done\n\n" if $args->{verbose};
    print @diff;

    RackMan->set_status(1) if @diff;
}


#
# push_config()
# -----------
sub push_config {
    my ($self, $args) = @_;

    my $rackman = $args->{rackman};
    my $path    = $rackman->config->val(general => "path");
    my $file    = RackMan::File->new({ path => $path, name => CONFIG_FILENAME });

    # read the file from disk
    $file->read;

    # store it on the device
    $self->store_to_device($args, $file);
}


#
# fetch_from_database()
# -------------------
sub fetch_from_database {
    my ($self, $args) = @_;

    my $rackman = $args->{rackman};
    my $host    = $self->attributes->{FQDN} || $self->object_name;
    my ($node, $domain) = split /\./, $host;

    # get the current device config
    my $file    = $self->fetch_from_device($args);

    my $rt_url      = $rackman->config->val(general => "racktables_url");
    my $short_url   = $rackman->config->val(general => "short_url");
    my $nagios_url  = $rackman->config->val(general => "nagios_url");
    s:/$:: for $nagios_url, $rt_url;

    # parse the config file
    my $config = Config::IniFiles->new(-file => $file->fullpath)
        or RackMan->error("can't parse config file: ", @Config::IniFiles::errors);

    # network settings
    $config->setval("NetworkTCP/IP", "HostName", $node);
    $config->setval("NetworkTCP/IP", "DomainName", $domain);

    # DNS settings
    my @dns_servers = split / +/,
        $rackman->config->val(general => "dns_servers");
    push @dns_servers, "0.0.0.0", "0.0.0.0";
    $config->setval("NetworkDNS", "DNSManualOverride", "enabled");
    $config->setval("NetworkDNS", "PrimaryDNSServerIP", $dns_servers[0]);
    $config->setval("NetworkDNS", "SecondaryDNSServerIP", $dns_servers[1]);

    # email settings
    my $mail_address = $rackman->config->val(CONFIG_SECTION, "mail_address");
    $config->setval("NetworkEMAIL", "EmailServerName",
        $rackman->config->val(general => "mail_server"));
    $config->setval("NetworkEMAIL", "EmailFromName", $mail_address);
    $config->setval("NetworkEMAIL", "EmailReceiver1Address", $mail_address);
    $config->setval("NetworkEMAIL", "EmailReceiver1Enable", "enabled");

    # system information
    $config->setval("SystemID", "Name", $host);
    $config->setval("SystemID", "Contact", $mail_address);
    $config->setval("SystemID", "Location",
        $self->rack->{name}.' @ '.$self->rack->{row_name});

    # NTP settings
    my @ntp_servers = split / +/,
        $rackman->config->val(general => "ntp_servers");
    push @ntp_servers, "0.0.0.0", "0.0.0.0";
    $config->setval("SystemDate/Time", "NTPEnable", "enabled");
    $config->setval("SystemDate/Time", "NTPPrimaryServer", $ntp_servers[0]);
    $config->setval("SystemDate/Time", "NTPSecondaryServer", $ntp_servers[1]);
    $config->setval("SystemDate/Time", "NTPTimeZone", "+01:00");

    # set the first bottom link to the corresponding page in RackTables
    if ($rt_url) {
        my $url = sprintf "$rt_url/index.php?page=object&object_id=%d",
                $self->object_id;
        $config->setval("SystemLinks", "LinkURL1", $url);
        $config->setval("SystemLinks", "LinkDisplay1", "RackTables");
        $config->setval("SystemLinks", "LinkName1",
            "link to $node\'s page on RackTables");
    }

    # set the second bottom link to the corresponding page in Nagios
    if ($nagios_url) {
        my $url = "$nagios_url/cgi-bin/extinfo.cgi?type=1&host=$host";
        $config->setval("SystemLinks", "LinkURL2", $url);
        $config->setval("SystemLinks", "LinkDisplay2", "Nagios");
        $config->setval("SystemLinks", "LinkName2",
            "link to $node\'s page on Nagios");
    }

    # set the third bottom link to the corresponding page on APC site
    $config->setval("SystemLinks", "LinkURL3",
        $self->attributes->{"HW type link"});
    $config->setval("SystemLinks", "LinkDisplay3",
        $self->attributes->{"HW type"}." specifications");
    $config->setval("SystemLinks", "LinkName3",
        "link to this model's specifications page on APC site");

    # the config format differs mostly for outlets settings, so we
    # detect the format version by looking the section present in
    # the file
    my ($outlet_sect, $outlet_name, $outlet_link)
        = $config->SectionExists("RackPDUOutlet")
        ? qw< RackPDUOutlet Name OutletLink >                 # Rack PDU v3
        : qw< Outlet OUTLET_NAME_A OUTLET_EXTERNAL_LINK_A >;  # Rack PDU v5

    # outlets settings
    for my $port (grep { $_->{oif_name} eq INTERFACE_NAME } @{ $self->ports }) {
        my ($name, $url) = ("(unused)", "#");

        if ($port->{peer_object_name}) {
            # note: an outlet name can be 23 chars max
            ($name) = split /\./, $port->{peer_object_name};
            my $max = 23 - 1 - length $port->{peer_port_name};
            substr($name, $max-1) = "~" if length $name > $max;
            $name .= ":$port->{peer_port_name}";
        }

        # construct the RackTables url for the peer device, if any
        if ($short_url and $port->{peer_object_id}) {
            # note: an outlet URL can be 60 chars max
            $url = "$short_url?oi=$port->{peer_object_id}"
                 . "&pi=$port->{peer_port_id}";

            substr($url , 59) = "~" if length $url  > 60;
        }

        my $n = int $port->{name};
        $config->setval($outlet_sect, "$outlet_name$n", $name);
        $config->setval($outlet_sect, "$outlet_link$n", $url);
    }

    # write the modified config back on disk
    $config->RewriteConfig;
    $file->read;

    return $file
}


#
# fetch_from_device()
# -----------------
sub fetch_from_device {
    my ($self, $args) = @_;

    my $rackman = $args->{rackman};
    my $host    = $self->attributes->{FQDN} || $self->object_name;
    my $login   = $rackman->options->{device_login}
               || $rackman->config->val(CONFIG_SECTION, "ftp_login", "apc");
    my $passwd  = $rackman->options->{device_password}
               || $rackman->config->val(CONFIG_SECTION, "ftp_password", "apc");

    # create a temporary directory to work in there
    my $olddir = getcwd();
    my $tmpdir = tempdir(CLEANUP => 1);

    # fetch the config file from the device using FTP
    print "  < fetching from device via FTP." if $args->{verbose};
    # .. connect
    my $ftp = Net::FTP->new($host)
        or RackMan->error_n("could not connect to '$host': $@");
    $ftp->login($login, $passwd)
        or RackMan->error_n("could not login: ", $ftp->message);

    # .. configure, chdir
    print "." if $args->{verbose};
    $ftp->ascii;
    $ftp->cwd("/")
        or RackMan->error_n("could not change working directory: ", $ftp->message);

    # .. fetch the file
    print "." if $args->{verbose};
    chdir $tmpdir;
    $ftp->get(CONFIG_FILENAME)
        or RackMan->error_n("could not fetch file: ", $ftp->message);
    chdir $olddir;
    $ftp->quit;
    print " done\n" if $args->{verbose};

    # put the config file in a RackMan:File instance
    my $file = RackMan::File->new({ path => $tmpdir, name => CONFIG_FILENAME });
    $file->read;


    return $file
}


#
# store_to_device()
# ---------------
sub store_to_device {
    my ($self, $args, $file) = @_;

    my $rackman = $args->{rackman};
    my $me      = $::COMMAND.": ".(caller(0))[3];
    my $host    = $self->attributes->{FQDN} || $self->object_name;
    my $login   = $rackman->options->{device_login}
               || $rackman->config->val(CONFIG_SECTION, "ftp_login", "apc");
    my $passwd  = $rackman->options->{device_password}
               || $rackman->config->val(CONFIG_SECTION, "ftp_password", "apc");
    my $community = $rackman->options->{write_community}
        || $rackman->config->val(CONFIG_SECTION, "write_community", "private");

    # check argument
    eval { $file->isa("RackMan::File") }
        or croak "argument must be a RackMan::File instance";

    # ensure the file is written on disk
    $file->write;

    # store the config file to the device using FTP
    print "  > storing to device via FTP." if $args->{verbose};
    # .. connect
    my $ftp = Net::FTP->new($host)
        or RackMan->error_n("could not connect to '$host': $@");
    $ftp->login($login, $passwd)
        or RackMan->error_n("could not login: ", $ftp->message);

    # .. configure, chdir
    print "." if $args->{verbose};
    $ftp->ascii;
    $ftp->cwd("/")
        or RackMan->error_n("could not change working directory: ", $ftp->message);

    # .. send the file
    print "." if $args->{verbose};
    $ftp->put($file->fullpath, CONFIG_FILENAME)
        or RackMan->error_n("could not store file: ", $ftp->message);
    $ftp->quit;
    print " done\n" if $args->{verbose};

    # send the SNMP request to make the PDU reboot
    #   mcontrolRestartAgent = restartCurrentAgent(1)
    my ($session, $error);
    ($session, $error) = Net::SNMP->session(
        -hostname   => $host, 
        -community  => $community,
        -version    => 2,
    ) or RackMan->error("can't create SNMP session: $error");

    my $result = $session->set_request(
        -varbindlist    => [ PDU_RESTART_OID, INTEGER, 1 ],
    ) or RackMan->error("could not execute SNMP set request: ", $session->error);
}


__PACKAGE__

__END__

=head1 NAME

RackMan::Device::PDU::APC_RackPDU - Role for APC Rack PDUs

=head1 DESCRIPTION

This module is the role for APC Rack PDUs.

It works by fetching the configuration from the corresponding PDU
(obviously, you need a network access to it) and updating it with
the values from the RackTables database. The resulting configuration
file can then be compared with the original or pushed back to the
device.


=head1 PUBLIC METHODS

=head2 write_config

Write the PDU configuration file, F<config.ini>, in the directory
specified in RackMan's config (section C<[general]>, parameter C<path>).

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head2 diff_config

Fetch the PDU configuration from the device twice, modify one according
to the database, compute and print the differences between the two.

The exit status of the program is set to 1 if there are differences,
0 otherwise.

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head2 push_config

Push the configuration from disk (where C<write_config> wrote it)
to the PDU device.

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head1 INTERNAL METHODS

=head2 fetch_from_database

Fetch the PDU configuration from the device via FTP and modify it
according to the information from the RackTables database.

B<Arguments:>

=over

=item 1. options (hashref)

=back

B<Return:>

=over

=item *

PDU configuration (RackMan::File instance)

=back


=head2 fetch_from_device

Fetch the PDU configuration from the device via FTP.

B<Arguments:>

=over

=item 1. options (hashref)

=back

B<Return:>

=over

=item *

PDU configuration (RackMan::File instance)

=back


=head2 store_to_device

Store the PDU configuration to the device via FTP.

B<Arguments:>

=over

=item 1. options (hashref)

=item 2. PDU configuration (RackMan::File instance)

=back


=head1 CONFIGURATION

See L<rack/"Section [device:pdu:apc_rackpdu]">


=head1 AUTHOR

Sebastien Aperghis-Tramoni

=cut