package RackMan::Device::Server::HP_ProLiant;

use Moose::Role;
use Net::ILO;
use RackMan;
use Socket;
use Term::ANSIColor qw< GREEN RED >;
use namespace::autoclean;


use constant {
    CONFIG_SECTION  => "device:server:hp_proliant",
};

use constant SERIAL_SPEED => qw< 0 9600 19200 38400 57600 115200 >;


#
# additional object attributes
#
has ilo_mac_addr => (
    is => "ro",
    isa => "HashRef",
    lazy => 1,
    builder => "_get_ilo_mac_addr",
);

has ilo_ipv4addr => (
    is => "ro",
    isa => "HashRef",
    lazy => 1,
    builder => "_get_ilo_ipv4addr",
);

has default_ilo_ipv4_gateway => (
    is => "ro",
    isa => "HashRef",
    lazy => 1,
    builder => "_get_default_ilo_ipv4_gateway",
);

has ilo_fqdn => (
    is => "ro",
    isa => "Str",
    lazy => 1,
    builder => "_get_ilo_fqdn",
);

has has_ilo => (
    is => "ro",
    isa => "Int",
    default => 1,
);


#
# _get_ilo_mac_addr()
# -----------------
sub _get_ilo_mac_addr {
    for my $port (@{ $_[0]->ports }) {
        return $port if $port->{name} eq "ilo"
    } 

    return
}


#
# _get_ilo_ipv4addr()
# -----------------
sub _get_ilo_ipv4addr {
    for my $addr (@{ $_[0]->ipv4addrs}) {
        if ($addr->{type} eq "regular" and $addr->{iface} eq "ilo") {
            return $addr
        }
    }

    return
}


#
# _get_ilo_fqdn()
# -------------
sub _get_ilo_fqdn {
    my ($self) = @_;

    my $ilo_addr = $self->ilo_ipv4addr->{addr};
    my $ilo_fqdn = gethostbyaddr(inet_aton($ilo_addr), AF_INET);

    RackMan->error("the iLO subsystem of ", $self->object_name,
        " has an IP address ($ilo_addr) but no associated FQDN; please",
        " check the reverse record") unless $ilo_fqdn;

    return $ilo_fqdn
}


