The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package perfSONAR_PS::Services::MA::CircuitStatus;

use base 'perfSONAR_PS::Services::Base';

use fields
    'LOCAL_MA_CLIENT',
    'TOPOLOGY_CLIENT',
    'STORE',
    'LS',
    'DOMAIN',
    'CIRCUITS',
    'INCOMPLETE_NODES',
    'TOPOLOGY_LINKS',
    'NODES',
    'LOGGER';

use warnings;
use strict;
use Log::Log4perl qw(get_logger);
use Module::Load;
use Fcntl qw (:flock);
use Fcntl;
use Params::Validate qw(:all);

use perfSONAR_PS::Services::Base;
use perfSONAR_PS::Services::MA::General;
use perfSONAR_PS::Common;
use perfSONAR_PS::Messages;
use perfSONAR_PS::Transport;
use perfSONAR_PS::Time;
use perfSONAR_PS::Error_compat qw/:try/;

use perfSONAR_PS::Client::LS::Remote;
use perfSONAR_PS::Client::Status::MA;
use perfSONAR_PS::Client::Topology::MA;

our $VERSION = 0.09;

sub init {
    my ($self, $handler) = @_;

    $self->{LOGGER} = get_logger("perfSONAR_PS::Services::MA::CircuitStatus");

    if (not defined $self->{CONF}->{"circuitstatus"}->{"status_ma_type"} or $self->{CONF}->{"circuitstatus"}->{"status_ma_type"} eq q{}) {
        if (not defined $self->{CONF}->{"circuitstatus"}->{"ls_instance"} or $self->{CONF}->{"circuitstatus"}->{"ls_instance"} eq q{}) {
            $self->{LOGGER}->error("No LS nor Status MA specified");
            return -1;
        } else {
            $self->{CONF}->{"circuitstatus"}->{"status_ma_type"} = "ls";
        }
    }

    if (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "ls") {
        my ($host, $port, $endpoint) = &perfSONAR_PS::Transport::splitURI($self->{CONF}->{"circuitstatus"}->{"ls_instance"});
        if (not $host or not $port or not $endpoint) {
            $self->{LOGGER}->error("Specified LS is not a URI: ".$self->{CONF}->{"circuitstatus"}->{"ls_instance"});
            return -1;
        }

        $self->{LS} = $self->{CONF}->{"circuitstatus"}->{"ls_instance"};
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "ma") {
        if (not defined $self->{CONF}->{"circuitstatus"}->{"status_ma_uri"} or $self->{CONF}->{"circuitstatus"}->{"status_ma_uri"} eq q{}) {
            $self->{LOGGER}->error("You specified an MA for the status, but did not specify the URI(status_ma_uri)");
            return -1;
        }
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "sqlite") {
        load perfSONAR_PS::Client::Status::SQL;

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

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

        $self->{LOCAL_MA_CLIENT} = perfSONAR_PS::Client::Status::SQL->new("DBI:SQLite:dbname=".$file, $self->{CONF}->{"circuitstatus"}->{"status_ma_table"});
        if (not defined $self->{LOCAL_MA_CLIENT}) {
            my $msg = "No database to dump";
            $self->{LOGGER}->error($msg);
            return -1;
        }
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "mysql") {
        load perfSONAR_PS::Client::Status::SQL;

        my $dbi_string = "dbi:mysql";

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

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

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

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

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

        $self->{LOCAL_MA_CLIENT} = perfSONAR_PS::Client::Status::SQL->new($dbi_string, $self->{CONF}->{"circuitstatus"}->{"status_ma_username"}, $self->{CONF}->{"circuitstatus"}->{"status_ma_password"});
        if (not defined $self->{LOCAL_MA_CLIENT}) {
            my $msg = "Couldn't create SQL client";
            $self->{LOGGER}->error($msg);
            return -1;
        }
    } else {
        $self->{LOGGER}->error("Invalid MA type specified");
        return -1;
    }

    if (not defined $self->{CONF}->{"circuitstatus"}->{"circuits_file_type"} or $self->{CONF}->{"circuitstatus"}->{"circuits_file_type"} eq q{}) {
        $self->{LOGGER}->error("No circuits file type specified");
        return -1;
    }

    if($self->{CONF}->{"circuitstatus"}->{"circuits_file_type"} eq "file") {
        if (not defined $self->{CONF}->{"circuitstatus"}->{"circuits_file"} or $self->{CONF}->{"circuitstatus"}->{"circuits_file"} eq q{}) {
            $self->{LOGGER}->error("No circuits file specified");
            return -1;
        }

        try {
            ($self->{DOMAIN}, $self->{CIRCUITS}, $self->{INCOMPLETE_NODES}, $self->{TOPOLOGY_LINKS}, $self->{NODES}) = $self->parseCircuitsFile($self->{CONF}->{"circuitstatus"}->{"circuits_file"});
        } catch perfSONAR_PS::Error_compat with {
            my $ex = shift;
            my $msg = "Error parsing circuits file: $ex";
            $self->{LOGGER}->error($msg);
            return -1;
        };

        if ($self->{"CONF"}->{"circuitstatus"}->{"topology_ma_type"} eq "none" and scalar keys %{ $self->{INCOMPLETE_NODES} } > 0) {
            my $msg = "You specified no topology MA, but there are incomplete nodes";
            $self->{LOGGER}->error($msg);
            return -1;
        }
    } else {
        $self->{LOGGER}->error("Invalid circuits file type specified: ".$self->{CONF}->{"circuitstatus"}->{"link_file_type"});
        return -1;
    }

    if (not defined $self->{CONF}->{"circuitstatus"}->{"topology_ma_type"} or $self->{CONF}->{"circuitstatus"}->{"topology_ma_type"} eq q{}) {
        $self->{LOGGER}->error("No topology MA type specified");
        return -1;
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"topology_ma_type"}) eq "xml") {
        load perfSONAR_PS::Client::Topology::XMLDB;

        if (not defined $self->{CONF}->{"circuitstatus"}->{"topology_ma_file"} or $self->{CONF}->{"circuitstatus"}->{"topology_ma_file"} eq q{}) {
            $self->{LOGGER}->error("You specified a Sleepycat XML DB Database, but then did not specify a database file(topology_ma_file)");
            return -1;
        }

        if (not defined $self->{CONF}->{"circuitstatus"}->{"topology_ma_environment"} or $self->{CONF}->{"circuitstatus"}->{"topology_ma_environment"} eq q{}) {
            $self->{LOGGER}->error("You specified a Sleepycat XML DB Database, but then did not specify a database name(topology_ma_environment)");
            return -1;
        }

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

        my $file = $self->{CONF}->{"circuitstatus"}->{"topology_ma_file"};
        my %ns = &perfSONAR_PS::Topology::Common::getTopologyNamespaces();

        $self->{TOPOLOGY_CLIENT} = perfSONAR_PS::Client::Topology::XMLDB->new($environment, $file, \%ns, 1);
        if (not defined $self->{TOPOLOGY_CLIENT}) {
            $self->{LOGGER}->error("Couldn't initialize topology client");
            return -1;
        }
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"topology_ma_type"}) eq "none") {
        $self->{LOGGER}->warn("Ignoring the topology MA. Everything must be specified explicitly in the circuits.conf file");
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"topology_ma_type"}) eq "ma") {
        if (not defined $self->{CONF}->{"circuitstatus"}->{"topology_ma_uri"} or $self->{CONF}->{"circuitstatus"}->{"topology_ma_uri"} eq q{}) {
            $self->{LOGGER}->error("You specified that you want a Topology MA, but did not specify the URI (topology_ma_uri)");
            return -1;
        }

        $self->{TOPOLOGY_CLIENT} = perfSONAR_PS::Client::Topology::MA->new($self->{CONF}->{"circuitstatus"}->{"topology_ma_uri"});
    } else {
        $self->{LOGGER}->error("Invalid database type specified: ".lc($self->{CONF}->{"circuitstatus"}->{"topology_ma_type"}) );
        return -1;
    }

    if (lc($self->{CONF}->{"circuitstatus"}->{"topology_ma_type"}) ne "none" and 
        defined $self->{INCOMPLETE_NODES} and keys %{ $self->{INCOMPLETE_NODES} } != 0) {
        my ($status, $res);

        ($status, $res) = $self->{TOPOLOGY_CLIENT}->open;
        if ($status != 0) {
            my $msg = "Problem opening topology MA: $res";
            $self->{LOGGER}->error($msg);
            return -1;
        }

        ($status, $res) = $self->{TOPOLOGY_CLIENT}->getAll;
        if ($status != 0) {
            my $msg = "Error getting topology information: $res";
            $self->{LOGGER}->error($msg);
            return -1;
        }

        my $topology = $res;

        ($status, $res) = $self->parseTopology($topology, $self->{INCOMPLETE_NODES}, $self->{DOMAIN});
        if ($status ne q{}) {
            my $msg = "Error parsing topology: $res";
            $self->{LOGGER}->error($msg);
            return -1;
        }
    }

    $self->{STORE} = $self->createMetadataStore($self->{NODES}, $self->{CIRCUITS});

    $self->{LOGGER}->debug("Store: ".$self->{STORE}->toString);

    if (defined $self->{CONF}->{"circuitstatus"}->{"cache_length"} and $self->{CONF}->{"circuitstatus"}->{"cache_length"} > 0) {
        if (not defined $self->{CONF}->{"circuitstatus"}->{"cache_file"} or $self->{CONF}->{"circuitstatus"}->{"cache_file"} eq q{}) {
            my $msg = "If you specify a cache time period, you need to specify a file to cache to \"cache_file\"";
            $self->{LOGGER}->error($msg);
            return -1;
        }

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

        $self->{CONF}->{"circuitstatus"}->{"cache_file"} = $file;

        $self->{LOGGER}->debug("Using \"$file\" to cache current results");
    }

    $handler->registerEventHandler("SetupDataRequest", "Path.Status", $self);
    $handler->registerEventHandler_Regex("SetupDataRequest", ".*select.*", $self);
    $handler->registerEventHandler("MetadataKeyRequest", "Path.Status", $self);

    return 0;
}

