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.08; 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 = ; 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, L, L, L, L, L, L 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 =head1 COPYRIGHT Copyright (c) 2004-2007, Internet2 and the University of Delaware All rights reserved. =cut # vim: expandtab shiftwidth=4 tabstop=4