#
# _get_default_ilo_ipv4_gateway()
# -------------------------
sub _get_default_ilo_ipv4_gateway {
    my ($self) = @_;
    my @addrs = $self->ilo_ipv4addr;
    return $self->_get_default_ipv4_gateway($addrs[0]{addr})
}


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

    print "  ! This action is not implemented. ",
        "Please use the 'push' action instead.\n"
        if $args->{verbose};
}


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

    my $rackman = $args->{rackman};
    my $config  = $rackman->config;

    # fetch the credentials
    my $ilo_password = $rackman->options->{device_password}
        || $config->val(CONFIG_SECTION, "ilo_password")
        or RackMan->error("missing password for iLO account 'Administrator'");

    # fetch the IP address and FQDN of the iLO subsystem
    my $ilo_addr = $self->ilo_ipv4addr->{addr};
    my $ilo_fqdn = $self->ilo_fqdn;
    my ($ilo_name, $ilo_domain) = split /\./, $ilo_fqdn;

    # connect to the iLO API
    my $ilo = Net::ILO->new({
        address  => $ilo_addr,
        username => "Administrator",
        password => $ilo_password,
    });

    my $ilo_type = $ilo->fw_type or RackMan->error($ilo->error);

    print "  $ilo_type v", $ilo->fw_version, " / ", $ilo->model, "\n"
        if $args->{verbose};

    # construct the expected values
    my $serial_speed = $config->val(CONFIG_SECTION, "serial_cli_speed", 5);
    my %expected = (
        hostname            => $ilo_name,
        domain_name         => $ilo_domain,
        server_name         => $self->object_name,
        serial_cli_speed    => (SERIAL_SPEED)[$serial_speed],
    );

    my @dns_servers = split / +/, $config->val(general => "dns_servers");
    my @dns_fields = qw< prim_dns_server sec_dns_server ter_dns_server >;
    $expected{$dns_fields[$_]} = $dns_servers[$_] for 0 .. $#dns_servers;

    if ($ilo->_version >= 3) {
        my @ntp_servers = split / +/, $config->val(general => "ntp_servers");
        my @ntp_fields = qw< sntp_server1 sntp_server2 >;
        $expected{$ntp_fields[$_]} = $ntp_servers[$_] for 0 .. $#ntp_servers;
    }

    # fetch the corresponding current values
    my %current = map { $_ => $ilo->$_ || "(undef)" } keys %expected;

    my $diff = 0;

    # compare the two hashes and print the differences
    for my $field (sort keys %expected) {
        next if $current{$field} eq $expected{$field};
        my $sp = " " x length $field;
        print "  - $field: ", RED("current value: $current{$field}"), "\n",
              "    $sp ", GREEN("expected value: $expected{$field}"), "\n\n";
        $diff = 1;
    }

    # check if the user "admin" is present
    my $user_info = $ilo->get_user("admin");

    if ($user_info) {
        if (not $user_info->{admin} =~ /^y(?:es)?/i) {
            print "  - account 'admin' exists but lack admin privilege\n";
            $diff = 1;
        }
    }
    else {
        print "  - account 'admin' does not exist\n";
        $diff = 1;
    }

    print "  - no differences\n" if !$diff and $args->{verbose};

    RackMan->set_status($diff);
}


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

    my $rackman = $args->{rackman};
    my $config  = $rackman->config;

    # fetch the credentials
    my $ilo_password = $rackman->options->{device_password}
        || $config->val(CONFIG_SECTION, "ilo_password")
        or RackMan->error("missing password for iLO account 'Administrator'");
    my $admin_password = $config->val(CONFIG_SECTION, "admin_password")
        or RackMan->error("missing password for iLO account 'admin'");

    # fetch the IP address and FQDN of the iLO subsystem
    my $ilo_addr = $self->ilo_ipv4addr->{addr};
    my $ilo_fqdn = $self->ilo_fqdn;
    my ($ilo_name, $ilo_domain) = split /\./, $ilo_fqdn;

    # connect to the iLO API
    print "  > configuring iLO subsystem" if $args->{verbose};
    my $ilo = Net::ILO->new({
        address  => $ilo_addr,
        username => "Administrator",
        password => $ilo_password,
    });

    my $ilo_type = $ilo->fw_type or RackMan->error_n($ilo->error);
    print " (" if $args->{verbose};

    # configure serial port
    print "serial CLI, " if $args->{verbose};
    my $serial_speed = $config->val(CONFIG_SECTION, "serial_cli_speed", 5);
    $ilo->serial_cli_speed($serial_speed)
        or RackMan->error_n($ilo->error);

    # configure the server name
    print "server name, " if $args->{verbose};
    $ilo->server_name($self->object_name)
        or RackMan->error_n($ilo->error);

    # check if the user "admin" is present
    print "user 'admin', " if $args->{verbose};
    my $user_info = $ilo->get_user("admin");

    if (not $user_info) {
        # if not, create it
        $ilo->add_user({
            name        => "admin",
            username    => "admin",
            password    => $admin_password,
            admin       => "Yes",
            remote_console_privilege    => "Yes",
            reset_privilege             => "Yes",
            virtual_media_privilege     => "Yes",
            config_ilo_privilege        => "Yes",
        }) or RackMan->error_n($ilo->error);
    }
    else {
        # if yes, change its password
        $ilo->mod_user({
            username    => "admin",
            password    => $admin_password,
        }) or RackMan->error_n($ilo->error);
    }

    # activate the license
    print "license, " if $args->{verbose};
    my $license_key = $config->val(CONFIG_SECTION, "license_key");

    if ($license_key and not $ilo->license($license_key)) {
        RackMan->error_n($ilo->error)
            unless $ilo->error =~ /already active/;
    }

    # complete network settings
    print "network" if $args->{verbose};
    my @ntp_servers = split / +/, $config->val(general => "ntp_servers");
    my @dns_servers = split / +/, $config->val(general => "dns_servers");

    $ilo->network({
        hostname        => $ilo_name,
        dhcp_enabled    => 'no',
        domain_name     => $ilo_domain,
        prim_dns_server => $dns_servers[0],
        sec_dns_server  => $dns_servers[1],
        ter_dns_server  => $dns_servers[2],
        sntp_server1    => $ntp_servers[0],
        sntp_server2    => $ntp_servers[1],
        sntp_server3    => $ntp_servers[2],
    }) or RackMan->error_n($ilo->error);

    print ") done\n" if $args->{verbose};
}