sub needLS {
    return 0;
}

sub handleEvent {
    my ($self, @args) = @_;
      my $parameters = validate(@args, {
                output => 1,
                messageId => 1,
                messageType => 1,
                messageParameters => 1,
                eventType => 1,
                subject => 1,
                filterChain => 1,
                data => 1,
                rawRequest => 1,
                doOutputMetadata => 1,
    		});

    my $output = $parameters->{"output"};
    my $messageId = $parameters->{"messageId"};
    my $messageType = $parameters->{"messageType"};
    my $message_parameters = $parameters->{"messageParameters"};
    my $eventType = $parameters->{"eventType"};
    my $d = $parameters->{"data"};
    my $raw_request = $parameters->{"rawRequest"};
    my @subjects = @{ $parameters->{"subject"} };
    my $doOutputMetadata = $parameters->{doOutputMetadata};

    my $md = $subjects[0];

    ${$doOutputMetadata} = 0;

    my ($status, $res1, $res2);

    # This could be wrapped in try/catch
    ($res1, $res2) = $self->resolveSelectChain($md, $raw_request);

    my $selectTime = $res1;
    my $subject_md = $res2;

    $eventType = undef;
    my $eventTypes = find($subject_md, "./nmwg:eventType", 0);
    foreach my $e ($eventTypes->get_nodelist) {
        my $value = extract($e, 1);
        $self->{LOGGER}->debug("Found: \"$value\"");
        if ($value eq "Path.Status") {
            $eventType = $value;
            last;
        }
    }

    if (not defined $eventType) {
        throw perfSONAR_PS::Error_compat("error.ma.event_type", "No supported event types for message of type \"$messageType\"");
    }

    if (defined $selectTime and $selectTime->getType("point") and $selectTime->getTime() eq "now") {
        $selectTime = undef;
    }

    my @circuits;
    if (find($subject_md, "./nmwg:key", 1)) {
        my $circuit_name = findvalue(find($subject_md, "./nmwg:key", 1), "./nmwg:parameters/nmwg:parameter[\@name=\"linkId\"]");

        if (not defined $circuit_name or not defined $self->{CIRCUITS}->{$circuit_name}) {
            my $msg = "The specified key is invalid";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.ma.invalid_key", "The specified key is invalid");
        }

        push @circuits, $circuit_name;
    } elsif (find($subject_md, "./nmwg:subject", 1)) {
        # this could get wrapped in try/catch
        
        my $circuit_name;
        try {
            $circuit_name  = $self->compatParseSubject(find($subject_md, "./nmwg:subject", 1));
        } catch perfSONAR_PS::Error_compat with {
            my $ex = shift;
            $self->{LOGGER}->error("Error parsing subject: ".$ex->errorMessage());
            $ex->rethrow();
        };

        push @circuits, $circuit_name;
    } else {
        @circuits = keys %{ $self->{CIRCUITS} };
    }

    $self->handlePathStatus($output, \@circuits, $selectTime);

    return (q{}, q{});
}

