From 69fa2017cc6030740b45d6174ebfc04d894595a1 Mon Sep 17 00:00:00 2001 From: Leigh B Stoller <stoller@flux.utah.edu> Date: Wed, 6 May 2020 11:21:27 -0600 Subject: [PATCH] Lots of work on rfmonitor stuff: * Save data for 24 hours in the rfmonitor directory. The files are now gzipped, we use pako JS library to gunzip them at the client browser. * Save data that corresponds to violations, in the archive directory for 7 days. * Change the violation email to reference the frequency graph for the current data set. * Lots of changes to the frequency graph code to handle specific log files (by unix timestamp) and build a menu of all of the available data files so the use can flip through the data sets. --- GNUmakefile.in | 4 +- apt/GNUmakefile.in | 4 +- apt/rfmonitor_daemon.in | 96 +++++-- configure | 3 + configure.ac | 3 + defs-default | 1 + powder/GNUmakefile.in | 96 +++++++ powder/bspowerconfig.in | 142 ++++++++++ powder/htaccess | 2 + powder/listing.php.in | 64 +++++ powder/powder_deadman.in | 360 ++++++++++++++++++++++++ powder/powder_keepalive.in | 180 ++++++++++++ {apt => powder}/powder_shutdown.in | 4 +- {apt => powder}/powderstats.in | 0 www/aptui/frequency-graph.ajax | 53 +++- www/aptui/frequency-graph.php | 18 ++ www/aptui/js/freqgraphs.js | 176 ++++++++++-- www/aptui/js/frequency-graph.js | 3 + www/aptui/js/status.js | 1 + www/aptui/server-ajax.php | 2 + www/aptui/status.php | 1 + www/aptui/template/frequency-graph.html | 27 +- 22 files changed, 1191 insertions(+), 49 deletions(-) create mode 100644 powder/GNUmakefile.in create mode 100644 powder/bspowerconfig.in create mode 100644 powder/htaccess create mode 100644 powder/listing.php.in create mode 100644 powder/powder_deadman.in create mode 100644 powder/powder_keepalive.in rename {apt => powder}/powder_shutdown.in (98%) rename {apt => powder}/powderstats.in (100%) diff --git a/GNUmakefile.in b/GNUmakefile.in index 32e7960c63..e6b540448e 100644 --- a/GNUmakefile.in +++ b/GNUmakefile.in @@ -1,5 +1,5 @@ # -# Copyright (c) 2000-2017 University of Utah and the Flux Group. +# Copyright (c) 2000-2017, 2020 University of Utah and the Flux Group. # # {{{EMULAB-LICENSE # @@ -59,7 +59,7 @@ ifeq ($(ISMAINSITE),1) SUBDIRS += tools/rmanage tools/whol endif ifeq ($(PGENISUPPORT),1) -SUBDIRS += protogeni apt +SUBDIRS += protogeni apt powder endif else SUBDIRS = db tbsetup account protogeni diff --git a/apt/GNUmakefile.in b/apt/GNUmakefile.in index 5329e3bf00..4a730ffa55 100644 --- a/apt/GNUmakefile.in +++ b/apt/GNUmakefile.in @@ -37,10 +37,10 @@ BIN_SCRIPTS = manage_profile manage_instance manage_dataset \ create_slivers searchip start-experiment manage_resgroup SBIN_SCRIPTS = apt_daemon aptevent_daemon portal_xmlrpc apt_checkup \ portal_monitor apt_scheduler portal_resources \ - manage_licenses manage_aggregate powder_shutdown \ + manage_licenses manage_aggregate \ rfmonitor_daemon aptimage_daemon aptexpire_daemon \ recalcmaxext aptresgroup_daemon aptbus_monitor \ - aptroute_monitor manage_rfranges powderstats + aptroute_monitor manage_rfranges LIB_SCRIPTS = APT_Profile.pm APT_Instance.pm APT_Dataset.pm APT_Geni.pm \ APT_Aggregate.pm APT_Utility.pm APT_Rspec.pm \ APT_Reservation.pm APT_RFRange.pm diff --git a/apt/rfmonitor_daemon.in b/apt/rfmonitor_daemon.in index 9d2b9c8484..0f767e9f2f 100644 --- a/apt/rfmonitor_daemon.in +++ b/apt/rfmonitor_daemon.in @@ -68,10 +68,13 @@ my $OURDOMAIN = "@OURDOMAIN@"; my $TBBASE = "@TBBASE@"; my $MAINSITE = @TBMAINSITE@; my $POWDER_RFMONITOR = @POWDER_RFMONITOR@; +my $POWDER_NICKNAME = "@POWDER_NICKNAME@"; my $PGENISUPPORT = @PROTOGENI_SUPPORT@; my $LOGFILE = "$TB/log/rfmonitor_daemon.log"; my $RAWDATADIR = "$TB/www/rfmonitor"; my $POWER = "$TB/bin/power"; +my $GZIP = "/usr/bin/gzip"; +my $FIND = "/usr/bin/find"; my $FORMAT = "portid,timestamp,frequency,power"; my $MAILDELAY = 3600; @@ -87,8 +90,8 @@ sub ShouldNotify($$); sub NodeNotified($$$); sub HandleChild($); sub LoadNodeData($); -sub HandleViolations($$); -sub WriteRawData($); +sub HandleViolations($$$); +sub WriteRawData($$$); # # Explanatory text. @@ -295,6 +298,7 @@ sub HandleChild($) my $noisefloor = $NOISEFLOOR; my @dbinserts = (); my %csvdata = (); + my $filestamp = time(); # Add a DB insert to the list of violations to store in the DB. my $addInsert = sub { @@ -556,8 +560,16 @@ sub HandleChild($) $nextdata->{$key} = $measurement; } } + # + # Write the raw data file(s) + # + WriteRawData(\%csvdata, $filestamp, scalar(keys(%violaters))); + + # + # We send links to the data and the graph page in the email. + # if (keys(%violaters)) { - HandleViolations(\%violaters, \%rflimits); + HandleViolations(\%violaters, \%rflimits, $filestamp); } if (@dbinserts) { my $query = "insert into node_rf_violations ". @@ -587,11 +599,6 @@ sub HandleChild($) else { print STDERR "Could not open $datafile for writing: $!\n"; } - # - # Write the raw data file(s) - # - WriteRawData(\%csvdata); - print "Finished with $address:$port at " . POSIX::strftime("%m/%d %H:%M:%S", localtime()) . "\n"; exit(0); @@ -632,15 +639,15 @@ sub LoadNodeData($) # List of nodes, and interfaces that are in violation. We want to generate # an informative email message and turn off the nodes. # -sub HandleViolations($$) +sub HandleViolations($$$) { - my ($violations, $rflimits) = @_; + my ($violations, $rflimits, $filestamp) = @_; # # Simple, send a message per node. # foreach my $nodeid (keys(%{$violations})) { - my ($TO, $subject, $body, $portalurl, $rfurl); + my ($TO, $subject, $body, $portalurl, $rfurl, $graphurl); my $forcemail = 0; my $headers; @@ -725,6 +732,13 @@ sub HandleViolations($$) if ($measurement->{'repeatcount'} == 1); } $body .= "\n"; + # + # This will need to change when there is more then one TX + # iface per node. + # + $graphurl = "https://www.powderwireless.net" . + "/frequency-graph.php?node_id=$nodeid&iface=$iface" . + "&cluster=${POWDER_NICKNAME}&logid=$filestamp&archived=1"; } if (!$mailonly) { $body .= "\n" . "This node will be immediately powered off!\n"; @@ -738,6 +752,7 @@ sub HandleViolations($$) else { $rfurl = "$TBBASE/portal/show-rfviolations.php?node_id=$nodeid"; } + $body .= "\n" . "Monitor Graph:\n" . $graphurl . "\n"; $body .= "\n" . "Violation History:\n" . $rfurl . "\n"; if ($portalurl) { @@ -827,16 +842,38 @@ sub NodeNotify($$$) # # Write RAW data to CSV files in /usr/testbed/www # -sub WriteRawData($) +sub WriteRawData($$$) { - my ($data) = @_; + my ($data, $filestamp, $violations) = @_; foreach my $portid (keys(%{$data})) { - my @samples = @{$data->{$portid}}; - my $newname = "$RAWDATADIR/${portid}.new"; - my $csvname = "$RAWDATADIR/${portid}.csv"; + my @samples = @{$data->{$portid}}; + my $filename = "${portid}-${filestamp}.csv"; + my $tmpname = "$RAWDATADIR/${portid}.tmp"; + my $csvname = "$RAWDATADIR/${portid}.csv"; + my $gzsymlink = "$RAWDATADIR/${portid}.csv.gz"; + my ($gzipname); + # + # When there are violations, we archive the raw data for longer + # in the archive directory, and the symlink points there instead. + # + if ($violations) { + $gzipname = "$RAWDATADIR/archive/${filename}.gz"; + } + else { + $gzipname = "$RAWDATADIR/${filename}.gz"; + } - if (open(CSV, "> $newname")) { + # + # Prune historical data older then 24 hours. + # Prune archived data older then 1 week. + # + system("$FIND -E $RAWDATADIR -regex '^.*\-[0-9]+\.csv\.gz\$' ". + "-mtime +24h -depth 1 -print " . ($impotent ? "" : "-delete")); + system("$FIND $RAWDATADIR/archive -mtime +7 -print ". + ($impotent ? "" : "-delete")); + + if (open(CSV, "> $tmpname")) { print CSV "frequency,power\n"; foreach my $ref (@samples) { @@ -847,16 +884,33 @@ sub WriteRawData($) print CSV "$freq,$power\n"; } if (close(CSV)) { - if (!rename($newname, $csvname)) { - print STDERR "Could not rename new CSV file $newname: $!\n"; + system("$GZIP -c $tmpname > $gzipname"); + if ($?) { + print STDERR "Could not gzip $tmpname\n"; + return -1; + } + if (!rename($tmpname, $csvname)) { + print STDERR "Could not rename new CSV file: $!\n"; + return -1; + } + # A violation, symlink into the + if ($violations && + system("/bin/ln -sf archive/${filename}.gz ". + " $RAWDATADIR/${filename}.gz")) { + print STDERR "Could not create archive symlink\n"; + return -1; + } + if (system("/bin/ln -sf ${filename}.gz $gzsymlink")) { + print STDERR "Could not update $gzsymlink\n"; + return -1; } } else { - print STDERR "Could not close new CSV file $newname: $!\n"; + print STDERR "Could not close new CSV file $tmpname: $!\n"; } } else { - print STDERR "Could not open new CSV file $newname: $!\n"; + print STDERR "Could not open new CSV file $tmpname: $!\n"; } } } diff --git a/configure b/configure index 8334fe9173..9f651c4ec5 100755 --- a/configure +++ b/configure @@ -678,6 +678,7 @@ TBOPSEMAIL_NOSLASH TBOPSEMAIL POWDER_RFMONITOR_HOST POWDER_RFMONITOR +POWDER_NICKNAME UI_EXTERNAL_ACCOUNTS UI_DISABLE_RESERVATIONS UI_DISABLE_DATASETS @@ -5367,6 +5368,7 @@ UI_DISABLE_RESERVATIONS=0 UI_EXTERNAL_ACCOUNTS=0 POWDER_RFMONITOR=0 POWDER_RFMONITOR_HOST="0.0.0.0" +POWDER_NICKNAME="" DISABLE_RESERVATION_EMAIL=0 MAILERNODE="ops" @@ -7299,6 +7301,7 @@ outfiles="$outfiles Makeconf GNUmakefile \ protogeni/rspec-emulab/0.2/GNUmakefile \ protogeni/rspec-emulab/2/GNUmakefile \ apt/GNUmakefile \ + powder/GNUmakefile \ collab/GNUmakefile \ collab/exp-vis/GNUmakefile collab/exp-vis/fetch-vis \ node_usage/GNUmakefile node_usage/mk-plots \ diff --git a/configure.ac b/configure.ac index e23b1d3c9a..58484280f5 100644 --- a/configure.ac +++ b/configure.ac @@ -375,6 +375,7 @@ AC_SUBST(UI_DISABLE_RESERVATIONS) AC_SUBST(UI_EXTERNAL_ACCOUNTS) AC_SUBST(POWDER_RFMONITOR) AC_SUBST(POWDER_RFMONITOR_HOST) +AC_SUBST(POWDER_NICKNAME) AC_SUBST(DISABLE_RESERVATION_EMAIL) AC_SUBST(MAILERNODE) @@ -571,6 +572,7 @@ UI_DISABLE_RESERVATIONS=0 UI_EXTERNAL_ACCOUNTS=0 POWDER_RFMONITOR=0 POWDER_RFMONITOR_HOST="0.0.0.0" +POWDER_NICKNAME="" DISABLE_RESERVATION_EMAIL=0 MAILERNODE="ops" @@ -1534,6 +1536,7 @@ outfiles="$outfiles Makeconf GNUmakefile \ protogeni/rspec-emulab/0.2/GNUmakefile \ protogeni/rspec-emulab/2/GNUmakefile \ apt/GNUmakefile \ + powder/GNUmakefile \ collab/GNUmakefile \ collab/exp-vis/GNUmakefile collab/exp-vis/fetch-vis \ node_usage/GNUmakefile node_usage/mk-plots \ diff --git a/defs-default b/defs-default index deb5cd2577..ca61130df3 100644 --- a/defs-default +++ b/defs-default @@ -225,6 +225,7 @@ BOOTINFO_EVENTS=0 # Powder RF monitor. POWDER_RFMONITOR=1 POWDER_RFMONITOR_HOST="$BOSSNODE_IP" +POWDER_NICKNAME="Emulab" # No longer allow geni users direct access. Must go through the Portal # or the local web interface. diff --git a/powder/GNUmakefile.in b/powder/GNUmakefile.in new file mode 100644 index 0000000000..eda120cc2d --- /dev/null +++ b/powder/GNUmakefile.in @@ -0,0 +1,96 @@ +# +# Copyright (c) 2000-2020 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# + +SRCDIR = @srcdir@ +TESTBED_SRCDIR = @top_srcdir@ +OBJDIR = .. +SUBDIR = powder + +include $(OBJDIR)/Makeconf + +SUBDIRS = + +SBIN_SCRIPTS = powder_shutdown powderstats \ + powder_keepalive powder_deadman bspowerconfig +WEB_SBIN_SCRIPTS= webpowder_shutdown +LIBEXEC_SCRIPTS = $(WEB_SBIN_SCRIPTS) + +# +# Force dependencies on the scripts so that they will be rerun through +# configure if the .in file is changed. +# +all: $(SBIN_SCRIPTS) $(LIBEXEC_SCRIPTS) listing.php + +subboss: + +include $(TESTBED_SRCDIR)/GNUmakerules + +install: rfmonitor-install \ + $(addprefix $(INSTALL_SBINDIR)/, $(SBIN_SCRIPTS)) \ + $(addprefix $(INSTALL_LIBEXECDIR)/, $(LIBEXEC_SCRIPTS)) + +boss-install: install install-subdirs + +rfmonitor-install: rfmonitor-subdir \ + $(INSTALL_WWWDIR)/rfmonitor/.htaccess \ + $(INSTALL_WWWDIR)/rfmonitor/listing.php + +$(INSTALL_WWWDIR)/rfmonitor/.htaccess: htaccess + $(INSTALL) -m 644 $< $@ + +$(INSTALL_WWWDIR)/rfmonitor/listing.php: listing.php + $(INSTALL) -m 644 $< $@ + +rfmonitor-subdir: + -mkdir -p $(INSTALL_WWWDIR)/rfmonitor + -mkdir -p $(INSTALL_WWWDIR)/rfmonitor/archive + +subboss-install: + +post-install: + +# +# Control node installation (aka, ops) +# +control-install: + +# This rule says what web* script depends on which installed binary directory. +$(WEB_SBIN_SCRIPTS): $(INSTALL_SBINDIR) + +# Just in case the dirs are not yet created, +$(INSTALL_SBINDIR) $(INSTALL_BINDIR): + +# And then how to turn the template into the actual script. +$(WEB_SBIN_SCRIPTS): $(TESTBED_SRCDIR)/WEBtemplate.in + @echo "Generating $@" + cat $< | sed -e 's,@PROGTOINVOKE@,$(word 2,$^)/$(subst web,,$@),' > $@ + +clean: clean-subdirs + +# How to recursively descend into subdirectories to make general +# targets such as `all'. +%.MAKE: + @$(MAKE) -C $(dir $@) $(basename $(notdir $@)) +%-subdirs: $(addsuffix /%.MAKE,$(SUBDIRS)) ; + +.PHONY: $(SUBDIRS) install diff --git a/powder/bspowerconfig.in b/powder/bspowerconfig.in new file mode 100644 index 0000000000..dca45dc938 --- /dev/null +++ b/powder/bspowerconfig.in @@ -0,0 +1,142 @@ +#!/usr/bin/perl -w +# +# Copyright (c) 2008-2020 University of Utah and the Flux Group. +# +# {{{GENIPUBLIC-LICENSE +# +# GENI Public License +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +# +# }}} +# +use strict; +use English; +use Getopt::Std; +use Data::Dumper; +use Date::Parse; + +# +# Generate config files for the local power control setup on the +# base station cnucs. +# +sub usage() +{ + print "Usage: bspowerconfig bsname\n"; + exit(1); +} +my $optlist = "d"; +my $debug = 0; +my %pduinfo = (); + +# +# Configure variables +# +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; +my $MAINSITE = @TBMAINSITE@; + +# un-taint path +$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin:/usr/site/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# Protos +sub fatal($); + +# +# Turn off line buffering on output +# +$| = 1; + +# Load the Testbed support stuff. +use lib "@prefix@/lib"; +use emdb; +use libtestbed; +use emutil; +use libEmulab; +use Node; + +# +# Load the radio info, which for now tells the node IDs of the radios. +# From there we can find the pdu info in the outlets table. +# +my $query_result = + DBQueryFatal("select node_id,location from apt_aggregate_radioinfo ". + "where installation_type='BS'"); + +while (my ($node_id,$location) = $query_result->fetchrow_array()) { + my $node = Node->Lookup($node_id); + if (!defined($node)) { + fatal("No such node $node_id in the DB"); + } + my $outlet_result = + DBQueryFatal("select power_id,outlet from outlets ". + "where node_id='$node_id'"); + if (!$outlet_result->numrows) { + print "No outlet info for $node_id, skipping\n"; + next; + } + my ($power_id,$outlet) = $outlet_result->fetchrow_array(); + my $powernode = Node->Lookup($power_id); + if (!defined($powernode)) { + fatal("No such node $power_id in the DB"); + } + my $iface = $powernode +} + +# +# Hmm, we do not have anything in the DB that maps a base station +# to its PDU name. +# +my %pdus = ( + "powder-pdu-smt" + "powder-pdu-fm" + "powder-pdu-meb" + "powder-pdu-dentistry" + "powder-pdu-ustar" + "powder-pdu-honors" +| powder-pdu-browning | +| powder-pdu-bes | +| powder-pdu-hospital +); + +# +# +# +my %options = (); +if (! getopts($optlist, \%options)) { + usage(); +} +if (defined($options{"d"})) { + $debug = 1; +} +usage() + if (!@ARGV); + +exit(0); + +sub fatal($) +{ + my ($msg) = @_; + + die("*** $0:\n". + " $msg\n"); +} + diff --git a/powder/htaccess b/powder/htaccess new file mode 100644 index 0000000000..739e9e3214 --- /dev/null +++ b/powder/htaccess @@ -0,0 +1,2 @@ +Options Indexes FollowSymLinks +Header Set Access-Control-Allow-Origin "*" diff --git a/powder/listing.php.in b/powder/listing.php.in new file mode 100644 index 0000000000..53df482e0d --- /dev/null +++ b/powder/listing.php.in @@ -0,0 +1,64 @@ +<?php +# +# Copyright (c) 2000-2020 University of Utah and the Flux Group. +# +# {{{EMULAB-LICENSE +# +# This file is part of the Emulab network testbed software. +# +# This file is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This file is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public +# License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this file. If not, see <http://www.gnu.org/licenses/>. +# +# }}} +# +$TBDIR = "@prefix@/"; +$RFDIR = "$TBDIR/www/rfmonitor/"; + +$listing = array(); + +function getFileList($dir, $archive) +{ + global $listing; + + // open pointer to directory and read list of files + $d = dir($dir); + if (!$d) { + exit("Failed to open $dir for reading"); + } + while (($entry = $d->read()) !== FALSE) { + # + # Only the ,gz files + # + if (!preg_match("/\.gz$/", $entry)) { + continue; + } + $listing[] = [ + 'name' => $entry, + 'size' => filesize("{$dir}{$entry}"), + 'lastmod' => filemtime("{$dir}{$entry}"), + 'archived' => $archive, + ]; + } + $d->close(); +} +getFileList($RFDIR, 0); +getFileList("$RFDIR/archive/", 1); + +header("Content-Type: text/plain"); +header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); +header("Cache-Control: no-cache, must-revalidate"); +header("Pragma: no-cache"); + +echo json_encode($listing); + +?> diff --git a/powder/powder_deadman.in b/powder/powder_deadman.in new file mode 100644 index 0000000000..103f35f052 --- /dev/null +++ b/powder/powder_deadman.in @@ -0,0 +1,360 @@ +#!/usr/bin/perl -w +# +# Copyright (c) 2008-2020 University of Utah and the Flux Group. +# +# {{{GENIPUBLIC-LICENSE +# +# GENI Public License +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +# +# }}} +# +use strict; +use English; +use Getopt::Std; +use Data::Dumper; +use Date::Parse; +use POSIX qw(strftime ceil); + +# +# Powder deadman switch; if do not hear from the Mothership within +# <insert number> minutes, we power throw all local experiment into +# panic (with power off) mode. +# +sub usage() +{ + print "Usage: powder_deadman [-d] [-s] [-n]\n"; + exit(1); +} +my $optlist = "dns"; +my $debug = 0; +my $impotent = 0; +my $oneshot = 0; +my $counter = 0; +my $lastping; + +# +# Configure variables +# +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; +my $MAINSITE = @TBMAINSITE@; +my $POWDER_DEADMAN = @POWDER_DEADMAN@; +my $LOGFILE = "$TB/log/powder_deadman.log"; +my $WAP = "$TB/sbin/wap"; +my $SUDO = "/usr/local/bin/sudo"; +my $POWER = "$TB/bin/power"; +my $SLEEP_INTERVAL = 10; +my $NOALIVE_THRESHOLD= 30; +my $ISALIVE_THRESHOLD= 60; + +# un-taint path +$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin:/usr/site/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# Protos +sub fatal($); +sub PowerAll($); +sub PowerControl($@); +sub NotifyTBOPS($$); + +# +# Turn off line buffering on output +# +$| = 1; + +if ($UID != 0) { + fatal("Must be root to run this script\n"); +} +# Silently exit if not enabled, specific to powder aggregates. +if (!$POWDER_DEADMAN) { + exit(0); +} + +# +# +# +my %options = (); +if (! getopts($optlist, \%options)) { + usage(); +} +if (defined($options{"d"})) { + $debug = 1; +} +if (defined($options{"s"})) { + $oneshot = 1; +} +if (defined($options{"n"})) { + $impotent = 1; +} + +# Load the Testbed support stuff. +use lib "@prefix@/lib"; +use emdb; +use EmulabConstants; +use libtestbed; +use emutil; +use libEmulab; +use Node; + +# In EmulabConstants +my $PROTOUSER = PROTOUSER(); + +if (! ($oneshot || $impotent)) { + if (CheckDaemonRunning("powder_deadman")) { + fatal("Not starting another powder_deadman daemon!"); + } + # Go to ground. + if (! $debug) { + if (TBBackGround($LOGFILE)) { + exit(0); + } + } + if (MarkDaemonRunning("powder_deadman")) { + fatal("Could not mark daemon as running!"); + } +} + +# +# When starting up, set this so know we are getting fresh keepalives +# from the Mothership. +# +emutil::UpdateVersionInfo('powder_isalive', time()); +emutil::UpdateVersionInfo('powder_deadman', undef); + +# +# Local enable. We just want to print something to the log if disabled. +# +my $enable; +if (GetSiteVar("powder/deadman_enable", \$enable) && $enable == 0) { + print "Currently disabled via sitevar powder/deadman_enable\n"; +} + +while (1) { + # + # We are not updated while NoLogins() is true. + # + if (NoLogins()) { + goto again; + } + # + # Local enable. + # + my $enable; + if (GetSiteVar("powder/deadman_enable", \$enable) && $enable == 0) { + goto again; + } + print "Running at ". + POSIX::strftime("20%y-%m-%d %H:%M:%S", localtime()) . "\n"; + + my $thisping = emutil::VersionInfo('powder_isalive'); + goto again + if (!defined($thisping)); + + my $deadman = emutil::VersionInfo('powder_deadman'); + + if ($debug) { + print "keepalive: $thisping\n"; + if ($deadman) { + print "deadman: $deadman, counter: $counter, lastping:" . + ($lastping ? $lastping : 0) . "\n"; + } + } + + # + # If we are already in a deadman state, we are waiting on getting + # keepalives from the MotherShip. We want to see isalive change a + # a few times in the last while before we power things on. + # + if ($deadman) { + if ($lastping == $thisping) { + # Nothing changing. + } + elsif (time() - $lastping > $ISALIVE_THRESHOLD) { + # Nothing for a while, lets reset the counter, we want to + # get three good keepalives within a smallish window. + $lastping = $thisping; + $counter = 0; + } + elsif ($counter < 3) { + $counter++; + $lastping = $thisping; + } + else { + # + # Mother is alive. + # + print "Mothership is alive at ". + POSIX::strftime("20%y-%m-%d %H:%M:%S", + localtime($lastping)) . "\n"; + PowerAll("on"); + emutil::UpdateVersionInfo('powder_deadman', undef); + $lastping = undef; + $counter = 0; + } + } + elsif (time() - $thisping > $NOALIVE_THRESHOLD) { + print "No contact from the Mothership for $NOALIVE_THRESHOLD seconds\n"; + emutil::UpdateVersionInfo('powder_deadman', time()); + $lastping = $thisping; + PowerAll("off"); + } + exit(0) + if ($oneshot); + + emutil::FlushCaches(); + again: + sleep($SLEEP_INTERVAL); +} +exit(0); + +# +# Put all active experiments into panic (with power off) mode. +# +sub PowerAll($) +{ + my ($onoff) = @_; + my @nodes = (); + + my $query_result = + DBQueryFatal("select node_id from nodes where role='testnode'"); + + while (my ($node_id) = $query_result->fetchrow_array()) { + my $node = Node->Lookup($node_id); + if (!defined($node)) { + print STDERR "Could not lookup node $node_id\n"; + next; + } + if (!$node->HasOutlet()) { + print STDERR "$node_id does not have an outlet, skipping.\n"; + next; + } + # When powering on, only reserved nodes/radios + next + if ($onoff eq "on" && + (!$node->IsReserved() || + ($node->pid() eq NODEDEAD_PID() && + $node->eid() eq NODEDEAD_EID()))); + + push(@nodes, $node); + } + return + if (!@nodes); + + PowerControl($onoff, @nodes); +} + +sub PowerControl($@) +{ + my ($onoff, @nodes) = @_; + + foreach my $node (@nodes) { + my $node_id = $node->node_id(); + + $node->Refresh(); + + if ($onoff eq "on") { + if ($node->eventstate() eq TBDB_NODESTATE_POWEROFF()) { + if ($impotent) { + print "Would power on $node_id\n"; + } + else { + print "Powering on $node_id\n"; + system("$SUDO -u $PROTOUSER $WAP $POWER on $node_id"); + # + # We want to notify if this fails ... + # + } + } + } + else { + if ($node->eventstate() ne TBDB_NODESTATE_POWEROFF()) { + if ($impotent) { + print "Would power off $node_id\n"; + } + else { + print "Powering off $node_id\n"; + system("$SUDO -u $PROTOUSER $WAP $POWER off $node_id"); + # + # We want to notify if this fails. + # + } + } + } + } + return 0; +} + +sub fatal($) +{ + my ($msg) = @_; + + if (! ($oneshot || $debug || $impotent)) { + # + # Send a message to the testbed list. + # + SENDMAIL($TBOPS, + "powder_deadman died", + $msg, + $TBOPS); + } + MarkDaemonStopped("powder_deadman") + if (! ($oneshot || $impotent)); + + die("*** $0:\n". + " $msg\n"); +} + +# +# Notify TBOPS +# +sub NotifyTBOPS($$) +{ + my ($subject, $message) = @_; + + if ($impotent) { + print "$subject\n"; + print "$message\n"; + return; + } + SENDMAIL($TBOPS, $subject, $message, $TBOPS); +} + +# +# We notify a user if the experimental node is reserved. +# +sub NotifyUser($$$) +{ + my ($node, $subject, $message) = @_; + my $experiment = $node->Reservation(); + my $node_id = $node->node_id(); + my $creator = $experiment->GetCreator(); + my $user_email = $creator->email(); + my $user_name = $creator->name(); + + if ($impotent) { + print "$subject\n"; + print "$message\n"; + return; + } + SENDMAIL("$user_name <$user_email>", $subject, $message, $TBOPS); +} + diff --git a/powder/powder_keepalive.in b/powder/powder_keepalive.in new file mode 100644 index 0000000000..86dcd4b6fe --- /dev/null +++ b/powder/powder_keepalive.in @@ -0,0 +1,180 @@ +#!/usr/bin/perl -w +# +# Copyright (c) 2008-2020 University of Utah and the Flux Group. +# +# {{{GENIPUBLIC-LICENSE +# +# GENI Public License +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and/or hardware specification (the "Work") to +# deal in the Work without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, +# and/or sell copies of the Work, and to permit persons to whom the Work +# is furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Work. +# +# THE WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE WORK OR THE USE OR OTHER DEALINGS +# IN THE WORK. +# +# }}} +# +use strict; +use English; +use Getopt::Std; +use Data::Dumper; +use Date::Parse; +use POSIX qw(strftime ceil); + +# +# Ping each of the remote endpoints to let them know boss is still +# here and operational. powder_deadman at the endpoints is watching +# for the keep alive signal, and will shutdown the local radios if +# it does not hear from the Mothership for some length of time, to +# be determined. +# +sub usage() +{ + print "Usage: powder_keepalive [-d] [-s] [-n]\n"; + exit(1); +} +my $optlist = "dns"; +my $debug = 0; +my $impotent = 0; +my $oneshot = 0; +my $deadman = 0; + +# +# Configure variables +# +my $TB = "@prefix@"; +my $TBOPS = "@TBOPSEMAIL@"; +my $MAINSITE = @TBMAINSITE@; +my $LOGFILE = "$TB/log/powder_keepalive.log"; +my $SLEEP_INTERVAL = 300; + +# un-taint path +$ENV{'PATH'} = '/bin:/usr/bin:/usr/local/bin:/usr/site/bin'; +delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'}; + +# Protos +sub fatal($); + +# +# Turn off line buffering on output +# +$| = 1; + +if ($UID != 0) { + fatal("Must be root to run this script\n"); +} +if (!$MAINSITE) { + exit(0); +} + +# +# +# +my %options = (); +if (! getopts($optlist, \%options)) { + usage(); +} +if (defined($options{"d"})) { + $debug = 1; +} +if (defined($options{"s"})) { + $oneshot = 1; +} +if (defined($options{"n"})) { + $impotent = 1; +} + +# Load the Testbed support stuff. +use lib "@prefix@/lib"; +use emdb; +use libtestbed; +use emutil; +use libEmulab; +use APT_Aggregate; + +if (! ($oneshot || $impotent)) { + if (CheckDaemonRunning("powder_keepalive")) { + fatal("Not starting another powder_keepalive daemon!"); + } + # Go to ground. + if (! $debug) { + if (TBBackGround($LOGFILE)) { + exit(0); + } + } + if (MarkDaemonRunning("powder_keepalive")) { + fatal("Could not mark daemon as running!"); + } +} + +while (1) { + # + # We always run, we do not look at NoLogins(). + # + print "Running at ". + POSIX::strftime("20%y-%m-%d %H:%M:%S", localtime()) . "\n"; + + my @aggregates = APT_Aggregate->LookupAll(); + if (!@aggregates) { + print "No Aggregates!\n"; + goto again; + } + foreach my $aggregate (@aggregates) { + next + if (!($aggregate->isFE() || $aggregate->ismobile())); + + # Skip if the monitor marked it as down. + next + if (!$aggregate->IsUp()); + + my $nickname = $aggregate->nickname(); + if ($debug) { + print "Pinging $nickname\n"; + } + my $error; + # Use the fastpath RPC + if ($aggregate->CheckStatus(\$error, 1)) { + print STDERR "$nickname: $error\n"; + } + } + exit(0) + if ($oneshot); + + emutil::FlushCaches(); + again: + sleep($SLEEP_INTERVAL); +} +exit(0); + +sub fatal($) +{ + my ($msg) = @_; + + if (! ($oneshot || $debug || $impotent)) { + # + # Send a message to the testbed list. + # + SENDMAIL($TBOPS, + "powder_keepalive died", + $msg, + $TBOPS); + } + MarkDaemonStopped("powder_keepalive") + if (! ($oneshot || $impotent)); + + die("*** $0:\n". + " $msg\n"); +} diff --git a/apt/powder_shutdown.in b/powder/powder_shutdown.in similarity index 98% rename from apt/powder_shutdown.in rename to powder/powder_shutdown.in index 120e0eba3e..0b6f4f91b8 100644 --- a/apt/powder_shutdown.in +++ b/powder/powder_shutdown.in @@ -1,6 +1,6 @@ #!/usr/bin/perl -w # -# Copyright (c) 2000-2019 University of Utah and the Flux Group. +# Copyright (c) 2000-2020 University of Utah and the Flux Group. # # {{{EMULAB-LICENSE # @@ -159,7 +159,7 @@ while (my ($uuid) = $query_result->fetchrow_array()) { my $aptagg = $agg->GetAptAggregate(); next - if ($agg->status() eq "deferred"); + if ($agg->deferred()); # # Skip anything that is not ready. Need to handle imaging. diff --git a/apt/powderstats.in b/powder/powderstats.in similarity index 100% rename from apt/powderstats.in rename to powder/powderstats.in diff --git a/www/aptui/frequency-graph.ajax b/www/aptui/frequency-graph.ajax index 3e9cbc15d0..3844797833 100644 --- a/www/aptui/frequency-graph.ajax +++ b/www/aptui/frequency-graph.ajax @@ -70,7 +70,57 @@ function Do_GetFrequencyData() SPITAJAX_ERROR(-1, "Illegal interface: $iface"); return 1; } - $url = $aggregate->weburl() . "/rfmonitor/${node_id}:${iface}.csv"; + $logid = ""; + if (isset($ajax_args["logid"]) && $ajax_args["logid"] != "") { + if (!TBvalid_userdata($ajax_args["logid"])) { + SPITAJAX_ERROR(-1, "Illegal logid"); + return 1; + } + $logid = "-" . $ajax_args["logid"]; + } + $archived = 0; + if (isset($ajax_args["archived"]) && $ajax_args["archived"] == "1") { + $archived = 1; + } + $url = $aggregate->weburl() . "/rfmonitor/" . + ($archived ? "/archive/" : "") . + "${node_id}:${iface}${logid}.csv.gz"; + $url = preg_replace("/^https:/i","http:", $url); + + $socket = fopen($url, "r"); + if (!$socket) { + SPITAJAX_ERROR(-1, "Could not open URL"); + return; + } + fpassthru($socket); + fclose($socket); + return; +} + +# +# Get directory We have both a CORS and unknown CA problem. +# +function Do_GetListing() +{ + global $instance, $creator, $this_user; + global $ajax_args; + $opt = ""; + + if (!isset($ajax_args["cluster"])) { + SPITAJAX_ERROR(1, "Missing urn argument"); + return 1; + } + $cluster = $ajax_args["cluster"]; + if (!TBvalid_node_id($cluster)) { + SPITAJAX_ERROR(-1, "Illegal characters in cluster"); + return 1; + } + $aggregate = Aggregate::LookupByNickname($cluster); + if (!$aggregate) { + SPITAJAX_ERROR(-1, "No such cluster: $cluster"); + return 1; + } + $url = $aggregate->weburl() . "/rfmonitor/listing.php"; $url = preg_replace("/^https:/i","http:", $url); $socket = fopen($url, "r"); @@ -83,6 +133,7 @@ function Do_GetFrequencyData() return; } + # Local Variables: # mode:php # End: diff --git a/www/aptui/frequency-graph.php b/www/aptui/frequency-graph.php index 573d208c8d..56ff9a7b08 100644 --- a/www/aptui/frequency-graph.php +++ b/www/aptui/frequency-graph.php @@ -41,6 +41,8 @@ $isadmin = (ISADMIN() ? 1 : 0); $reqargs = RequiredPageArguments("cluster", PAGEARG_STRING, "node_id", PAGEARG_STRING, "iface", PAGEARG_STRING); +$optargs = OptionalPageArguments("logid", PAGEARG_STRING, + "archived", PAGEARG_BOOLEAN); # # The monitor looks at only one iface, rf0. That may change later. @@ -67,6 +69,16 @@ if ($iface != "rf0") { SPITUSERERROR("Illegal interface: $iface"); exit(); } +if (isset($logid)) { + if (!TBvalid_userdata($logid)) { + SPITUSERERROR("Illegal logid: $logid"); + exit(); + } +} +if (!isset($archived)) { + $archived = 0; +} +$url = $aggregate->weburl() . "/rfmonitor"; SPITHEADER(1); echo "<link rel='stylesheet' @@ -84,6 +96,11 @@ echo "<script type='text/javascript'>\n"; echo " window.CLUSTER = '$cluster';\n"; echo " window.NODEID = '$node_id';\n"; echo " window.IFACE = '$iface';\n"; +echo " window.URL = '$url';\n"; +echo " window.ARCHIVED = $archived;\n"; +if (isset($logid)) { + echo " window.LOGID = '$logid';\n"; +} echo "</script>\n"; REQUIRE_UNDERSCORE(); @@ -93,6 +110,7 @@ REQUIRE_APTFORMS(); AddLibrary("js/freqgraphs.js"); AddTemplateList(array("frequency-graph")); SPITREQUIRE("js/frequency-graph.js", + "<script src='js/lib/pako/pako.min.js'></script>\n". "<script src='js/lib/d3.v5.js'></script>\n"); SPITFOOTER(); ?> diff --git a/www/aptui/js/freqgraphs.js b/www/aptui/js/freqgraphs.js index d4c1b3d989..f7b61d9beb 100644 --- a/www/aptui/js/freqgraphs.js +++ b/www/aptui/js/freqgraphs.js @@ -11,7 +11,7 @@ window.ShowFrequencyGraph = (function () var d3 = d3v5; function CreateGraph(args, data) { - console.log(data); + //console.log(data); var selector = args.selector + " .frequency-graph-subgraph"; var parentWidth = $(selector).width(); @@ -282,7 +282,7 @@ window.ShowFrequencyGraph = (function () }); bin.avg = sum / _.size(bin.samples); }); - console.info("bins", result); + //console.info("bins", result); return result; } @@ -317,6 +317,11 @@ window.ShowFrequencyGraph = (function () var ParentTop = $(selector).position().top; var ParentLeft = $(selector).position().left; + // Clear old graph + $(selector).html(""); + // And the sub graph. + $(args.selector + " .frequency-graph-subgraph").html(""); + var margin = {top: 20, right: 20, bottom: 130, left: 55}; var width = parentWidth - margin.left - margin.right; var height = parentHeight - margin.top - margin.bottom; @@ -591,9 +596,12 @@ window.ShowFrequencyGraph = (function () return d; } - function GetFrequencyData(url, route, method, args, callback) + function GetFrequencyData(datatype, route, method, args, callback) { var url = 'server-ajax.php'; + if (!datatype) { + datatype = "text"; + } var networkError = { "code" : -1, @@ -626,7 +634,7 @@ window.ShowFrequencyGraph = (function () type: "GET", // the type of data we expect back - dataType : "text", + dataType : datatype, }); var defer = $.Deferred(); @@ -640,24 +648,152 @@ window.ShowFrequencyGraph = (function () return defer; } - return function(args) { + // Easier to get a binary (gzip) file this way, since jquery does + // not directly support doing this. + function GetBlob(url, success, failure) { + var oReq = new XMLHttpRequest(); + oReq.open("GET", url, true); + oReq.responseType = "arraybuffer"; + + oReq.onload = function(oEvent) { + success(oReq.response) + }; + oReq.onerror = function(oEvent) { + failure(); + }; + oReq.send(); + } + + /* + * Saving this. It is faster to go directly to the aggregate, but + * they all have to have valid certificates. Note that we cannot load + * it via http from inside an https page, the browser will block it. + */ + function SaveMe(args) { console.info("ShowFrequencyGraph", args); + GetBlob(window.URL + ".gz", + function (arrayBuffer) { + console.info("gz version"); + var output = pako.inflate(arrayBuffer, { 'to': 'string' }); + + var data = d3.csvParse(output, type); + CreateBinGraph(args, data); + }, + function () { + $.get(window.URL) + .done(function (data) { + console.info("text version"); + data = d3.csvParse(data, type); + CreateBinGraph(args, data); + }) + .fail(function() { + alert("Could not get data file: " + window.URL); + }); + }); + } - GetFrequencyData(null, "frequency-graph", "GetFrequencyData", - {"cluster" : args.cluster, - "node_id" : args.node_id, - "iface" : args.iface}, - function (value) { - // XXX This will always be a string. Need to - // figure out how to deal with errors. - if (typeof(value) == "object") { - console.info("Could not get CVS data" + - "data: " + value.value); - return; - } - var data = d3.csvParse(value, type); - CreateBinGraph(args, data); - }); + function BuildMenu(args) + { + var callback = function (value) { + // XXX This will always be a string. Need to + // figure out how to deal with errors. + if (typeof(value) == "object") { + console.info("Could not get listing data: " + value.value); + return; + } + var listing = JSON.parse(_.unescape(value)); + console.info(listing); + // nuc2:rf0-1588699912.csv.gz + var re = /([^:]+):([^\-]+)\-(\d+)\.csv\.gz/; + _.each(listing, function(info, index) { + var name = info.name; + var match = name.match(re); + //console.info(name, match); + if (!match) { + return; + } + info["node_id"] = match[1]; + info["iface"] = match[2]; + info["logid"] = match[3]; + info["cluster"] = args.cluster; + info["selector"] = args.selector; + + var url = "frequency-graph.php" + + "?cluster=" + args.cluster + + "&node_id=" + info.node_id + + "&iface=" + info.iface + + "&logid=" + info.logid; + if (info.archived) { + url + "&archived=" + info.archived; + } + var html = + "<li>" + + " <a href='" + url + "' index='<%- index %>'>" + + match[1] + ":" + match[2] + " - " + + moment(match[3], "X").format("L LT") + "</a></li>"; + var item = $(html); + // If the incoming args match this listing, start it active. + if (args.logid && + info.node_id == args.node_id && + info.iface == args.iface && + info.logid == args.logid) { + $(item).addClass("active"); + } + $(item).find("a").click(function (event) { + event.preventDefault(); + $('#moregraphs-dropdown').find("li").removeClass("active"); + $(item).addClass("active"); + $(".frequency-graph-date") + .html(moment(match[3], "X").format("L LT")) + .removeClass("hidden"); + UpdateGraph(info); + }); + $('#moregraphs-dropdown').append(item); + }); + }; + GetFrequencyData("html", "frequency-graph", "GetListing", + {"cluster" : args.cluster}, callback); + } + + function UpdateGraph(args) + { + /* + * Gack, we cannot get binary data with the jquery ajax call. + * Well there is lots of noise from google about how to mess + * with it, but instead I am just going to create a GET url + * that talks ajax server routine. + */ + var url = "server-ajax.php" + + "?ajax_route=frequency-graph" + + "&ajax_method=GetFrequencyData" + + "&ajax_args[cluster]=" + args.cluster + + "&ajax_args[node_id]=" + args.node_id + + "&ajax_args[iface]=" + args.iface; + // Optional specific log. + if (args.logid) { + url = url + "&ajax_args[logid]=" + args.logid; + } + if (args.archived) { + url = url + "&ajax_args[archived]=1"; + } + console.info(url); + + GetBlob(url, + function (arrayBuffer) { + console.info("gz version"); + var output = pako.inflate(arrayBuffer, { 'to': 'string' }); + + var data = d3.csvParse(output, type); + CreateBinGraph(args, data); + }, + function () { + alert("Could not get data file: " + url); + }); + } + + return function(args) { + BuildMenu(args); + UpdateGraph(args); }; } )(); diff --git a/www/aptui/js/frequency-graph.js b/www/aptui/js/frequency-graph.js index 45e17fdc0a..37a3349bd3 100644 --- a/www/aptui/js/frequency-graph.js +++ b/www/aptui/js/frequency-graph.js @@ -14,6 +14,9 @@ $(function () "cluster" : window.CLUSTER, "node_id" : window.NODEID, "iface" : window.IFACE, + "url" : window.URL, + "logid" : window.LOGID, + "archived" : window.ARCHIVED, }; $('#main-body').html(mainTemplate(options)); // Its a little too big by itself diff --git a/www/aptui/js/status.js b/www/aptui/js/status.js index 30f5c7a648..e8a0a12441 100644 --- a/www/aptui/js/status.js +++ b/www/aptui/js/status.js @@ -4235,6 +4235,7 @@ $(function () "cluster" : amlist[info.aggregate_urn].nickname, "node_id" : info.node_id, "iface" : "rf0", + "logid" : null, } var html = monitorTemplate(options); diff --git a/www/aptui/server-ajax.php b/www/aptui/server-ajax.php index 2b87a1d009..d6b3ef43bd 100644 --- a/www/aptui/server-ajax.php +++ b/www/aptui/server-ajax.php @@ -641,6 +641,8 @@ $routing = array("geni-login" => "guest" => false, "methods" => array("GetFrequencyData" => "Do_GetFrequencyData", + "GetListing" => + "Do_GetListing", ) ), "rfrange" => diff --git a/www/aptui/status.php b/www/aptui/status.php index 7429b91633..d6ad3355ed 100644 --- a/www/aptui/status.php +++ b/www/aptui/status.php @@ -307,6 +307,7 @@ AddLibrary("js/bindings.js"); AddLibrary("js/paramsets.js"); if ($ISPOWDER) { AddLibrary("js/freqgraphs.js"); + AddLibrary("js/lib/pako/pako.min.js"); } SPITREQUIRE("js/status.js"); diff --git a/www/aptui/template/frequency-graph.html b/www/aptui/template/frequency-graph.html index 9c08e742e8..d4d121e4dd 100644 --- a/www/aptui/template/frequency-graph.html +++ b/www/aptui/template/frequency-graph.html @@ -1,8 +1,33 @@ +<style> + #moregraphs-dropdown { + max-height: 400px; + overflow-y: scroll; + } +</style> <div class="frequency-graph-div"> + <span class='dropdown pull-right'> + <button id='action-menu-button' type='button' + class='btn btn-default btn-xs dropdown-toggle' + data-toggle='dropdown'> + More Graphs <span class="caret"></span> + </button> + <ul class='dropdown-menu text-left' id="moregraphs-dropdown" role='menu'> + </ul> + </span> <center> <h4> Radio Monitoring Graph for - <%- cluster %> <%- node_id %>:<%- iface %> + <%- cluster %> + <span class="frequency-graph-nodeid"> + <%- node_id %></span>:<span class="frequency-graph-iface"><%- iface %> + </span> + <% if (logid) { %> + <span class="frequency-graph-date" style="margin-left: 15px;"> + <%- moment(logid, "X").format("L LT") %></span> + <% } else { %> + <span class="frequency-graph-date hidden" + style="margin-left: 15px;"></span> + <% } %> </h4> </center> <div class="panel panel-default" style="margin-bottom: 0px;"> -- GitLab