#
# tmpl_params()
# -----------
around tmpl_params => sub {
    my ($orig, $self) = @_;

    my $name = $self->object_name;

    # fetch the IP address and FQDN of the iLO subsystem
    my $ilo_addr = $self->ilo_ipv4addr->{addr};
    my $ilo_fqdn = $self->ilo_fqdn;
    my ($ilo_name, $ilo_domain) = split /\./, $ilo_fqdn;

    # find iLO MAC address
    my @ilo_mac_addr = $self->ilo_mac_addr;
    RackMan->error("RackObject '$name' lacks an iLO interface")
        unless defined $ilo_mac_addr[0];
    RackMan->error("RackObject '$name' lacks an iLO MAC address")
        unless defined $ilo_mac_addr[0]{l2address};

    # fetch iLO IPv4 address
    my @ilo_ipv4addr = $self->ilo_ipv4addr;
    RackMan->error("RackObject '$name' lacks an iLO IPv4 address")
        unless defined $ilo_ipv4addr[0]{addr};

    # iLO IPv4 network parameters
    my $ilo_gateway = $self->default_ilo_ipv4_gateway;
    my $iaddr = NetAddr::IP->new($ilo_ipv4addr[0]{addr}, $ilo_gateway->{masklen});
    my $ilo_netmask = $iaddr->mask;

    my %params = (
        $self->$orig,
        ilo_fqdn    => $ilo_fqdn,
        ilo_name    => $ilo_name,
        ilo_ip      => $ilo_ipv4addr[0]{addr},
        ilo_mac     => $ilo_mac_addr[0]{l2address_text},
        ilo_netmask => $ilo_netmask,
        ilo_network => $ilo_gateway->{network},
        ilo_gateway => $ilo_gateway->{addr} || "0.0.0.0",
    );

    return %params
};



__PACKAGE__

__END__

=head1 NAME

RackMan::Device::Server::HP_ProLiant - Role for HP ProLiant servers

=head1 DESCRIPTION

This module is the role for HP ProLiant servers, to configure their
iLO management subsystem.

Because iLO is a protocol, no configuration file can be written on
disk, therefore the C<write> action is not implemented.


=head1 PUBLIC METHODS

=head2 write_config

I<Not implemented>


=head2 diff_config

Because there is no configuration file I<per se>, this action can't
perform a traditional diff. However, it can and does compare some of
the values fetched from the iLO subsystem with the expected values,
as computed from the RackTables database.

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head2 push_config

Talk with the iLO subsystem of the target server to configure it
accordingly to the information from the RackTables database.

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head2 tmpl_params

Return a hash of additional template parameters.
See L<"TEMPLATE PARAMETERS">


=head2 ilo_mac_addr

Return the MAC address of the iLO subsystem


=head2 ilo_ipv4addr

Return the IPv4 address of the iLO subsystem


=head1 TEMPLATE PARAMETERS

This role provides the following additional template parameters:

=over

=item *

C<ilo_fqdn> - FQDN of the iLO subsystem

=item *

C<ilo_name> - domain name of the iLO subsystem

=item *

C<ilo_ip> - IPv4 address of the iLO subsystem

=item *

C<ilo_mac> - MAC address of the iLO subsystem

=item *

C<ilo_gateway> - gateway of the iLO subsystem

=item *

C<ilo_netmask> - netmask of the iLO subsystem

=item *

C<ilo_network> - IPv4 network of the iLO subsystem

=back


=head1 CONFIGURATION

See L<rack/"Section [device:server:hp_proliant]">


=head1 AUTHOR

Sebastien Aperghis-Tramoni

=cut