sub compatParseSubject {
    my ($self, $subject) = @_;
    my $circuit_name;

    if (!find($subject, "./nmtl2:link", 1)) {
        throw perfSONAR_PS::Error_compat("error.ma.invalid_subject", "The specified subject does not contain a link element");
    }

    $circuit_name = findvalue($subject, "./nmtl2:link/nmtl2:name");
    if (defined $circuit_name and defined $self->{CIRCUITS}->{$circuit_name}) {
        return $circuit_name;
    }

    my $nodes = find($subject, "./nmtl2:link/nmwgtopo3:node", 0);
    my $count = 0;
    my ($node1, $node2);
    foreach my $node ($nodes->get_nodelist) {
        my $node_name = findvalue($node, "./nmwgtopo3:name");
        if (not defined $node_name) {
            throw perfSONAR_PS::Error_compat("error.ma.invalid_subject", "The specified subject contains an unfinished node");
        }
    }

    return $circuit_name;
}

sub generateMDXpath {
    my ($subject) = @_;

    return q{};
}

sub createMetadataStore {
    my ($self, $nodes, $circuits) = @_;

    my $doc = perfSONAR_PS::XML::Document_string->new();

    $doc->startElement(prefix => "nmwg", tag => "store", namespace => "http://ggf.org/ns/nmwg/base/2.0/");
    foreach my $node_id (keys %{ $nodes }) {
        my $node = $nodes->{$node_id};

        $self->outputNodeElement($doc, $node);
    }

    foreach my $circuit_id (keys %{ $circuits }) {
        my $circuit = $circuits->{$circuit_id};

        $self->outputCircuitElement($doc, $circuit);
    }
    $doc->endElement("store");

    my $parser = XML::LibXML->new();
    my $xmlDoc;
    eval {
        $xmlDoc = $parser->parse_string($doc->getValue);
    };
    if ($@ or not defined $xmlDoc) {
        my $msg = "Couldn't parse metadata store: $@";
        $self->{LOGGER}->error($msg);
        throw perfSONAR_PS::Error_compat("error.configuration", $msg);
    }

    return $xmlDoc->documentElement;
}

sub resolveSelectChain {
    my ($self, $md, $request) = @_;

    if (!$request->getNamespaces()->{"http://ggf.org/ns/nmwg/ops/select/2.0/"}) {
        $self->{LOGGER}->debug("No select namespace means there is no select chain");
    }

    if (!find($md, "./select:subject", 1)) {
        $self->{LOGGER}->debug("No select subject means there is no select chain");
    }

    if ($request->getNamespaces()->{"http://ggf.org/ns/nmwg/ops/select/2.0/"} and find($md, "./select:subject", 1)) {
        my $other_md = find($request->getRequestDOM(), "//nmwg:metadata[\@id=\"".find($md, "./select:subject", 1)->getAttribute("metadataIdRef")."\"]", 1);
        if(!$other_md) {
            throw perfSONAR_PS::Error_compat("error.ma.chaining", "Cannot resolve supposed subject chain in metadata.");
        }

        if (!find($md, "./select:subject/select:parameters", 1)) {
            throw perfSONAR_PS::Error_compat ("error.ma.select", "No select parameters specified in given chain.");
        }

        my $time = findvalue($md, "./select:subject/select:parameters/select:parameter[\@name=\"time\"]");
        my $startTime = findvalue($md, "./select:subject/select:parameters/select:parameter[\@name=\"startTime\"]");
        my $endTime = findvalue($md, "./select:subject/select:parameters/select:parameter[\@name=\"endTime\"]");
        my $duration = findvalue($md, "./select:subject/select:parameters/select:parameter[\@name=\"duration\"]");

        if (defined $time and (defined $startTime or defined $endTime or defined $duration)) {
            throw perfSONAR_PS::Error_compat ("error.ma.select", "Ambiguous select parameters");
        }

        if (defined $time) {
            return (perfSONAR_PS::Time->new("point", $time), $other_md);
        }

        if (not defined $startTime) {
            throw perfSONAR_PS::Error_compat ("error.ma.select", "No start time specified");
        } elsif (not defined $endTime and not defined $duration) {
            throw perfSONAR_PS::Error_compat ("error.ma.select", "No end time specified");
        } elsif (defined $endTime) {
            return (perfSONAR_PS::Time->new("range", $startTime, $endTime), $other_md);
        } else {
            return (perfSONAR_PS::Time->new("duration", $startTime, $duration), $other_md);
        }
    } else {
        # No select subject means they didn't specify one which results in "now"
        $self->{LOGGER}->debug("No select chain");

        my $ret_time;
        my $time = findvalue($md, "./nmwg:parameters/nmwg:parameter[\@name=\"time\"]");
        if (defined $time and lc($time) ne "now" and $time ne q{}) {
            $ret_time = perfSONAR_PS::Time->new("point", $time);
        }

        return ($ret_time, $md);
    }
}

