package perfSONAR_PS::Collectors::LinkStatus;

use strict;
use warnings;
use Log::Log4perl qw(get_logger);
use Time::HiRes qw( gettimeofday );
use Module::Load;

use perfSONAR_PS::Common;
use perfSONAR_PS::DB::File;
use perfSONAR_PS::Client::Status::MA;
use perfSONAR_PS::Status::Common;
use perfSONAR_PS::Collectors::LinkStatus::Link;
use perfSONAR_PS::Collectors::LinkStatus::Agent::SNMP;
use perfSONAR_PS::Collectors::LinkStatus::Agent::Script;
use perfSONAR_PS::Collectors::LinkStatus::Agent::Constant;

use perfSONAR_PS::SNMPWalk;

use base 'perfSONAR_PS::Collectors::Base';

use fields 'CLIENT', 'LINKS', 'LINKSBYID', 'SNMPAGENTS';

our $VERSION = 0.05;

my %link_prev_update_status = ();

sub new {
    my ($self, $conf, $directory) = @_;

    $self = fields::new($self) unless ref $self;
    $self->SUPER::new($conf, $directory);
    return $self;
}

sub init($) {
    my ($self) = @_;
    my $logger = get_logger("perfSONAR_PS::Collectors::LinkStatus");

    $logger->debug("init()");

    if (!defined $self->{CONF}->{"link_file_type"} or $self->{CONF}->{"link_file_type"} eq "") {
        $logger->error("no link file type specified");
        return -1;
    }

    if($self->{CONF}->{"link_file_type"} ne "file") {
        $logger->error("invalid link file type specified: " . $self->{CONF}->{"link_file_type"});
        return -1;
    }

    if ($self->parseLinkFile($self->{CONF}->{"link_file"}, $self->{CONF}->{"link_file_type"}) != 0) {
        $logger->error("couldn't load links to measure");
        return -1;
    }

    if (defined $self->{CONF}->{"ma_type"}) {
        if (lc($self->{CONF}->{"ma_type"}) eq "sqlite") {
            load perfSONAR_PS::Client::Status::SQL;

            if (!defined $self->{CONF}->{"ma_file"} or $self->{CONF}->{"ma_file"} eq "") {
                $logger->error("You specified a SQLite Database, but then did not specify a database file(ma_file)");
                return -1;
            }

            my $file = $self->{CONF}->{"ma_file"};
            if (defined $self->{DIRECTORY}) {
                if (!($file =~ "^/")) {
                    $file = $self->{DIRECTORY}."/".$file;
                }
            }

            $self->{CLIENT} = perfSONAR_PS::Client::Status::SQL->new("DBI:SQLite:dbname=".$file, $self->{CONF}->{"ma_table"});
        } elsif (lc($self->{CONF}->{"ma_type"}) eq "ma") {
            if (!defined $self->{CONF}->{"ma_uri"} or $self->{CONF}->{"ma_uri"} eq "") {
                $logger->error("You specified to use an MA, but did not specify which one(ma_uri)");
                return -1;
            }

            $self->{CLIENT} = perfSONAR_PS::Client::Status::MA->new($self->{CONF}->{"ma_uri"});
        } elsif (lc($self->{CONF}->{"ma_type"}) eq "mysql") {
            load perfSONAR_PS::Client::Status::SQL;

            my $dbi_string = "dbi:mysql";

            if (!defined $self->{CONF}->{"ma_name"} or $self->{CONF}->{"ma_name"} eq "") {
                $logger->error("You specified a MySQL Database, but did not specify the database (ma_name)");
                return -1;
            }

            $dbi_string .= ":".$self->{CONF}->{"ma_name"};

            if (!defined $self->{CONF}->{"ma_host"} or $self->{CONF}->{"ma_host"} eq "") {
                $logger->error("You specified a MySQL Database, but did not specify the database host (ma_host)");
                return -1;
            }

            $dbi_string .= ":".$self->{CONF}->{"ma_host"};

            if (defined $self->{CONF}->{"ma_port"} and $self->{CONF}->{"ma_port"} ne "") {
                $dbi_string .= ":".$self->{CONF}->{"ma_port"};
            }

            $self->{CLIENT} = perfSONAR_PS::Client::Status::SQL->new($dbi_string, $self->{CONF}->{"ma_username"}, $self->{CONF}->{"ma_password"});
            if (!defined $self->{CLIENT}) {
                my $msg = "Couldn't create SQL client";
                $logger->error($msg);
                return (-1, $msg);
            }
        }
    } else {
        $logger->error("Need to specify a location to store the status reports");
        return -1;
    }

    my ($status, $res) = $self->{CLIENT}->open;
    if ($status != 0) {
        my $msg = "Couldn't open newly created client: $res";
        $logger->error($msg);
        return -1;
    }

    $self->{CLIENT}->close;

    return 0;
}

