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

use Moose::Role;
use RackMan;
use RackMan::File;
use RackMan::Utils;
use namespace::autoclean;


use constant {
    CONFIG_SECTION  => "device:switch:cisco_catalyst",
    CONFIG_FILENAME => "config.cmd",
};


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

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

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

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


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

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

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

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

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

    # computes the differences between the two files
    print "  - computing differences..." if $args->{verbose};
    my @diff = diff_lines(\@cfg_current, \@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 $config  = RackMan::File->new;

    # read the configuration file from disk
    $config->name(CONFIG_FILENAME);
    $config->path($path);
    $config->read;

    # send it to the device
    $self->store_to_device($args, $config);
}


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

    my $rackman = $args->{rackman};
    my $config  = RackMan::File->new;
    my %interface;

    for my $port (@{ $self->ports }) {
        next unless $port->{name} =~ /^gi/i;

        (my $name = $port->{name}) =~ s/^\D+/GigabitEthernet/;
        (my $num  = $port->{name}) =~ s/^\D+//;
        my $description = defined $port->{peer_object_name}
            ? " description $port->{peer_object_name} ($port->{peer_port_name})"
            : " no description";

        $interface{$num} = "interface $name\n$description\n!\n";
    }

    # we need to first store the generated command, then sort them
    # by the interface number (the "0/2" part in "GigabitEthernet0/2")
    $config->add_content("!\n");
    $config->add_content($interface{$_})
        for sort {
            my @a = split "/", $a;
            my @b = split "/", $b;
            $a[0] <=> $b[0] || $a[1] <=> $b[1] || $a[2] <=> $b[2]
        } keys %interface;

    return $config
}


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

    my $rackman = $args->{rackman};
    my $config  = RackMan::File->new;

    # fetch the access password
    my $ios_password = $rackman->options->{device_password}
        || $rackman->config->val(CONFIG_SECTION, "ios_password")
        or RackMan->error("missing IOS access password");

    # load the protocol module
    { local $SIG{__WARN__} = sub {}; require Net::Telnet::Cisco; }

    # connect to the device
    my $host = $self->attributes->{FQDN} || $self->object_name;
    my $session = Net::Telnet::Cisco->new(Host => $host);
    $session->login(Password => $ios_password);
    $session->cmd("terminal length 0");

    # fetch the list of interfaces with their current description
    print "  < fetching the list of interfaces... " if $args->{verbose};
    my @out = $session->cmd("show interface description");
    $config->add_content("!\n");

    print "parsing... " if $args->{verbose};
    chomp(my $line = shift @out);
    my @fields = split / +/, $line;

    for my $line (@out) {
        $line =~ s/[\x01-\x1f]//g;  # clean control chars
        next unless defined $line and length $line;

        # parse the status
        my %interface;
        $line =~ s/admin down/admin-down/;
        @interface{@fields} = split / +/, $line, ~~@fields;

        # interface name
        next unless $interface{Interface} =~ /^gi/i;
        $interface{Interface} =~ s/^\D+/GigabitEthernet/;
        $config->add_content("interface $interface{Interface}\n");

        # interface description
        $interface{Description} ||= "";
        my $description = length $interface{Description}
            ? "description $interface{Description}" : "no description";
        $config->add_content(" $description\n");

        $config->add_content("!\n");
    }

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

    return $config
}


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

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

    # fetch the access passwords
    my $ios_password = $rackman->options->{device_password}
        || $rackman->config->val(CONFIG_SECTION, "ios_password")
        or RackMan->error("missing IOS access password");
    my $enable_password
        = $rackman->config->val(CONFIG_SECTION, "enable_password")
        or RackMan->error("missing IOS admin password");

    # load the protocol module
    { local $SIG{__WARN__} = sub {}; require Net::Telnet::Cisco; }

    # connect to the device
    my $host = $self->attributes->{FQDN} || $self->object_name;
    my $session = Net::Telnet::Cisco->new(Host => $host);
    $session->login(Password => $ios_password);
    $session->enable(Password => $enable_password);

    # enter configure mode
    $session->cmd("configure terminal");
    print "  > sending commands batch... " if $args->{verbose};

    # send the commands batch to the device
    for my $cmd (split $/, $config->content) {
        $session->cmd($cmd);
    }

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

    return $config
}


__PACKAGE__

__END__

=head1 NAME

RackMan::Device::Switch::Cisco_Catalyst - Role for Cisco Catalyst network switches

=head1 DESCRIPTION

This module is the role for Cisco Catalyst network switches.


=head1 PUBLIC METHODS

=head2 write_config

Write the Cisco commands for configuring the device to a F<config.cmd>
file, 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 configuration from the device, compare it with the configuration
generated from the RackTables database and print the differences.

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

Read the configuration from disk and send it to the device.

B<Arguments:>

=over

=item 1. options (hashref)

=back


=head1 INTERNAL METHODS

=head2 fetch_from_database

Generate the Cisco commands for configuring the device accordingly to
the information from the RackTables database.

B<Arguments:>

=over

=item 1. options (hashref)

=back

B<Return:>

=over

=item *

configuration (RackMan::File instance)

=back


=head2 fetch_from_device

Fetch the list of interfaces with their description, and convert it
to the corresponding commands batch.

B<Arguments:>

=over

=item 1. options (hashref)

=back

B<Return:>

=over

=item *

commands batch (RackMan::File instance)

=back


=head2 store_to_device

Send the Cisco commands to the device.

B<Arguments:>

=over

=item 1. options (hashref)

=item 2. commands batch (RackMan::File instance)

=back


=head1 CONFIGURATION

See L<rack/"Section [device:switch:cisco_catalyst]">


=head1 AUTHOR

Sebastien Aperghis-Tramoni

=cut