sub getLinkStatus {
    my ($self, $link_ids, $time) = @_;

    my %clients = ();

    if (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "ma") {
        my %client;
        my @children;

        foreach my $link_id (keys %{ $self->{TOPOLOGY_LINKS} }) {
            push @children, $link_id;
        }

        $client{"CLIENT"} = perfSONAR_PS::Client::Status::MA->new($self->{CONF}->{"circuitstatus"}->{"status_ma_uri"});
        $client{"LINKS"} = $link_ids;

        my ($status, $res) = $client{"CLIENT"}->open;
        if ($status != 0) {
            my $msg = "Problem opening status MA ".$self->{CONF}->{"circuitstatus"}->{"status_ma_uri"}.": $res";
            $self->{LOGGER}->warn($msg);
        } else {
            $clients{$self->{CONF}->{"circuitstatus"}->{"status_ma_uri"}} = \%client;
        }
    } elsif (lc($self->{CONF}->{"circuitstatus"}->{"status_ma_type"}) eq "ls") {
        # Consult the LS to find the Status MA for each link

        my %queries = ();

        foreach my $link_id (@{ $link_ids }) {
            my $xquery = q{};
            $xquery .= "        declare namespace nmwg=\"http://ggf.org/ns/nmwg/base/2.0/\";\n";
            $xquery .= "        for \$data in /nmwg:store/nmwg:data\n";
            $xquery .= "          let \$metadata_id := \$data/\@metadataIdRef\n";
            $xquery .= "          where \$data//*:link[\@id=\"$link_id\"] and \$data//nmwg:eventType[text()=\"http://ggf.org/ns/nmwg/characteristic/link/status/20070809\"]\n";
            $xquery.= "          return /nmwg:store/nmwg:metadata[\@id=\$metadata_id]\n";

            $queries{$link_id} = $xquery;
        }

        my $ls = perfSONAR_PS::Client::LS::Remote->new($self->{LS});
        my ($status, $res) = $ls->query(\%queries);
        if ($status != 0) {
            my $msg = "Couldn't lookup Link Status MAs from LS: $res";
            $self->{LOGGER}->warn($msg);
        } else {
            foreach my $link_id (@{ $link_ids }) {
                if (not defined $res->{$link_id}) {
                    $self->{LOGGER}->warn("Couldn't find any information on link $link_id");
                    next;
                }

                my ($link_status, $link_res) = @{ $res->{$link_id} };

                if ($link_status != 0) {
                    $self->{LOGGER}->warn("Couldn't find any information on link $link_id");
                    next;
                }

                my $accessPoint;

                $accessPoint = findvalue($link_res, "./psservice:datum/nmwg:metadata/perfsonar:subject/psservice:service/psservice:accessPoint");

                if (not defined $accessPoint or $accessPoint eq q{}) {
                    my $msg = "Received response with no access point for link: $link_id";
                    $self->{LOGGER}->warn($msg);
                    next;
                }

                if (not defined $clients{$accessPoint}) {
                    my %client = ();
                    my @children = ();
                    my $new_client;

                    push @children, $link_id;

                    $client{"CLIENT"} = perfSONAR_PS::Client::Status::MA->new($accessPoint);
                    $client{"LINKS"} = \@children;

                    my ($status, $res) = $client{"CLIENT"}->open;
                    if ($status != 0) {
                        my $msg = "Problem opening status MA $accessPoint: $res";
                        $self->{LOGGER}->warn($msg);
                        next;
                    }

                    $clients{$accessPoint} = \%client;
                } else {
                    push @{ $clients{$accessPoint}->{"LINKS"} }, $link_id;
                }
            }
        }
    } else {
        my %client;

        $client{"CLIENT"} = $self->{LOCAL_MA_CLIENT};
        $client{"LINKS"} = $link_ids;

        my ($status, $res) = $client{"CLIENT"}->open;
        if ($status != 0) {
            my $msg = "Problem opening status MA ".$self->{CONF}->{"circuitstatus"}->{"status_ma_uri"}.": $res";
            $self->{LOGGER}->warn($msg);
        } else {
            $clients{"local"} = \%client;
        }
    }

    my %response = ();

    foreach my $ap_id (keys %clients) {
        my $ma = $clients{$ap_id};

        my ($status, $res) = $ma->{"CLIENT"}->getLinkStatus($ma->{"LINKS"}, $time);
        if ($status != 0) {
            my $msg = "Error getting link status: $res";
            $self->{LOGGER}->warn($msg);
        } else {
            foreach my $link_id (keys %{ $res }) {
                $response{$link_id} = $res->{$link_id};
            }
        }
    }

    return \%response;
}