sub parseLinkFile($$$) {
    my($self, $file, $type) = @_;
    my $logger = get_logger("perfSONAR_PS::Collectors::LinkStatus");
    my $links_config;

    if (defined $self->{DIRECTORY}) {
        if (!($file =~ "^/")) {
            $file = $self->{DIRECTORY}."/".$file;
        }
    }

    my $filedb = perfSONAR_PS::DB::File->new($file);
    $filedb->openDB;
    $links_config = $filedb->getDOM();

    $self->{LINKSBYID} = ();

    foreach my $link ($links_config->getElementsByTagName("link")) {
        my ($status, $res) = $self->parseLinkElement($link);
        if ($status != 0) {
            my $msg = "Failure parsing link element: $res";
            $logger->error($msg);
            return -1;
        }

        my $parsed_link = $res;

        push @{ $self->{LINKS} }, $parsed_link;

        foreach my $id ($parsed_link->getIDs()) {
            if (defined $self->{LINKSBYID}->{$id}) {
                $logger->error("Tried to redefine link $id");
                return -1;
            }

            $self->{LINKSBYID}->{$id} = $parsed_link;
        }
    }

    return 0;
}

sub parseLinkElement($$) {
    my ($self, $link_desc) = @_;
    my $logger = get_logger("perfSONAR_PS::Collectors::LinkStatus");

    my $link = perfSONAR_PS::Collectors::LinkStatus::Link->new();

    my $knowledge = $link_desc->getAttribute("knowledge");
    if (!defined $knowledge) {
        my $msg = "It is not stated whether or knowledge is full or partial";
        $logger->error($msg);
        return -1;
    }

    $link->setKnowledge($knowledge);

    foreach my $id_elm ($link_desc->getElementsByTagName("id")) {
        my $id = $id_elm->textContent;

        $link->addID($id);
    }

    if (scalar($link->getIDs()) == 0) {
        my $msg = "No ids associated with specified link";
        $logger->error($msg);
        return (-1, $msg);
    }

    my $primary_time_source;

    foreach my $agent ($link_desc->getElementsByTagName("agent")) {
        my ($status, $res);

        # The following sections for grabbing 'operStatus' and
        # 'adminStatus' subfields in the 'agent' are in there for
        # backwards compatibility reasons and are deprecated.
        my $oper_info = $agent->find('operStatus')->shift;
        if (defined $oper_info) {
            my $oper_agent_ref;

            ($status, $oper_agent_ref) = $self->parseAgentElement($oper_info, "oper");
            if ($status != 0) {
                $logger->error("Problem parsing operational status agent for link");
                return -1;
            }

            my $is_time_source = $oper_info->getAttribute("primary_time_source");
            if (defined $is_time_source and $is_time_source eq "1") {
                if (defined $primary_time_source) {
                    my $msg = "Link has multiple primary time sources";
                    $logger->error($msg);
                    return (-1, $msg);
                }

                $logger->debug("Setting primary time source");

                $primary_time_source = $oper_agent_ref;
            }

            $link->addAgent($oper_agent_ref);
        }

        my $admin_info = $agent->find('adminStatus')->shift;
        if (defined $admin_info) {
            my $admin_agent_ref;

            ($status, $admin_agent_ref) = $self->parseAgentElement($admin_info, "admin");
            if ($status != 0) {
                $logger->error("Problem parsing adminstrative status agent for link");
                return -1;
            }

            my $is_time_source = $admin_info->getAttribute("primary_time_source");
            if (defined $is_time_source and $is_time_source eq "1") {
                if (defined $primary_time_source) {
                    my $msg = "Link has multiple primary time sources";
                    $logger->error($msg);
                    return (-1, $msg);
                }

                $logger->debug("Setting primary time source");

                $primary_time_source = $admin_agent_ref;
            }

            $link->addAgent($admin_agent_ref);
        }

        if (defined $oper_info or defined $admin_info) {
            next;
        }

        ($status, $res) = $self->parseAgentElement($agent, "");
        if ($status != 0) {
            my $msg = "Problem parsing operational status agent for link: $res";
            $logger->error($msg);
            return (-1, $msg);
        }

        my $is_time_source = $agent->getAttribute("primary_time_source");
        if (defined $is_time_source and $is_time_source eq "1") {
            if (defined $primary_time_source) {
                my $msg = "Link has multiple primary time sources";
                $logger->error($msg);
                return (-1, $msg);
            }

            $logger->debug("Setting primary time source");

            $primary_time_source = $res;
        }

        $link->addAgent($res);
    }

    $link->setPrimaryTimeSource($primary_time_source);

    if (scalar($link->getAgents()) == 0) {
        my $msg = "Didn't specify any agents for link";
        $logger->error($msg);
        return (-1, $msg);
    }

    return (0, $link);
}