sub handlePathStatus {
    my ($self, $output, $circuits, $time) = @_;
    my ($status, $res);
    
    if (defined $self->{CONF}->{"circuitstatus"}->{"cache_length"} and $self->{CONF}->{"circuitstatus"}->{"cache_length"} > 0 and not defined $time) {
        my $mtime = (stat $self->{CONF}->{"circuitstatus"}->{"cache_file"})[9];

        if (time - $mtime < $self->{CONF}->{"circuitstatus"}->{"cache_length"}) {
            $self->{LOGGER}->debug("Using cached results in ".$self->{CONF}->{"circuitstatus"}->{"cache_file"});
            if (open(CACHEFILE, $self->{CONF}->{"circuitstatus"}->{"cache_file"})) {
                my $response;
                local $/;
                flock CACHEFILE, LOCK_SH;
                $response = <CACHEFILE>;
                close CACHEFILE;
                $output->addOpaque($response);
                return;
            } else {
                $self->{LOGGER}->warn("Unable to open cached results in ".$self->{CONF}->{"circuitstatus"}->{"cache_file"});
            }
        }
    }

    # get the list of topology link IDs to lookup    
    my %link_ids = ();
    foreach my $circuit_name (@{ $circuits }) {
        my $circuit = $self->{CIRCUITS}->{$circuit_name};

        foreach my $sublink_id (@{ $circuit->{"sublinks"} }) {
            $link_ids{$sublink_id} = q{};
        }
    }

    # Lookup the link status
    my @links = keys %link_ids;

    $res = $self->getLinkStatus(\@links, $time);

    # Fill in any missing links
    foreach my $link_id (@links) {
        if (not defined $res->{$link_id}) {
            my $msg = "Did not receive any information about link $link_id";
            $self->{LOGGER}->warn($msg);

            my $link;
            if (not defined $time) {
            my $curr_time = time;
            $link = perfSONAR_PS::Status::Link->new($link_id, "full", $curr_time, $curr_time, "unknown", "unknown");
            } else {
            $link = perfSONAR_PS::Status::Link->new($link_id, "full", $time->getStartTime(), $time->getEndTime(), "unknown", "unknown");
            }

            $res->{$link_id} = [ $link ];
        }
    }

    my %circuit_status = ();

    foreach my $circuit_name (@{ $circuits }) {
        my $circuit = $self->{CIRCUITS}->{$circuit_name};

        my @data_points = ();

        if (defined $time and $time->getType() ne "point") {
            foreach my $sublink_id (@{ $circuit->{"sublinks"} }) {
                foreach my $link_status (@{ $res->{$sublink_id} }) {
                    push @data_points, $link_status;
                }
            }
        } else {
            my $circuit_admin_value = "unknown";
            my $circuit_oper_value = "unknown";
            my $circuit_time;

            foreach my $sublink_id (@{ $circuit->{"sublinks"} }) {
                foreach my $link_status (@{ $res->{$sublink_id} }) {
                    $self->{LOGGER}->debug("Sublink: $sublink_id");
                    my $oper_value = $link_status->getOperStatus;
                    my $admin_value = $link_status->getAdminStatus;
                    my $end_time = $link_status->getEndTime;

                    $circuit_time = $end_time if (not defined $circuit_time or $end_time > $circuit_time);

                    if ($circuit_oper_value eq "down" or $oper_value eq "down")  {
                        $circuit_oper_value = "down";
                    } elsif ($circuit_oper_value eq "degraded" or $oper_value eq "degraded")  {
                        $circuit_oper_value = "degraded";
                    } elsif ($circuit_oper_value eq "up" or $oper_value eq "up")  {
                        $circuit_oper_value = "up";
                    } else {
                        $circuit_oper_value = "unknown";
                    }

                    if ($circuit_admin_value eq "maintenance" or $admin_value eq "maintenance") {
                        $circuit_admin_value = "maintenance";
                    } elsif ($circuit_admin_value eq "troubleshooting" or $admin_value eq "troubleshooting") {
                        $circuit_admin_value = "troubleshooting";
                    } elsif ($circuit_admin_value eq "underrepair" or $admin_value eq "underrepair") {
                        $circuit_admin_value = "underrepair";
                    } elsif ($circuit_admin_value eq "normaloperation" or $admin_value eq "normaloperation") {
                        $circuit_admin_value = "normaloperation";
                    } else {
                        $circuit_admin_value = "unknown";
                    }
                }
            }

            if (not defined $time and defined $self->{CONF}->{"circuitstatus"}->{"max_recent_age"} and $self->{CONF}->{"circuitstatus"}->{"max_recent_age"} ne q{}) {
                my $curr_time = time;

                if ($curr_time - $circuit_time > $self->{CONF}->{"circuitstatus"}->{"max_recent_age"}) {
                    $self->{LOGGER}->debug("Old link time: $circuit_time Current Time: ".$curr_time.": ".($curr_time - $circuit_time));
                    $circuit_time = $curr_time;
                    $circuit_oper_value = "unknown";
                    $circuit_admin_value = "unknown";
                }
            } else {
                $circuit_time = $time->getTime();
            }

            my $link = perfSONAR_PS::Status::Link->new(q{}, q{}, $circuit_time, $circuit_time, $circuit_oper_value, $circuit_admin_value);
            push @data_points, $link;
        }

        $circuit_status{$circuit_name} = \@data_points;
    }

    my $doc = perfSONAR_PS::XML::Document_string->new();

    startParameters($doc, "params.0");
     addParameter($doc, "DomainName", $self->{DOMAIN});
    endParameters($doc);
    $self->outputResults($doc, \%circuit_status, $time);

    if (not defined $time and defined $self->{CONF}->{"circuitstatus"}->{"cache_length"} and $self->{CONF}->{"circuitstatus"}->{"cache_length"} > 0) {
        $self->{LOGGER}->debug("Caching results in ".$self->{CONF}->{"circuitstatus"}->{"cache_file"});

        unlink($self->{CONF}->{"circuitstatus"}->{"cache_file"});

        if (sysopen(CACHEFILE, $self->{CONF}->{"circuitstatus"}->{"cache_file"}, O_WRONLY | O_CREAT, 0600)) {
            flock CACHEFILE, LOCK_EX;
            print CACHEFILE $doc->getValue();
            close CACHEFILE;
        } else {
            $self->{LOGGER}->warn("Unable to cache results");
        }
    }

    $output->addOpaque($doc->getValue());

    return;
}

sub outputResults {
    my ($self, $output, $results, $time) = @_;

    my %output_endpoints = ();
    my $i = 0;

    foreach my $circuit_name (keys %{ $results }) {
        my $circuit = $self->{CIRCUITS}->{$circuit_name};
        foreach my $endpoint (@{ $circuit->{"endpoints"} }) {
            next if (not defined $self->{NODES}->{$endpoint->{name}});
            next if (defined $output_endpoints{$endpoint->{name}});

            startMetadata($output, "metadata.".genuid(), q{}, undef);
             $output->startElement(prefix => "nmwg", tag => "subject", namespace => "http://ggf.org/ns/nmwg/base/2.0/", attributes => { id => "sub-".$endpoint->{name} });
              $self->outputNodeElement($output, $self->{NODES}->{$endpoint->{name}});
             $output->endElement("subject");
            endMetadata($output);

            $output_endpoints{$endpoint->{name}} = 1;
        }

        my $mdid = "metadata.".genuid();

        startMetadata($output, $mdid, q{}, undef);
         $output->startElement(prefix => "nmwg", tag => "subject", namespace => "http://ggf.org/ns/nmwg/base/2.0/", attributes => { id => "sub$i" });
          $self->outputCircuitElement($output, $circuit);
         $output->endElement("subject");
        endMetadata($output);

        my @data = @{ $results->{$circuit_name} };
        startData($output, "data.$i", $mdid, undef);
        foreach my $datum (@data) {
            my %attrs = ();

            $attrs{"timeType"} = "unix";
            $attrs{"timeValue"} = $datum->getEndTime();
            if (defined $time and $time ne "point") {
                $attrs{"startTime"} = $datum->getStartTime();
                $attrs{"endTime"} = $datum->getEndTime();
            }

            $output->startElement(prefix => "ifevt", tag => "datum", namespace => "http://ggf.org/ns/nmwg/event/status/base/2.0/", attributes => \%attrs);
            $output->createElement(prefix => "ifevt", tag => "stateAdmin", namespace => "http://ggf.org/ns/nmwg/event/status/base/2.0/", content => $datum->getAdminStatus);
            $output->createElement(prefix => "ifevt", tag => "stateOper", namespace => "http://ggf.org/ns/nmwg/event/status/base/2.0/", content => $datum->getOperStatus);
            $output->endElement("datum");
        }
        endData($output);

        $i++;
    }

    return;
}