sub parseAgentElement($$$) {
    my ($self, $agent, $status_type) = @_;
    my $logger = get_logger("perfSONAR_PS::Collectors::LinkStatus");

    my $new_agent;

    if ($status_type eq "") {
        $status_type = $agent->getAttribute("status_type");
        if (!defined $status_type) {
            my $msg = "Agent does not contain a status_type attribute stating which status (operational or administrative) it returns";
            $logger->error($msg);
            return (-1, $msg);
        }
    }

    if ($status_type ne "oper" and $status_type ne "operational" and $status_type ne "admin" and $status_type ne "administrative") {
        my $msg = "Agent's stated status_type is neither 'oper' nor 'admin'";
        $logger->error($msg);
        return (-1, $msg);
    }

    my $type = $agent->getAttribute("type");
    if (!defined $type or $type eq "") {
        my $msg = "Agent has no type information";
        $logger->debug($msg);
        return (-1, $msg);
    }

    if ($type eq "script") {
        my $script_name = $agent->findvalue("script_name");
        if (!defined $script_name or $script_name eq "") {
            my $msg = "Agent of type 'script' has no script name defined";
            $logger->debug($msg);
            return (-1, $msg);
        }

        if (defined $self->{DIRECTORY}) {
            if (!($script_name =~ "^/")) {
                $script_name = $self->{DIRECTORY}."/".$script_name;
            }
        }

        if (!-x $script_name) {
            my $msg = "Agent of type 'script' has non-executable script: \"$script_name\"";
            $logger->debug($msg);
            return (-1, $msg);
        }

        my $script_params = $agent->findvalue("script_parameters");

        $new_agent = perfSONAR_PS::Collectors::LinkStatus::Agent::Script->new($status_type, $script_name, $script_params);
    } elsif ($type eq "constant") {
        my $value = $agent->findvalue("constant");
        if (!defined $value or $value eq "") {
            my $msg = "Agent of type 'constant' has no value defined";
            $logger->debug($msg);
            return (-1, $msg);
        }

        $new_agent = perfSONAR_PS::Collectors::LinkStatus::Agent::Constant->new($status_type, $value);
    } elsif ($type eq "snmp") {
        my $oid = $agent->findvalue("oid");
        if (!defined $oid or $oid eq "") {
            if ($status_type eq "oper") {
                $oid = "1.3.6.1.2.1.2.2.1.8";
            } elsif ($status_type eq "admin") {
                $oid = "1.3.6.1.2.1.2.2.1.7";
            }
        }

        my $hostname = $agent->findvalue('hostname');
        if (!defined $hostname or $hostname eq "") {
            my $msg = "Agent of type 'SNMP' has no hostname";
            $logger->error($msg);
            return (-1, $msg);
        }

        my $ifName = $agent->findvalue('ifName');
        my $ifIndex = $agent->findvalue('ifIndex');

        if ((!defined $ifIndex or $ifIndex eq "") and (!defined $ifName or $ifName eq "")) {
            my $msg = "Agent of type 'SNMP' has no name or index specified";
            $logger->error($msg);
            return (-1, $msg);
        }

        my $version = $agent->findvalue("version");
        if (!defined $version or $version eq "") {
            my $msg = "Agent of type 'SNMP' has no snmp version";
            $logger->error($msg);
            return (-1, $msg);
        }

        my $community = $agent->findvalue("community");
        if (!defined $community or $community eq "") {
            my $msg = "Agent of type 'SNMP' has no community string";
            $logger->error($msg);
            return (-1, $msg);
        }

        if (!defined $self->{SNMPAGENTS}->{$hostname}) {
            $self->{SNMPAGENTS}->{$hostname} = perfSONAR_PS::Collectors::LinkStatus::Agent::SNMP::Host->new( $hostname, "" , $version, $community, "");
        }

        if (!defined $ifIndex or $ifIndex eq "") {
            $logger->debug("Looking up $ifName from $hostname");

            my ($status, $res) = snmpwalk($hostname, undef, "1.3.6.1.2.1.31.1.1.1.1", $community, $version);
            if ($status != 0) {
                my $msg = "Error occurred while looking up ifIndex for specified ifName $ifName: $res";
                $logger->error($msg);
                return (-1, $msg);
            }

            foreach my $oid_ref ( @{ $res } ) {
                my $oid = $oid_ref->[0];
                my $type = $oid_ref->[1];
                my $value = $oid_ref->[2];

                $logger->debug("$oid = $type: $value($ifName)");
                if ($value eq $ifName) {
                    if ($oid =~ /1\.3\.6\.1\.2\.1\.31\.1\.1\.1\.1\.(\d+)/) {
                        $ifIndex = $1;
                    }
                }
            }

            if (!defined $ifIndex or $ifIndex eq "") {
                my $msg = "Didn't find ifName $ifName in host $hostname";
                $logger->error($msg);
                return (-1, $msg);
            }
        }

        my $old_agent;

        if (defined $self->{SNMPAGENTS}->{$hostname}) {
            $old_agent = $self->{SNMPAGENTS}->{$hostname};
        }

        $new_agent = perfSONAR_PS::Collectors::LinkStatus::Agent::SNMP->new($status_type, $hostname, $ifIndex, $version, $community, $oid, $old_agent);
    } else {
        my $msg = "Unknown agent type: \"$type\"";
        $logger->error($msg);
        return (-1, $msg);
    }

    # here is where we could pull in the possibility of a mapping from the
    # output of the SNMP/script/whatever to "up, down, degraded, unknown"

    return (0, $new_agent);
}