sub outputNodeElement {
    my ($self, $output, $node) = @_;

    $output->startElement(prefix => "nmwgtopo3", tag => "node", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", attributes => { id => $self->{DOMAIN}."-".$node->{"name"} });
      $output->createElement(prefix => "nmwgtopo3", tag => "type", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", attributes => { type => "logical" }, content => "TopologyPoint");
      $output->createElement(prefix => "nmwgtopo3", tag => "name", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", attributes => { type => "logical" }, content => $node->{"name"});
    if (defined $node->{"city"} and $node->{"city"} ne q{}) {
        $output->createElement(prefix => "nmwgtopo3", tag => "city", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", content => $node->{"city"});
    }
    if (defined $node->{"country"} and $node->{"country"} ne q{}) {
        $output->createElement(prefix => "nmwgtopo3", tag => "country", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", content => $node->{"country"});
    }
    if (defined $node->{"latitude"} and $node->{"latitude"} ne q{}) {
        $output->createElement(prefix => "nmwgtopo3", tag => "latitude", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", content => $node->{"latitude"});
    }
    if (defined $node->{"longitude"} and $node->{"longitude"} ne q{}) {
        $output->createElement(prefix => "nmwgtopo3", tag => "longitude", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", content => $node->{"longitude"});
    }
    if (defined $node->{"institution"} and $node->{"institution"} ne q{}) {
        $output->createElement(prefix => "nmwgtopo3", tag => "institution", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", , content => $node->{"institution"});
    }
    $output->endElement("node");

    return;
}

sub outputCircuitElement {
    my ($self, $output, $circuit) = @_;

    $output->startElement(prefix => "nmtl2", tag => "link", namespace => "http://ggf.org/ns/nmwg/topology/l2/3.0/");
      $output->createElement(prefix => "nmtl2", tag => "name", namespace => "http://ggf.org/ns/nmwg/topology/l2/3.0/", attributes => { type => "logical" }, content => $circuit->{"name"});
      $output->createElement(prefix => "nmtl2", tag => "globalName", namespace => "http://ggf.org/ns/nmwg/topology/l2/3.0/", attributes => { type => "logical" }, content => $circuit->{"globalName"});
      $output->createElement(prefix => "nmtl2", tag => "type", namespace => "http://ggf.org/ns/nmwg/topology/l2/3.0/", content => $circuit->{"type"});
      foreach my $endpoint (@{ $circuit->{"endpoints"} }) {
      $output->startElement(prefix => "nmwgtopo3", tag => "node", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", attributes => { nodeIdRef => $self->{DOMAIN}."-".$endpoint->{"name"} });
      $output->createElement(prefix => "nmwgtopo3", tag => "role", namespace => "http://ggf.org/ns/nmwg/topology/base/3.0/", content => $endpoint->{"type"});
      $output->endElement("node");
      }
#      startParameters($output, "params.0");
#        addParameter($output, "supportedEventType", "Path.Status");
#      endParameters($output);
    $output->endElement("link");

    return;
}

sub parseCircuitsFile {
    my ($self, $file) = @_;

    my %nodes = ();
    my %incomplete_nodes = ();
    my %topology_links = ();
    my %circuits = ();

    my $parser = XML::LibXML->new();
    my $doc;
    eval {
        $doc = $parser->parse_file($file);
    };
    if ($@ or not defined $doc) {
        my $msg = "Couldn't parse circuits file $file: $@";
        $self->{LOGGER}->error($msg);
        throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
    }

    my $conf = $doc->documentElement;

    my $domain = findvalue($conf, "domain");
    if (not defined $domain) {
        my $msg = "No domain specified in configuration";
        $self->{LOGGER}->error($msg);
        throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
    }

    my $find_res;

    $find_res = find($conf, "./*[local-name()='node']", 0);
    if ($find_res) {
    foreach my $endpoint ($find_res->get_nodelist) {
        my $node_id = $endpoint->getAttribute("id");
        my $node_type = $endpoint->getAttribute("type");
        my $node_name = $endpoint->getAttribute("name");
        my $city = findvalue($endpoint, "city");
        my $country = findvalue($endpoint, "country");
        my $longitude = findvalue($endpoint, "longitude");
        my $institution = findvalue($endpoint, "institution");
        my $latitude = findvalue($endpoint, "latitude");

        if (not defined $node_name or $node_name eq q{}) {
            my $msg = "Node needs to have a name";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
        }

        $node_name =~ s/[^a-zA-Z0-9_]//g;
        $node_name = uc($node_name);

        if (defined $nodes{$node_name}) {
            my $msg = "Multiple endpoints have the name \"$node_name\"";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
        }

        my %tmp = ();
        my $new_node = \%tmp;

        $new_node->{"id"} = $node_id if (defined $node_id and $node_id ne q{});
        $new_node->{"name"} = $node_name if (defined $node_name and $node_name ne q{});
        $new_node->{"city"} = $city if (defined $city and $city ne q{});
        $new_node->{"country"} = $country if (defined $country and $country ne q{});
        $new_node->{"longitude"} = $longitude if (defined $longitude and $longitude ne q{});
        $new_node->{"latitude"} = $latitude if (defined $latitude and $latitude ne q{});
        $new_node->{"institution"} = $institution if (defined $institution and $institution ne q{});

        if (defined $node_id and
            (not defined $city or not defined $country or not defined $longitude or not defined $latitude or not defined $institution)) {
            $incomplete_nodes{$node_id} = $new_node;
        }

        $nodes{$node_name} = $new_node;
    }
    }

    $find_res = find($conf, "./*[local-name()='circuit']", 0);
    if ($find_res) {
    foreach my $circuit ($find_res->get_nodelist) {
        my $global_name = findvalue($circuit, "globalName");
        my $local_name = findvalue($circuit, "localName");
        my $knowledge = $circuit->getAttribute("knowledge");
        my $circuit_type;

        if (not defined $global_name or $global_name eq q{}) {
            my $msg = "Circuit has no global name";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
        }

        if (not defined $knowledge or $knowledge eq q{}) {
            $self->{LOGGER}->warn("Don't know the knowledge level of circuit \"$global_name\". Assuming full");
            $knowledge = "full";
        } else {
            $knowledge = lc($knowledge);
        }

        if (not defined $local_name or $local_name eq q{}) {
            $local_name = $global_name;
        }

        my %sublinks = ();

        $find_res = find($circuit, "./*[local-name()='linkID']", 0);
        if ($find_res) {
        foreach my $topo_id ($find_res->get_nodelist) {
            my $id = $topo_id->textContent;

            if (defined $sublinks{$id}) {
                my $msg = "Link $id appears multiple times in circuit $global_name";
                $self->{LOGGER}->error($msg);
                throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
            }

            $sublinks{$id} = q{};
            $topology_links{$id} = q{};
        }
        }

        my @endpoints = ();

        my $num_endpoints = 0;

        my $prev_domain;

        $find_res = find($circuit, "./*[local-name()='endpoint']", 0);
        if ($find_res) {
        foreach my $endpoint ($find_res->get_nodelist) {
            my $node_type = $endpoint->getAttribute("type");
            my $node_name = $endpoint->getAttribute("name");

            if (not defined $node_type or $node_type eq q{}) {
                my $msg = "Node with unspecified type found";
                $self->{LOGGER}->error($msg);
                throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
            }

            if (not defined $node_name or $node_name eq q{}) {
                my $msg = "Endpint needs to specify a node name";
                $self->{LOGGER}->error($msg);
                throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
            }

            $node_name =~ s/[^a-zA-Z0-9_]//g;
            $node_name = uc($node_name);

            if (lc($node_type) ne "demarcpoint" and lc($node_type) ne "endpoint") {
                my $msg = "Node found with invalid type $node_type. Must be \"DemarcPoint\" or \"EndPoint\"";
                $self->{LOGGER}->error($msg);
                throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
            }

            my ($domain, @junk) = split(/-/, $node_name);
            if (not defined $prev_domain) {
                $prev_domain = $domain;
            } elsif ($domain eq $prev_domain) {
                    $circuit_type = "DOMAIN_Link";
            } else {
                if ($knowledge eq "full") {
                    $circuit_type = "ID_Link";
                } else {
                    $circuit_type = "ID_LinkPartialInfo";
                }
            }

            my %new_endpoint = ();

            $new_endpoint{"type"} = $node_type;
            $new_endpoint{"name"} = $node_name;

            push @endpoints, \%new_endpoint;

            $num_endpoints++;
        }
        }

        if ($num_endpoints != 2) {
            my $msg = "Invalid number of endpoints, $num_endpoints, must be 2";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
        }

        my @sublinks = keys %sublinks;

        my %new_circuit = ();

        $new_circuit{"globalName"} = $global_name;
        $new_circuit{"name"} = $local_name;
        $new_circuit{"sublinks"} = \@sublinks;
        $new_circuit{"endpoints"} = \@endpoints;
        $new_circuit{"type"} = $circuit_type;

        if (defined $circuits{$local_name}) {
            my $msg = "Error: existing circuit of name $local_name";
            $self->{LOGGER}->error($msg);
            throw perfSONAR_PS::Error_compat ("error.configuration", $msg);
        } else {
            $circuits{$local_name} = \%new_circuit;
        }
    }
    }

    return ($domain, \%circuits, \%incomplete_nodes, \%topology_links, \%nodes);
}

sub parseTopology {
    my ($self, $topology, $incomplete_nodes, $domain_name) = @_;
    my %ids = ();

    foreach my $node ($topology->getElementsByLocalName("node")) {
        my $id = $node->getAttribute("id");
        $self->{LOGGER}->debug("node: ".$id);

        next if not defined $incomplete_nodes->{$id};

        $self->{LOGGER}->debug("found node ".$id." in here");

        my $longitude = findvalue($node, "./*[local-name()='longitude']");
        $self->{LOGGER}->debug("searched for longitude");
        my $institution = findvalue($node, "./*[local-name()='institution']");
        $self->{LOGGER}->debug("searched for institution");
        my $latitude = findvalue($node, "./*[local-name()='latitude']");
        $self->{LOGGER}->debug("searched for latitude");
        my $city = findvalue($node, "./*[local-name()='city']");
        $self->{LOGGER}->debug("searched for city");
        my $country = findvalue($node, "./*[local-name()='country']");
        $self->{LOGGER}->debug("searched for country");

        $incomplete_nodes->{$id}->{"type"} = "TopologyPoint";

        if (not defined $incomplete_nodes->{$id}->{"longitude"} and defined $longitude and $longitude ne q{}) {
            # conversions may need to be made
            $incomplete_nodes->{$id}->{"longitude"} = $longitude;
        }

        if (not defined $incomplete_nodes->{$id}->{"latitude"} and defined $latitude and $latitude ne q{}) {
            # conversions may need to be made
            $incomplete_nodes->{$id}->{"latitude"} = $latitude;
        }

        if (not defined $incomplete_nodes->{$id}->{"institution"}) {
            if ( defined $institution and $institution ne q{}) {
                # conversions may need to be made
                $incomplete_nodes->{$id}->{"institution"} = $institution;
            } else {
                $incomplete_nodes->{$id}->{"institution"} = $domain_name;
            }
        }

        if (not defined $incomplete_nodes->{$id}->{"city"} and defined $city and $city ne q{}) {
            $incomplete_nodes->{$id}->{"city"} = $city;
        }

        if (not defined $incomplete_nodes->{$id}->{"country"} and defined $country and $country ne q{}) {
            $incomplete_nodes->{$id}->{"country"} = $country;
        }
    }

    return ("", undef);
}

1;

__END__
=head1 NAME

perfSONAR_PS::Services::MA::CircuitStatus - A module that provides methods for an E2EMon Compatible MP.

=head1 DESCRIPTION

This module aims to offer simple methods for dealing with requests for information, and the
related tasks of interacting with backend storage.

=head1 SYNOPSIS

use perfSONAR_PS::Services::MA::CircuitStatus;

my %conf = readConfiguration();

my %ns = (
        nmwg => "http://ggf.org/ns/nmwg/base/2.0/",
        ifevt => "http://ggf.org/ns/nmwg/event/status/base/2.0/",
        nmtm => "http://ggf.org/ns/nmwg/time/2.0/",
        nmwgtopo3 => "http://ggf.org/ns/nmwg/topology/base/3.0/",
        nmtl2 => "http://ggf.org/ns/nmwg/topology/l2/3.0/",
        nmtl3 => "http://ggf.org/ns/nmwg/topology/l3/3.0/",
     );

my $ma = perfSONAR_PS::Services::MA::CircuitStatus->new(\%conf, \%ns);

# or
# $ma = perfSONAR_PS::Services::MA::CircuitStatus->new;
# $ma->setConf(\%conf);
# $ma->setNamespaces(\%ns);

if ($ma->init != 0) {
    print "Error: couldn't initialize measurement archive\n";
    exit(-1);
}

while(1) {
    my $request = $ma->receive;
    $ma->handleRequest($request);
}

=head1 API

The offered API is simple, but offers the key functions we need in a measurement archive.

=head2 init 

       Initializes the MP and validates or fills in entries in the
    configuration file. Returns 0 on success and -1 on failure.

=head2 receive($self)

    Grabs an incoming message from transport object to begin processing. It
    completes the processing if the message was handled by a lower layer.
    If not, it returns the Request structure.

=head2 handleRequest($self, $request)

    Handles the specified request returned from receive()

=head2 __handleRequest($self)

    Validates that the message is one that we can handle, calls the
    appropriate function for the message type and builds the response
    message. 

=head2 parseRequest($self, $request)

    Goes through each metadata/data pair, extracting the eventType and
    calling the function associated with that eventType.

=head2 handlePathStatusRequest($self, $time) 

    Performs the required steps to handle a path status message: contacts
    the topology service to resolve node information, contacts the LS if
    needed to find the link status service, contacts the link status
    service and munges the results.

=head2 outputNodes($nodes) 

    Takes the set of nodes and outputs them in an E2EMon compatiable
    format.

=head2 outputCircuits($circuits) 

    Takes the set of links and outputs them in an E2EMon compatiable
    format.

=head2 parseCircuitsFile($file) 

    Parses the links configuration file. It returns an array containg up to
    five values. The first value is the status and can be one of 0 or -1.
    If it is -1, parsing the configuration file failed and the error
    message is in the next value. If the status is 0, the next 4 values are
    the domain name, a pointer to the set of links, a pointer to a hash
    containg the set of nodes to lookup in the topology service and a
    pointer to a hash containing the set of links to lookup in the status
    service.
    
=head2 parseTopology($topology, $nodes, $domain_name)

    Parses the output from the topology service and fills in the details
    for the nodes. The domain name is passed so that when a node has no
    name specified in the configuration file, it can be constructd based on
    the domain name and the node's name in the topology service.

=head1 SEE ALSO

L<perfSONAR_PS::Services::Base>, L<perfSONAR_PS::Services::MA::General>, L<perfSONAR_PS::Common>,
L<perfSONAR_PS::Messages>, L<perfSONAR_PS::Transport>,
L<perfSONAR_PS::Client::Status::MA>, L<perfSONAR_PS::Client::Topology::MA>


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, aaron@internet2.edu

=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