sub collectMeasurements($$) {
    my($self, $sleeptime) = @_;
    my $logger = get_logger("perfSONAR_PS::Collectors::LinkStatus");
    my ($status, $res);

    $logger->debug("collectMeasurements()");

    ($status, $res) = $self->{CLIENT}->open;
    if ($status != 0) {
        my $msg = "Couldn't open connection to database: $res";
        $logger->error($msg);
        return (-1, $msg);
    }

    foreach my $link (@{$self->{LINKS}}) {
        my ($status, $res);

        $logger->debug("Getting information on link: ".(@{$link->getIDs()}[0]));

        ($status, $res) = $link->measure();
        if ($status != 0) {
            $logger->warn("Couldn't get information on link: ".(@{$link->getIDs()}[0]));
            next;
        }

        my @link_statuses = @{ $res };

        foreach my $link_id (@{ $link->getIDs() }) {
            my $do_update = 0;

            foreach my $link_status (@link_statuses) {
                $logger->debug("Updating $link_id: ".$link_status->getTime()." - ".$link_status->getOperState().", ".$link_status->getAdminState());

                if (defined $link_prev_update_status{$link_id} and $link_prev_update_status{$link_id} == 0) {
                    $do_update = 1;
                }

                ($status, $res) = $self->{CLIENT}->updateLinkStatus($link_status->getTime(),
                                                                    $link_id,
                                                                    $link->getKnowledge(),
                                                                    $link_status->getOperState(),
                                                                    $link_status->getAdminState(),
                                                                    $do_update);
                if ($status != 0) {
                    $logger->error("Couldn't store link status for link $link_id: $res");
                }

                $link_prev_update_status{$link_id} = $status;
            }
        }
    }

    if ($sleeptime) {
        $sleeptime = $self->{CONF}->{"collection_interval"};
    }
}

1;

__END__

=head1 NAME

perfSONAR_PS::Collectors::LinkStatus - A module that will collect link status
information and store the results into a Link Status MA.

=head1 DESCRIPTION

This module loads a set of links and can be used to collect status information
on those links and store the results into a Link Status MA.

=head1 SYNOPSIS

=head1 DETAILS

This module is meant to be used to periodically collect information about Link
Status. It can do this by running scripts or consulting SNMP servers directly.
It reads a configuration file that contains the set of links to track. It can
then be used to periodically obtain the status and then store the results into
a measurement archive. 

It includes a submodule SNMPAgent that provides a caching SNMP poller allowing
easier interaction with SNMP servers.

=head1 API

=head2 init($self)
    This function initializes the collector. It returns 0 on success and -1
    on failure.

=head2 collectMeasurements($self)
    This function is called by external users to collect and store the
    status for all links.

=head1 SEE ALSO

To join the 'perfSONAR-PS' mailing list, please visit:

https://mail.internet2.edu/wws/info/i2-perfsonar

The perfSONAR-PS subversion repository is located at:

https://svn.internet2.edu/svn/perfSONAR-PS

Questions and comments can be directed to the author, or the mailing list.

=head1 VERSION

$Id:$

=head1 AUTHOR

Aaron Brown, E<lt>aaron@internet2.eduE<gt>, Jason Zurawski, E<lt>zurawski@internet2.eduE<gt>

=head1 LICENSE

You should have received a copy of the Internet2 Intellectual Property Framework along
with this software.  If not, see <http://www.internet2.edu/membership/ip.html>

=head1 COPYRIGHT

Copyright (c) 2004-2007, Internet2 and the University of Delaware

All rights reserved.

=cut

# vim: expandtab shiftwidth=4 tabstop=4
