diff --git a/admin/archive/install-cisco-models.sh b/admin/archive/install-cisco-models.sh new file mode 100755 index 0000000..6e0bce4 --- /dev/null +++ b/admin/archive/install-cisco-models.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +echo This script will NOT backup your existing NMIS installation, please backup your installation before proceeding. +echo press Enter to continue ctrl+C to stop. +read X + +unalias cp + +cp ./install/* /usr/local/nmis8/install +cp ./models-install/* /usr/local/nmis8/models +cp ./mibs/nmis_mibs.oid /usr/local/nmis8/mibs + +/usr/local/nmis8/install/install_cisco_model_dev.pl + +/usr/local/nmis8/admin/fixperms.pl diff --git a/admin/uuid_update_nodes.pl b/admin/archive/uuid_update_nodes.pl similarity index 98% rename from admin/uuid_update_nodes.pl rename to admin/archive/uuid_update_nodes.pl index 007fb23..d7471ab 100755 --- a/admin/uuid_update_nodes.pl +++ b/admin/archive/uuid_update_nodes.pl @@ -55,6 +55,6 @@ my $C = loadConfTable(conf=>$arg{conf},debug=>$arg{debug}); # Update the UUIDs for the nodes. -createNodeUUID(); +NMIS::UUID::createNodeUUID(); print $t->elapTime(). " Begin\n"; diff --git a/admin/export_nodes.pl b/admin/export_nodes.pl index 923d76f..52b513b 100755 --- a/admin/export_nodes.pl +++ b/admin/export_nodes.pl @@ -108,7 +108,7 @@ # Step 7: Check the results if ( not -f $arg{nodes} ) { - createNodeUUID(); + NMIS::UUID::createNodeUUID(); exportNodes($arg{nodes}); } else { @@ -169,4 +169,4 @@ sub changeCellSep { $string =~ s/\r\n/\\n/g; $string =~ s/\n/\\n/g; return $string; -} \ No newline at end of file +} diff --git a/admin/migrate_rrd_locations.pl b/admin/migrate_rrd_locations.pl index 1f541c0..974056a 100755 --- a/admin/migrate_rrd_locations.pl +++ b/admin/migrate_rrd_locations.pl @@ -35,7 +35,7 @@ # # nmis collection is disabled while this operation is performed, and a record # of operations is kept for rolling back in case of problems. -our $VERSION = "8.6.1G"; +our $VERSION = "8.6.2a"; use strict; use File::Copy; @@ -50,16 +50,14 @@ use NMIS; use func 1.2.1; -my $usage = "Usage: ".basename($0)." newlayout=/path/to/new/Common-database.nmis [simulate=true] [info=true] [missingonly=true]\n +my $usage = "Usage: ".basename($0)." newlayout=/path/to/new/Common-database.nmis [simulate=true] [info=true]\n newlayout: Common-database.nmis file to use for new locations, merged into current one. simulate: only show what would be done, don't make any changes info: produce more informational output -missingonly: no renaming, just add missing entries to Common-database. \n\n"; my %args=getArguements(@ARGV); my $simulate = getbool($args{simulate}); -my $missingonly = getbool($args{missingonly}); my $leavelocked = getbool($args{leavelocked}); my $base = Cwd::abs_path("$FindBin::RealBin/.."); @@ -105,66 +103,15 @@ my $LNT = loadLocalNodeTable(); my (%rrdfiles,$countfiles); -# verify that the current common-database doesn't have anything custom that -# the new shipped version does not have + my $curlayoutfile = $C->{''}."/Common-database.nmis"; my $curlayout = readFiletoHash(file => $curlayoutfile); if (ref($curlayout) ne "HASH" or ref($curlayout->{database}) ne "HASH") { - print STDERR "Cannot fine a current database layout file (Common-database.nmis), cannot proceed with migration!\n"; + print STDERR "Cannot find a current database layout file (Common-database.nmis), cannot proceed with migration!\n"; exit 1; } -print STDERR "Checking compatibility of current and new database layout files...\n"; -for my $oldtypekey (sort keys %{$curlayout->{database}->{type}}) -{ - if (!$newlayout->{database}->{type}->{$oldtypekey}) - { - print STDERR "\n\nError: Your current database layout file contains custom entries!\n -There is an entry for the RRD type \"$oldtypekey\", which is not present -in the new database layout. This is likely caused by local custom models. -This script cannot perform any database migration until all custom types -are merged into $newdbf, and will abort now.\n\n"; - exit 1; - } -} - -if ($missingonly) -{ - print STDERR "Checking for missing entries in current database layout\n"; - my $needtosave; - - for my $newtype (keys %{$newlayout->{database}->{type}}) - { - next if exists $curlayout->{database}->{type}->{$newtype}; - print STDERR "Adding $newtype\n"; - if (!$simulate) - { - $curlayout->{database}->{type}->{$newtype} = - $newlayout->{database}->{type}->{$newtype}; - $needtosave = 1; - } - } - if ($needtosave) - { - push @rollback, "mv $curlayoutfile.pre-update $curlayoutfile"; - rename($curlayoutfile,"$curlayoutfile.pre-update") - or die "Could not rename $curlayoutfile: $!\n"; - writeHashtoFile(file => $curlayoutfile, data => $curlayout); - - # save the rollback file anyway - &saverollback; - print STDERR "Saving rollback information in $rollbackf\n"; - print STDERR "Update complete.\n"; - } - else - { - print STDERR "No missing entries found.\n"; - } - unlink($lockoutfile) if (!$leavelocked); - exit 0; -} - print STDERR "Identifying RRD files to rename\n"; # find all rrd files for all nodes with sys() objects based on # the current config, and remember the locations, the types, index, element - everything :-/ @@ -174,14 +121,14 @@ # cache disabled so that the func table_cache gets populated $S->init(name=>$node, snmp=>'false', cache_models => 'false'); my $NI = $S->ndinfo; - + # walk graphtype keys, if hash value: key is index, go one level deeper; # otherwise key of graphtype is all getDBName needs for my $section (keys %{$NI->{graphtype}}) { # the generic ones remain where they were - in /metrics/. next if ($section =~ /^(network|nmis|metrics)$/); - + if (ref($NI->{graphtype}->{$section}) eq "HASH") { my $index = $section; @@ -223,7 +170,14 @@ or ref($cacheobj->{$cachekey}->{data}->{database}) ne "HASH" or ref($cacheobj->{$cachekey}->{data}->{database}->{type}) ne "HASH"); -$cacheobj->{$cachekey}->{data}->{database}->{type} = $newlayout->{database}->{type}; +# merge the old and new stuff, new wins but orphaned old is NOT deleted! +for my $newentry (keys %{$newlayout->{database}->{type}}) +{ + $cacheobj->{$cachekey}->{data}->{database}->{type}->{$newentry} + = $newlayout->{database}->{type}->{$newentry}; +} + +my $gotchas; # oldfile -> newfile my %todos; @@ -234,24 +188,43 @@ # again, disabling the model cache use so that the massaged table_cache remains in force my $S = Sys->new; $S->init(name=>$node, snmp=>'false', cache_models => 'false'); - + for my $oldname (keys %{$rrdfiles{$node}}) { my $meta = $rrdfiles{$node}->{$oldname}; - + my $newname = $S->getDBName(graphtype => $meta->{graphtype}, index => $meta->{index}, item => $meta->{item}); if (!$newname) { + if ($simulate) + { + warn("FATAL: Cannot determine new name for $oldname (graphtype=".$meta->{graphtype} + .", index=".$meta->{index}.", item=".$meta->{item}.")\n"); + $newname = "fatal_conflict_".++$gotchas; # fudgery to make output possible under simulate + next; + } die "Cannot determine new name for $oldname (graphtype=".$meta->{graphtype} - .", index=".$meta->{index}.", item=".$meta->{item}.")\n"; + .", index=".$meta->{index}.", item=".$meta->{item}.")\n"; } if ($oldname ne $newname) { my $friendlyold = $oldname; $friendlyold =~ s/^$C->{database_root}//; my $friendlynew = $newname; $friendlynew =~ s/^$C->{database_root}//; - + + # make sure there's no clashes + if (my @conflicts = grep($todos{$_} eq $newname, keys %todos)) + { + if ($simulate) + { + warn("FATAL: cannot rename $oldname to $newname, clashes with renaming $conflicts[0]\n"); + $todos{$oldname} = "fatal_conflict_".++$gotchas; + next; + } + die("FATAL: cannot rename $oldname to $newname, clashes with renaming $conflicts[0]\n"); + } + info("Old RRD file $friendlyold, new $friendlynew"); $todos{$oldname} = $newname; } @@ -264,6 +237,7 @@ my %olddirs; print STDERR "Found ".int(scalar(keys %todos)). " RRD files to move.\n"; +print STDERR "$gotchas conflicts that must be resolved before migration!\n" if ($gotchas); if (keys %todos and !$simulate) { @@ -332,7 +306,11 @@ push @rollback, "mv $curlayoutfile.pre-migrate $curlayoutfile"; rename($curlayoutfile,"$curlayoutfile.pre-migrate") or die "Could not rename $curlayoutfile: $!\n"; - $curlayout->{database}->{type} = $newlayout->{database}->{type}; + # merge the old and new stuff, new wins but orphaned old is NOT deleted! + for my $newentry (keys %{$newlayout->{database}->{type}}) + { + $curlayout->{database}->{type}->{$newentry} = $newlayout->{database}->{type}->{$newentry}; + } writeHashtoFile(file => $curlayoutfile, data => $curlayout); # save the rollback file anyway @@ -353,6 +331,7 @@ } exit 0; +# record relationship of node -> rrdfile sub record_rrd { my (%args) = @_; @@ -377,6 +356,16 @@ sub record_rrd if (exists $rrdfiles{$args{node}}->{$fn}) { my $old = $rrdfiles{$args{node}}->{$fn}; + + if ($simulate) + { + warn("FATAL: $fn already known!\n +clash between old node=$old->{node}, graphtype=$old->{graphtype}, index=$old->{index}, item=$old->{item} +and new node=$args{node}, graphtype=$args{graphtype}, index=$args{index}, item=$args{item}\n"); + $rrdfiles{ $args{node}."_fatal_conflict_".++$gotchas } = {%args}; + return; + } + die "error: $fn already known!\n clash between old node=$old->{node}, graphtype=$old->{graphtype}, index=$old->{index}, item=$old->{item} and new node=$args{node}, graphtype=$args{graphtype}, index=$args{index}, item=$args{item}\n"; diff --git a/admin/model_audit.pl b/admin/model_audit.pl index cf5dc2d..6bcd420 100755 --- a/admin/model_audit.pl +++ b/admin/model_audit.pl @@ -49,7 +49,7 @@ my $LNT = loadLocalNodeTable(); -print qq|"name","host","group","version","active","collect","last updated","icmp working","wmi working","snmp working","nodeModel","nodeVendor","nodeType","roleType","netType","sysObjectID","sysObjectName","sysDescr","intCount","intCollect"\n|; +print qq|"name","host","group","version","active","collect","last updated","icmp working","wmi working","snmp working","nodeModel","nodeVendor","nodeType","roleType","netType","sysObjectID","sysName","sysObjectName","sysDescr","intCount","intCollect"\n|; foreach my $node (sort keys %{$LNT}) { @@ -73,15 +73,15 @@ my $sysDescr = $NI->{system}{sysDescr}; $sysDescr =~ s/[\x0A\x0D]/\\n/g; - my $lastUpdate = returnDateStamp($NI->{system}{lastUpdateSec}); + my $lastUpdate = returnDateStamp($NI->{system}{last_poll}); my $pingable = getbool($LNT->{$node}->{ping})? getbool($NI->{system}{nodedown})? "false": "true" : "N/A"; my $snmpable = defined($NI->{system}->{snmpdown})? getbool($NI->{system}->{snmpdown})? "false" : "true" : "N/A"; my $wmiworks = defined($NI->{system}->{wmidown})? getbool($NI->{system}->{wmidown})? "false" : "true" : "N/A"; - $lastUpdate = "unknown" if not defined $NI->{system}{lastUpdateSec}; + $lastUpdate = "unknown" if not defined $NI->{system}{last_poll}; $pingable = "unknown" if not defined $NI->{system}{nodedown}; $snmpable = "unknown" if not defined $NI->{system}{snmpdown}; - print qq|"$LNT->{$node}{name}","$LNT->{$node}{host}","$LNT->{$node}{group}","$LNT->{$node}{version}","$LNT->{$node}{active}","$LNT->{$node}{collect}","$lastUpdate","$pingable","$wmiworks","$snmpable","$NI->{system}{nodeModel}","$NI->{system}{nodeVendor}","$NI->{system}{nodeType}","$LNT->{$node}{roleType}","$LNT->{$node}{netType}","$NI->{system}{sysObjectID}","$NI->{system}{sysObjectName}","$sysDescr","$intCount","$intCollect"\n|; + print qq|"$LNT->{$node}{name}","$LNT->{$node}{host}","$LNT->{$node}{group}","$LNT->{$node}{version}","$LNT->{$node}{active}","$LNT->{$node}{collect}","$lastUpdate","$pingable","$wmiworks","$snmpable","$NI->{system}{nodeModel}","$NI->{system}{nodeVendor}","$NI->{system}{nodeType}","$LNT->{$node}{roleType}","$LNT->{$node}{netType}","$NI->{system}{sysObjectID}","$NI->{system}{sysName}","$NI->{system}{sysObjectName}","$sysDescr","$intCount","$intCollect"\n|; } diff --git a/admin/model_format.pl b/admin/model_format.pl deleted file mode 100755 index 0ad9f61..0000000 --- a/admin/model_format.pl +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/perl -# -## $Id: model_audit.pl,v 1.1 2012/08/13 05:09:17 keiths Exp $ -# -# Copyright (C) Opmantek Limited (www.opmantek.com) -# -# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM -# -# This file is part of Network Management Information System ("NMIS"). -# -# NMIS is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# NMIS 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with NMIS (most likely in a file named LICENSE). -# If not, see -# -# For further information on NMIS or for a license other than GPL please see -# www.opmantek.com or email contact@opmantek.com -# -# User group details: -# http://support.opmantek.com/users/ -# -# ***************************************************************************** - -use FindBin; -use lib "$FindBin::Bin/../lib"; - -use strict; -use func; - -my %arg = getArguements(@ARGV); - -# Set debugging level. -my $debug = setDebug($arg{debug}); -$debug = 1; - -my $C = loadConfTable(conf=>$arg{conf},debug=>$arg{debug}); - -my $modelName = "Model-ZyXEL-MGS.nmis"; - -my $modelFile = "/usr/local/nmis8/models/$modelName"; -my $newModelFile = "$modelFile.new"; - -my $model = readFiletoHash(file=>$modelFile); -writeHashtoFile(file=>$newModelFile,data=>$model); - - diff --git a/admin/nmis_run_node.pl b/admin/nmis_run_node.pl index bb11244..525ee2f 100755 --- a/admin/nmis_run_node.pl +++ b/admin/nmis_run_node.pl @@ -81,7 +81,7 @@ my $recentUpdate = 0; # has an update been run in the last 24 hours? - if ( $NI->{system}{lastUpdatePoll} > time() - $update ) { + if ( $NI->{system}{last_update} > time() - $update ) { $recentUpdate = 1; } @@ -106,7 +106,7 @@ print "SKIP No Match: $NI->{system}{name}: $arg{field}=$NI->{system}{$arg{field}}\n"; } if ( not $runIt and $recentUpdate ) { - print "SKIP Update: $NI->{system}{name}: lastUpdatePoll=$NI->{system}{lastUpdatePoll}\n"; + print "SKIP Update: $NI->{system}{name}: lastUpdatePoll=$NI->{system}{last_update}\n"; } } } diff --git a/admin/node_admin.pl b/admin/node_admin.pl index 3eeb05f..43b3bab 100755 --- a/admin/node_admin.pl +++ b/admin/node_admin.pl @@ -432,7 +432,7 @@ # no uuid? then we add one if (!$mayberec->{uuid}) { - $mayberec->{uuid} = getUUID($node); + $mayberec->{uuid} = NMIS::UUID::getUUID($node); } # ok, looks good enough. save the node info. diff --git a/admin/nodes_auto_admin.pl b/admin/nodes_auto_admin.pl index ea09c3c..163f84e 100755 --- a/admin/nodes_auto_admin.pl +++ b/admin/nodes_auto_admin.pl @@ -170,7 +170,7 @@ sub processNodes { } - if ( $NI->{system}{nodedown} eq "true" and $NI->{system}{lastUpdateSec} eq "" ) { + if ( $NI->{system}{nodedown} eq "true" and $NI->{system}{last_poll} eq "" ) { # run an update. #print "$node has never been polled\n"; push(@badNodes,"$node,$LNT->{$node}{host}"); diff --git a/admin/outage_admin.pl b/admin/outage_admin.pl new file mode 100755 index 0000000..1ae163c --- /dev/null +++ b/admin/outage_admin.pl @@ -0,0 +1,436 @@ +#!/usr/bin/perl +# +# Copyright 1999-2017 Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# a command-line outage administration tool for NMIS +our $VERSION = "1.0.0"; + +if (@ARGV == 1 && $ARGV[0] eq "--version") +{ + print "version=$VERSION\n"; + exit 0; +} + +use strict; +use FindBin; +use lib "$FindBin::RealBin/../lib"; + +use File::Basename; +use File::Spec; +use Data::Dumper; +use JSON::XS; + +use func; +use NMIS; + +my $bn = basename($0); +my $usage = "Usage: $bn act=[action to take] [extras...] + +\t$bn act=list [filter=X...] +\t$bn act=create [outage.A=B... outage.X.Y=Z...] +\t$bn act=update id= [outage.A=B... outage.X.Y=Z...] +\t$bn act={delete|show} id= +\t$bn act=check [node=X] [time=T] + +list: shows overview of defined outage schedules +show: displays the details for an outage + +create: creates new outage schedule + for detailed help, run $0 act=create +update: updates existing outage schedule + only the given outage.A, outage.X.Y properties are changed. + +check: reports which outages would apply at the + given time (or now) and for one node (if given) or all nodes +\n\n"; + +my $create_help = qq|Supported Arguments for Outage Creation: + +outage.description: free-form textual description. +outage.change_id: change management ticket identifier, used for event tagging + +outage.frequency: one of 'once', 'daily', 'weekly' or 'monthly' +outage.start, outage.end: date and time of outage start and end, + format depends on frequency + daily: "HH:MM" or "HH:MM:SS". 24:00 is allowed for end. + weekly: "MDAY HH:MM" or "MDAY HH:MM:SS", MDAY one of 'Mon', 'Tue' etc. + monthly: "D HH:MM:SS", "-D HH:MM:SS", "D HH:MM", "-D HH:MM" + D is the numeric day of the month, 1..31. -D counts from the end of the month, + -1 is the last day of the month, -2 the second to last etc. + once: ISO8601 date time recommended, + e.g. 2017-10-31T03:04:26+0000 + +outage.options: optional key=values to adjust NMIS' behaviour during an outage +outage.selector: any number of criteria for selecting devices for this outage + selector keys: node.X or config.Y, node config or global config properties + selector values: single string, /regex string/ or array or single strings. + arrays must be given as separate indexed entries. + all selectors must match for a node to be subject to the outage. + +example: $0 act=create \\ +outage.description='certain nodes are busy each month start' \\ +outage.change_id='ticket #42' \\ +outage.frequency=monthly outage.start="1 12:00" outage.end="1 13:30" \\ +outage.selector.node.group.0="busybodies" \\ +outage.selector.node.group.1="alsobad"\n\n|; + +die $usage if (!@ARGV or ( @ARGV == 1 and $ARGV[0] =~ /^--?[h?]/)); +my %args = getArguements(@ARGV); + +my $debuglevel = setDebug($args{debug}); +my $infolevel = setDebug($args{info}); +my $confname = $args{conf} || "Config"; + +my $wantquiet = getbool($args{quiet}); + +my ($thislogin) = getpwuid($<); # only first field is of interest + +# get us a common config first +my $config = loadConfTable(conf=>$confname, + dir=>"$FindBin::RealBin/../conf", + debug => $debuglevel); +die "could not load configuration $confname!\n" + if (!$config or !keys %$config); + + +# overview of all outages; no selector, no options +if ($args{act} eq "list") +{ + my %filter; + for my $maybe (keys %args) + { + next if ($maybe !~ /^(id|description|change_id|frequency|start|end|options\.nostats|selector.(config|node).[^=]+)$/); + + if ($args{$maybe} =~ m!^/(.*)/(i)?$!) + { + my ($re,$options) = ($1,$2); + $filter{$maybe} = ($options? qr{$re}i : qr{$re}); + } + else + { + $filter{$maybe} = $args{$maybe}; + } + } + + my $res = NMIS::find_outages(filter => \%filter); + die "failed to find outages: $res->{error}\n" if (!$res->{success}); + + if (!@{$res->{outages}}) + { + print STDERR "No matching outages.\n" if (!$wantquiet); + exit 0; + } + + # uuids are 36c wide, align only if output is to tty + my $fmt = (-t \*STDOUT? "%36s\t%16s\t%30s\t\%10s\t%20s\t%20s\n" : "%s\t%s\t%s\t%s\t%s\t%s\n"); + # header only if tty + printf($fmt, "ID", "Change ID", "Description", + "Frequency", "Start", "End") if (-t \*STDOUT); + + for my $orec (@{$res->{outages}}) + { + printf($fmt, + $orec->{id}, + $orec->{change_id}, + $orec->{description}, + $orec->{frequency}, + ($orec->{frequency} eq "once" && $orec->{start} =~ /^\d+(\.\d+)?$/? + POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime($orec->{start})) : $orec->{start}), + ($orec->{frequency} eq "once" && $orec->{end} =~ /^\d+(\.\d+)?$/? + POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime($orec->{end})) : $orec->{end}) + , ); + + } +} +# remove one outage by id +elsif ($args{act} eq "delete") +{ + my $outid = $args{"id"}; + die "Cannot delete outage without id argument!\n\n$usage\n" if (!$outid); + + my $res = NMIS::remove_outage(id => $outid, meta => { user => $thislogin }); + die "failed to remove outage: $res->{error}\n" if (!$res->{success}); +} +# show one outage structure in flattened form +elsif ($args{act} eq "show") +{ + my $outid = $args{"id"}; + die "Cannot show outage without id argument!\n\n$usage\n" if (!$outid); + + my $res = NMIS::find_outages(filter => { id => $outid }); + die "Failed to lookup outage: $res->{error}" if (!$res->{success}); + # there can be at most one with this id + my $theoneandonly = $res->{outages}->[0]; + die "No outage with id $outid exists!\n" if (!$theoneandonly); + + my %flatearth = flatten($theoneandonly); + for my $k (sort keys %flatearth) + { + my $val = $flatearth{$k}; + print "$k=$flatearth{$k}\n"; + } + exit 0; +} +elsif ($args{act} eq "update") +{ + # update: id required + my $outid = $args{id}; + + die "Cannot update outage without id argument!\n\n$usage\n" + if (!$outid); + + # look it up, amend with given values + my $res = NMIS::find_outages(filter => { id => $outid }); + die "Failed to lookup outage: $res->{error}" if (!$res->{success}); + # there can be at most one with this id + my $updateme = $res->{outages}->[0]; + die "No outage with id $outid exists!\n" if (!$updateme); + + my $dosomething; + for my $name (grep(/^outage\./, keys %args)) + { + my $dotted = $name; $dotted =~ s/^outage\.//; + $updateme->{$dotted} = (defined($args{$name}) && $args{$name} ne "")? + $args{$name} : undef; + ++$dosomething; + + my $error = translate_dotfields($updateme); + die "translation of arguments failed: $error\n" if ($error); + } + die "No changes for outage \"$outid\"!\n" if (!$dosomething); + + $updateme->{id} = $outid; # bsts... + $res = NMIS::update_outage(%$updateme, meta => { user => $thislogin }); + die "Failed to update \"$outid\": $res->{error}\n" if (!$res->{success}); +} +elsif ($args{act} eq "create") +{ + # create w/o args? show help + die $create_help if (!grep(/^outage\./, keys %args)); + + my ($addables,%createme); + for my $name (grep(/^outage\./, keys %args)) + { + my $dotted = $name; $dotted =~ s/^outage\.//; + $createme{$dotted} = (defined($args{$name}) && $args{$name} ne "")? + $args{$name} : undef; + ++$addables; + + my $error = translate_dotfields(\%createme); + die "translation of arguments failed: $error\n" if ($error); + } + die "No arguments for creating an outage!\n" if (!$addables); + # make sure the user doesn't pass a clashing id arg! + $createme{id} //= $args{id} if (defined $args{id}); + if ($createme{id}) + { + my $clash = NMIS::find_outages(filter => { id => $createme{id} }); + die "Failed to lookup outage: $clash->{error}" if (!$clash->{success}); + die "Cannot create outage with id \"$createme{id}\": already exists!\n" + if (@{$clash->{outages}}); + } + + my $res = NMIS::update_outage(%createme, meta => { user => $thislogin }); + die "Failed to create: $res->{error}\n" if (!$res->{success}); + + # print the created id if not quiet, and without fluff if not tty + print((-t \*STDOUT? "Created outage \"$res->{id}\"\n" : $res->{id}."\n")) + if (!$wantquiet); +} +elsif ($args{act} eq "check") +{ + my $node = $args{node}; # optional + my $uuid = $args{uuid}; # optional + my $when = $args{time} || time; + if ($when !~ /^\d+(\.\d+)?$/) + { + $when = func::parseDateTime($when) || func::getUnixTime($when); + } + + my $res = NMIS::check_outages( node => $node, uuid => $uuid, time => $when); + die "Failed to check outages: $res->{error}\n" if (!$res->{success}); + + if ($uuid && !$node) + { + my $LNT = loadLocalNodeTable; + $node = (grep($_->{uuid} eq $uuid, values %$LNT))[0]->{name}; + } + + print "\nRelevant outages" + .($node? " for node $node, ":"") + ." at time " + .localtime($when).":\n"; + + for (["Past:", "past"], ["Future:", "future"], ["Current: ", "current" ]) + { + my ($tag, $source) = @$_; + + if (!@{$res->{$source}}) + { + print "$tag None\n"; + } + else + { + my @output; + for my $match (@{$res->{$source}}) + { + my $msg = "\n\t\"$match->{description}\" ($match->{id})\n\t$match->{frequency} from '" + . ($match->{start} =~ /^\d+(\.\d+)?$/? scalar(localtime($match->{start})) : $match->{start}) + ."' to '" + . ($match->{end} =~ /^\d+(\.\d+)?$/? scalar(localtime($match->{end})) : $match->{end})."'"; + $msg .= "\n\t(actual '".localtime($match->{actual_start}). "' to '".localtime($match->{actual_end})."')" + if ($match->{actual_start}); + push @output, $msg; + } + print "$tag ".join("\n\n", @output)."\n"; + } + } + print "\n"; + exit 0; +} +else +{ + # fallback: complain about the arguments + die "Could not parse arguments!\n\n$usage\n"; +} + +exit 0; + +# translates EXISTING deep structure into key1.key2.key3 constructs, +# also supports key1.N.key2.M but toplevel thing must be hash. +# args: deep hash ref, prefix +# returns: flat hash +sub flatten +{ + my ($deep, $prefix) = @_; + my %flattened; + + if ($prefix ne "") + { + $prefix .= "."; + } + else + { + $prefix='outage.'; + } + + if (ref($deep) eq "HASH") + { + for my $k (keys %$deep) + { + if (ref($deep->{$k})) + { + %flattened = (%flattened, flatten($deep->{$k}, "$prefix$k")); + } + else + { + $flattened{"$prefix$k"} = $deep->{$k}; + } + } + } + elsif (ref($deep) eq "ARRAY") + { + for my $idx (0..$#$deep) + { + if (ref($deep->[$idx])) + { + %flattened = (%flattened, flatten($deep->[$idx], "$prefix$idx")); + } + else + { + $flattened{"$prefix$idx"} = $deep->[$idx]; + } + } + } + else + { + die "invalid inputs to flatten: ".Dumper($deep)."\n"; + } + return %flattened; +} + +# this function translates a toplevel hash with fields in dot-notation +# into a deep structure. this is primarily needed in deep data objects +# handled by the crudcontroller but not necessarily just there. +# +# notations supported: fieldname.number for array, +# fieldname.subfield for hash and nested combos thereof +# +# args: resource record ref to fix up, which will be changed inplace! +# returns: undef if ok, error message if problems were encountered +sub translate_dotfields +{ + my ($resource) = @_; + return "toplevel structure must be hash, not ".ref($resource) if (ref($resource) ne "HASH"); + + # we support hashkey1.hashkey2.hashkey3, and hashkey1.NN.hashkey2.MM + for my $dotkey (grep(/\./, keys %{$resource})) + { + my $target = $resource; + my @indir = split(/\./, $dotkey); + for my $idx (0..$#indir) # span the intermediate structure + { + my $thisstep = $indir[$idx]; + # numeric? make array, textual? make hash + if ($thisstep =~ /^\d+$/) + { + # check that structure is ok. + return "data conflict with $dotkey at step $idx: need array but found ".(ref($target) || "leaf value") + if (ref($target) ne "ARRAY"); + # last one? park value + if ($idx == $#indir) + { + $target->[$thisstep] = $resource->{$dotkey}; + } + else + { + # check what the next one is and prime the obj + $target = $target->[$thisstep] ||= ($indir[$idx+1] =~ /^\d+$/? []: {} ); + } + } + else # hash + { + # check that structure is ok. + return "data conflict with $dotkey at step $idx: need hash but found ". (ref($target) || "leaf value") + if (ref($target) ne "HASH"); + # last one? park value + if ($idx == $#indir) + { + $target->{$thisstep} = $resource->{$dotkey}; + } + else + { + # check what the next one is and prime the obj + $target = $target->{$thisstep} ||= ($indir[$idx+1] =~ /^\d+$/? []: {} ); + } + } + } + delete $resource->{$dotkey}; + } + return undef; +} diff --git a/admin/rrd_resize.pl b/admin/rrd_resize.pl index 6653498..826919d 100755 --- a/admin/rrd_resize.pl +++ b/admin/rrd_resize.pl @@ -165,7 +165,8 @@ sub processRRDFile { dbg("$rrd backup'ed up to $rrd.bak"); } else { - print "SKIPPING: $rrd could not be backup'ed\n"; + print "SKIPPING: \n"; + die "FATAL: $rrd could not be backup'ed. Stopping now!\n"; } } else { diff --git a/admin/service_graph_helper.pl b/admin/service_graph_helper.pl index fce95bc..3c8f56c 100755 --- a/admin/service_graph_helper.pl +++ b/admin/service_graph_helper.pl @@ -51,6 +51,7 @@ use JSON::XS; use Data::Dumper; use UI::Dialog; +use version 0.77; use func; use NMIS; @@ -87,11 +88,11 @@ die "User cancelled operation.\n" if ($dia->state ne "OK"); -my %allsvc = loadServiceStatus; +my %allsvc = loadServiceStatus(); + # only interested in this server's services! %allsvc = %{$allsvc{$config->{server_name}}} if (ref($allsvc{$config->{server_name}}) eq "HASH"); - my $servicesel = $dia->menu( text => "Please select the service you want to graph:", list => [ map { ($_,'') } (sort keys %allsvc) ] ); @@ -260,11 +261,11 @@ $color = sprintf("%02x%02x%02x", int(rand(256)), int(rand(256)), int(rand(256))) if ($color eq "random"); - - my $linedef = "LINE1:$ds#$color:$label"; - push @{$graph{option}->{standard}}, $linedef; - push @{$graph{option}->{small}}, $linedef; } + + my $linedef = "LINE1:$ds#$color:$label"; + push @{$graph{option}->{standard}}, $linedef; + push @{$graph{option}->{small}}, $linedef; } # now deal with the printing choices @@ -355,11 +356,15 @@ exit 0; -# ui::dialog doesn't escape current values :-( +# versions of ui::dialog before 1.13 don't escape their inputs properly +# https://rt.cpan.org/Public/Bug/Display.html?id=107364 sub escape { my ($input) = @_; - $input =~ s/\$/\\\$/g; + if (version->parse($UI::Dialog::VERSION) < version->parse("1.13")) + { + $input =~ s/\$/\\\$/g; + } return $input; } diff --git a/admin/show_model_structure.pl b/admin/show_model_structure.pl new file mode 100755 index 0000000..7f725d3 --- /dev/null +++ b/admin/show_model_structure.pl @@ -0,0 +1,80 @@ +#!/usr/bin/perl +# small helper for analysing/exporting model structures: +# shows flattened structure and values. +# works off expanded model_cache json files, +# or off UNEXPANDED models/models-install files (i.e. common sections are +# not filled in!) +use strict; + +use FindBin; +use lib "$FindBin::RealBin/../lib"; +use Data::Dumper; +use func; + +my $mf = $ARGV[0]; +die "usage: $0 \n" + if (!$mf or !-f $mf); + +my $deepdata = readFiletoHash(file => $mf, json => ($mf =~ /\.json$/i)); + +my %flatearth = flatten($deepdata); +for my $k (sort keys %flatearth) +{ + print "$k = $flatearth{$k}\n"; +} +exit 0; + +# translates EXISTING deep structure into key1/key2/key3 constructs, +# also supports key1/N/key2/M but toplevel thing must be hash. +# args: deep hash ref +# returns: flat hash +sub flatten +{ + my ($deep, $prefix) = @_; + my %flattened; + + if ($prefix) + { + $prefix .= "/"; + } + else + { + $prefix='/'; + } + + if (ref($deep) eq "HASH") + { + for my $k (keys %$deep) + { + my $visk = $k; + $visk =~ s!/!\\/!g; # escape any '/' that key might contain + if (ref($deep->{$k})) + { + %flattened = (%flattened, flatten($deep->{$k}, "$prefix$visk")); + } + else + { + $flattened{"$prefix$visk"} = $deep->{$k}; + } + } + } + elsif (ref($deep) eq "ARRAY") + { + for my $idx (0..$#$deep) + { + if (ref($deep->[$idx])) + { + %flattened = (%flattened, flatten($deep->[$idx], "$prefix$idx")); + } + else + { + $flattened{"$prefix$idx"} = $deep->[$idx]; + } + } + } + else + { + die "invalid inputs to flatten: ".Dumper($deep)."\n"; + } + return %flattened; +} diff --git a/admin/support.pl b/admin/support.pl index f350f05..ed432ba 100755 --- a/admin/support.pl +++ b/admin/support.pl @@ -27,7 +27,7 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -our $VERSION = "1.6.0"; +our $VERSION = "1.6.2"; use strict; use Data::Dumper; use File::Basename; @@ -341,6 +341,8 @@ sub collect_evidence system("iostat -kx 1 5 > $targetdir/system_status/iostat"); } + system("date > $targetdir/system_status/date"); + # copy /etc/hosts, /etc/resolv.conf, interface and route status system("cp","/etc/hosts","/etc/resolv.conf","/etc/nsswitch.conf","$targetdir/system_status/") == 0 or warn "can't save dns configuration files: $!\n"; @@ -379,7 +381,7 @@ sub collect_evidence # collect all defined log files mkdir("$targetdir/logs"); - my @logfiles = (map { $globalconf->{$_} } (grep(/_log$/, keys %$globalconf))); + my @logfiles = grep(/^.+$/, (map { $globalconf->{$_} } (grep(/_log$/, keys %$globalconf)))); if (!@logfiles) # if the nmis load failed, fall back to the most essential standard logs { @logfiles = map { "$globalconf->{''}/$_" } @@ -450,6 +452,7 @@ sub collect_evidence for my $oksubdir (qw(scripts nodeconf)) { + next if (! -d "$basedir/conf/$oksubdir"); # those dirs may or may not exist system("cp $basedir/conf/$oksubdir/* $targetdir/conf/$oksubdir") == 0 or warn "can't copy conf to $targetdir/conf/$oksubdir: $!\n"; } diff --git a/admin/update_config_defaults.pl b/admin/update_config_defaults.pl index 598dbf0..85de556 100755 --- a/admin/update_config_defaults.pl +++ b/admin/update_config_defaults.pl @@ -29,19 +29,19 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** +use strict; +our $VERSION = "8.6.2G"; # Auto configure to the /lib use FindBin; use lib "$FindBin::Bin/../lib"; -# -use strict; use func; my $confFile = "/usr/local/nmis8/conf/Config.nmis"; print < $ARGV[0], backup => "$ARGV[0].backup"); +# patch in outages as the after status +my @curfields = split(/,/, $conf->{system}->{network_viewNode_field_list}); +if (!grep($_ eq "outage", @curfields)) +{ + $conf->{system}->{network_viewNode_field_list} = join("," , + $curfields[0], + "outage", + @curfields[1..$#curfields]); +} + $conf->{"system"}->{"nmis_executable"} = '(/(bin|admin|install/scripts|conf/scripts)/[a-zA-Z0-9_\\.-]+|\\.pl|\\.sh)$'; $conf->{'authentication'}{'auth_user_name_regex'} = "[\\w \\-\\.\\@\\`\\']+"; @@ -83,6 +93,8 @@ $conf->{'system'}{'threshold_period-pkts_hc'} = "-5 minutes"; $conf->{'system'}{'threshold_period-interface'} = "-5 minutes"; + + $conf->{'system'}{'log_node_configuration_events'} = "false"; $conf->{'system'}{'os_execperm'} = "0770"; diff --git a/admin/updateconfig.pl b/admin/updateconfig.pl index eebd0c0..67df798 100755 --- a/admin/updateconfig.pl +++ b/admin/updateconfig.pl @@ -1,7 +1,5 @@ #!/usr/bin/perl # -## $Id: updateconfig.pl,v 1.6 2012/08/27 21:59:11 keiths Exp $ -# # Copyright (C) Opmantek Limited (www.opmantek.com) # # ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM @@ -29,7 +27,7 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -our $VERSION="1.1.0"; +our $VERSION="2.0.0"; use strict; use File::Basename; @@ -38,7 +36,7 @@ use func; -my ($template, $live) = @ARGV; +my ($template, $live, $wantdebug) = @ARGV; if (!$template or !-f $template or !$live or !-f $live) { my $me = basename($0); @@ -58,42 +56,59 @@ die "Invalid template config!\n" if (ref($templateconf) ne "HASH" or !keys %$templateconf); - die "Invalid live config!\n" if (ref($liveconf) ne "HASH" or !keys %$liveconf); -my @added; - -# attention: this covers ONLY TWO LEVELS of indirection! -for my $section (sort keys %$templateconf) -{ - for my $item (sort keys %{$templateconf->{$section}}) - { - next if (exists $liveconf->{$section}->{$item}); # undef is fine, only interested in MISSING - print "Updating missing $section/$item\n"; - - $liveconf->{$section}->{$item} = $templateconf->{$section}->{$item}; - push @added, [ $section, $item]; - } -} -if (@added) +my $havechanges; +updateConfig($templateconf, $liveconf, "", 1, \$havechanges); +if ($havechanges) { writeHashtoFile(file=>$live, data=>$liveconf); - - print "\nItems added to Live Config:\n"; - for (@added) - { - my ($section, $item) = @$_; - my $value = $liveconf->{$section}->{$item}; - - print " $section/$item=". (defined($value)? - $value =~ /\s+/ || $value eq ""? "'$value'": - $value : "undef"). "\n"; - } } else { - print "Found no items to add to Live Config.\n"; + print "No missing configuration items were detected.\n"; } exit 0; +# recursively fill in _missing_ things from install into live +# args: install, live - hash ref, loc (textual), further recursion allowed yes/no +# returns: nothing +sub updateConfig +{ + my ($install, $live, $loc, $recurseok, $accum) = @_; + + die "invalid install structure: ".ref($install)."\n" + if (ref($install) ne "HASH"); + die "invalid live structure: ".ref($live)."\n" + if (ref($live) ne "HASH"); + die "cannot merge live ".ref($live) + ." and install ".ref($install) + .", structure mismatch\n" if (ref($live) ne ref($install)); + + for my $item (sort keys %$install) + { + if (exists($live->{$item})) + { + if (ref($install->{$item}) eq "HASH" + && ref($live->{$item}) eq "HASH" + && $recurseok) + { + print "recursing deeper into ${loc}/$item\n" if ($wantdebug); + updateConfig($install->{$item}, $live->{$item}, "${loc}/$item", $recurseok, $accum); + } + else + { + print "NOT recursing into ${loc}/$item\n" if (ref($install->{$item}) && $wantdebug); + } + } + else + { + print "Adding ${loc}/$item = ".(ref($install->{$item})? "": $install->{$item})."\n"; + $live->{$item} = $install->{$item}; + ++$$accum; + } + } + return; +} + diff --git a/admin/upgrade_models.pl b/admin/upgrade_models.pl index 0518bb3..f78e233 100755 --- a/admin/upgrade_models.pl +++ b/admin/upgrade_models.pl @@ -29,7 +29,7 @@ # ***************************************************************************** # # this helper upgrades model files where safe to do so -our $VERSION="8.6.1G"; +our $VERSION="8.6.2a"; use strict; use Digest::MD5; # good enough @@ -163,38 +163,42 @@ sub compute_signature # model file, signatures for the last few releases are stored here __DATA__ +Common-ADSL.nmis 8c5779cf5faaf45a Common-Cisco-asset.nmis 675e126af3677a52 Common-Cisco-cbqos.nmis e270054af44bc308 Common-Cisco-cpu.nmis d0570e92ed3e4985 Common-Cisco-macTable.nmis e503d0cbd7220f8f -Common-Cisco-memory.nmis f9af857a75788ae4 +Common-Cisco-memory.nmis 29c177a6745e98da f9af857a75788ae4 Common-Cisco-neighbor.nmis 680e05322f63c24f Common-Cisco-netflow.nmis 33ad2b9786e1e4f4 -Common-Cisco-rtt.nmis 7cd3a757c422f5bd -Common-Cisco-status.nmis 5075457bccea2102 c08e756f6ccd2fa8 +Common-Cisco-rtt.nmis 4df49293858fac3d 7cd3a757c422f5bd +Common-Cisco-status.nmis f0c8c45368792ae1 c08e756f6ccd2fa8 5075457bccea2102 +Common-Cisco-temp.nmis 0eea6a6191e02701 83bd6bac56c7b7fb Common-Cisco-vlan.nmis 2e41983677ada8b7 Common-Huawei-cbqos.nmis 58b9266692e45cae +Common-Juniper-jnxCoS.nmis f2520160747fd14f Common-Juniper-jnxOperations.nmis 265a1c2ef344630f -Common-Windows-alerts.nmis 56028b9a1767f70a 49d9dd2900082d39 +Common-Windows-alerts.nmis d76034004173d122 56028b9a1767f70a 49d9dd2900082d39 Common-Windows-interface.nmis 8c29f6ab41dcb2ff Common-Windows-system.nmis 57809a9258f320a4 aded8512fdc1a137 -Common-Windows-wmi.nmis 75a6d5002bb1e851 eae47688035b325d +Common-Windows-wmi.nmis dbc5c412d3fa5cb5 eae47688035b325d 75a6d5002bb1e851 Common-asset.nmis f6c2fe2777c14437 Common-calls.nmis 77ca79216fd1aefa Common-cbqos-in.nmis 68c58453714e91dc a0bbc467ffd18646 6c8df3ec0d0c0858 Common-cbqos-out.nmis ab31ff8eee5db591 48665448110af552 1210e379c4b6a92d -Common-database.nmis b32bf11f5b860b3e b3d083221e94f22c 8b309566ec783d52 6514b77ecc0dd2c8 5bacf781a1495ba5 a934b015029bbe63 a41276db67e9be61 92a7f5ebc9af25aa 5f1f1a8792f0498d b70fbcbf9e210596 1b1d1620e6b66683 5dbac9d4f0590b2c 2227e7b78b5547b4 e56cad8dc7066418 4bd336af049d4135 0cf0a876087bd7f2 411a0d92a51a96a3 +Common-database.nmis f5fde11eb5101a3f b3d083221e94f22c 8b309566ec783d52 6514b77ecc0dd2c8 5bacf781a1495ba5 a934b015029bbe63 a41276db67e9be61 92a7f5ebc9af25aa 5f1f1a8792f0498d b70fbcbf9e210596 1b1d1620e6b66683 5dbac9d4f0590b2c 2227e7b78b5547b4 e56cad8dc7066418 4bd336af049d4135 0cf0a876087bd7f2 b32bf11f5b860b3e 411a0d92a51a96a3 b2ff737dc0c025c1 Common-entityMib.nmis 23129c70d072e098 Common-event.nmis 3c17ac2753efd729 -Common-heading.nmis 56bbc1eafea2ffbb dc1e5ee59839f42a 6b6ffeeb92a8996f fc120bc0906f3b70 7e458f0172e120ea 0f6d824494640deb 8e453ea283e3fd7a c1c8d886c4c7f7f1 4be5135841352538 a2c592a8fe826493 f652ed992cf8d4a2 087587a01227a79e 1c1c2e7cf3a0606c 5c37adf22c7f4ab4 c2bd5a05f75efac3 +Common-heading.nmis 03e97ba31018fe04 dc1e5ee59839f42a 6b6ffeeb92a8996f fc120bc0906f3b70 7e458f0172e120ea 0f6d824494640deb 8e453ea283e3fd7a c1c8d886c4c7f7f1 4be5135841352538 a2c592a8fe826493 f652ed992cf8d4a2 087587a01227a79e 1c1c2e7cf3a0606c 5c37adf22c7f4ab4 c2bd5a05f75efac3 56bbc1eafea2ffbb e6e2c1a1df569f27 +Common-ifStack.nmis 273aabe064bfb46b Common-lldp.nmis e2d224fefdae20fb Common-macTable.nmis 361d95305604254f Common-mpls.nmis c05bc0c2b2f47123 -Common-routing.nmis bef2fb4c73d5fec9 329d8897cefd7011 +Common-routing.nmis 1c9b0570350148f4 329d8897cefd7011 bef2fb4c73d5fec9 Common-software.nmis b8a70318d469754b Common-stats.nmis 2f7157c230386ee2 efbcfd8340518376 14dd2080e99197df 051ad0c9af4e10ba 67a57e6c34135bc1 6094dfc29937dd19 17ac95f3a6a726cb Common-summary.nmis 10d878a1904ebb31 -Common-threshold.nmis a20ec1fe3d77e0a8 2085498abd902193 5f00df141ba53a85 709aa976ce2acd85 42a0e451c9206d1a c787902cfc0496e9 306e9d25639e3af6 43500a40644ea0e6 +Common-threshold.nmis d72ac1154c8b2264 2085498abd902193 5f00df141ba53a85 709aa976ce2acd85 42a0e451c9206d1a c787902cfc0496e9 306e9d25639e3af6 43500a40644ea0e6 a20ec1fe3d77e0a8 Graph-APCBattTemp.nmis 236bfec034b1269c de5f3206f3eeae68 Graph-APCCapacity.nmis 67294b482fbb1e1f a243dadb1d7875dd Graph-APCCurrent.nmis 0c04f8bd2eaaeccd 907121ea0bcf9be0 @@ -217,6 +221,7 @@ sub compute_signature Graph-EltekTempAlarms.nmis 3aebc4fa0e6abe2d Graph-EltekVoltageAlarms.nmis e9e61c6ec0eb5f5a Graph-GPSSats.nmis 16bf012753e5e027 833fc39873ef07b0 7c1500a755e89524 +Graph-InterfaceStatus.nmis 8c440295d1bac8b9 Graph-LinkRate.nmis b5c44a69bd96b53c Graph-LinkRateAp.nmis b5c44a69bd96b53c de25b80252ba37fd Graph-LinkRateStat.nmis b5c44a69bd96b53c de25b80252ba37fd @@ -248,7 +253,10 @@ sub compute_signature Graph-a3bandwidth.nmis e4fe343b326bd85f Graph-a3errors.nmis 6ded8760642b828f Graph-a3traffic.nmis 4cad6c4885476f48 -Graph-abits.nmis 21cb28543ecd2bb2 4591e25ddd6dbfe5 34802a5b374e8180 +Graph-abits-oneway-dcu.nmis 24324b56ff760ca4 +Graph-abits-oneway-jnxCoS.nmis 24324b56ff760ca4 +Graph-abits-oneway-scu.nmis 24324b56ff760ca4 +Graph-abits.nmis a623a93d3b420cef 4591e25ddd6dbfe5 34802a5b374e8180 21cb28543ecd2bb2 Graph-acpu.nmis 38f5bdc3d1da6e4e Graph-alcoma-linkrate.nmis fb28a70ea6683cc9 Graph-alcoma-power.nmis ce99c62355e8ca92 @@ -260,6 +268,11 @@ sub compute_signature Graph-bgpPeer.nmis 1011685b62b52bcf b9c1298f7adc649a Graph-bgpPeerStats.nmis c8832752bd860517 a38db8b5ce41f4c4 Graph-bits.nmis 0d948fe34ab19cf8 +Graph-bti-fc-optical.nmis 62069c43c1245513 +Graph-bti-ge-bytes.nmis 7674a9843b0bfb17 +Graph-bti-ge-optical.nmis 62069c43c1245513 +Graph-bti-ops-status.nmis 7f56926adc9f926c +Graph-bti-stm-optical.nmis 62069c43c1245513 Graph-buffer.nmis 8a03625101fad4d6 5ab5b989c00298f3 Graph-calls.nmis a9a927145cf81af5 Graph-cbqos-in.nmis f6d6fcef870cfa3a @@ -296,7 +309,7 @@ sub compute_signature Graph-gsm_status_2g.nmis 8b0fef6603cf7b49 Graph-gsm_status_3g.nmis 33b988d5abbe6988 Graph-health-ping.nmis 01dcf864dbb5093a f9cec5794bb6a4da 6b96feaf72a8ab85 -Graph-health.nmis 2d7292e8ec0c4f33 732400abda37f46d +Graph-health.nmis 72b2b2003ddbe828 732400abda37f46d 2d7292e8ec0c4f33 Graph-hrbufmem.nmis 18ecbd05ab9219ee Graph-hrcachemem.nmis 849c721e0976297f Graph-hrcpu.nmis d133563fac3acb74 @@ -324,12 +337,13 @@ sub compute_signature Graph-inDropPackets.nmis 6bdd095534e04e38 Graph-ip.nmis c6baba48b23bda7f 74a54c12bbb5bac0 89b1939ad69db272 Graph-jnxCPU.nmis ff556b1c9129bbb4 +Graph-jnxCoS-oneway.nmis b691aabdaaeba290 Graph-jnxMem.nmis ab1a213c7c3e95bc Graph-jnxTemp.nmis 9a63dfa867000f76 Graph-kpi.nmis 35a06b89a805d58a Graph-laload.nmis 497c9f570b333d58 52bd4c924a95ae13 Graph-lockstat.nmis 90b0b76602f5def8 91da9e50c54b4c0b -Graph-maxbits.nmis 3487a645887135af +Graph-maxbits.nmis f6a62685c2a14f25 3487a645887135af Graph-mem-cluster.nmis eec46df71e79d7db Graph-mem-dram.nmis 9a53757525316170 Graph-mem-io.nmis be89eb2045aae54e 08471f91a7d0a104 @@ -343,6 +357,7 @@ sub compute_signature Graph-memoryBuffer.nmis ec200e3ebf7b7aec Graph-memoryPool.nmis b48eea15617c91e0 Graph-metrics.nmis f99bf0c709884dec 2cf6e680cb579dd2 +Graph-mikrotikCpu.nmis 77ecf780cf5a178d Graph-mimosaChain.nmis c95d0d47bce007b7 c93ba012db73275b Graph-mimosaStream.nmis 60d81663b57165a7 9471ea7235aede9e Graph-modem.nmis 829adca5c684f5c8 @@ -362,6 +377,10 @@ sub compute_signature Graph-ppxAtmCells.nmis c1a05e642f34cbaa Graph-ppxAtmUtil.nmis 004ba1365bde1cae Graph-ppxCardCPU.nmis 9a949a954cb16413 +Graph-ppxCardMEM.nmis cfd1a3d838b66574 +Graph-ppxCardMEMFast.nmis 6d224f366d34d31f +Graph-ppxCardMEMNormal.nmis fa617fd3b67b0f60 +Graph-ppxCardMEMShared.nmis a9b900be86b69059 Graph-psu-status.nmis 3b76482dee427b7b 646e7efbe6dcb363 Graph-pvc.nmis faea714bab5cc7ba Graph-rbt-mem-proc.nmis 3d69c9ea2fbc7a6f @@ -370,6 +389,7 @@ sub compute_signature Graph-rbt_optimisation.nmis ca61c4e432f4cf6d Graph-response.nmis 21f085a7823787f0 ea1267d3f5f23e70 Graph-routenumber.nmis e9e4d23bd813f52d +Graph-rttMonLatestRtt.nmis 7f521072002bf6b9 Graph-sensorhum.nmis 0558d8286f67f651 Graph-sensortemp.nmis 620a8f116920bb08 Graph-service-cpu.nmis 18b23d723391bada @@ -405,17 +425,20 @@ sub compute_signature Graph-util.nmis f54dd8075acd46a3 Graph-vmwVmState.nmis c3cd84bddf449c3f Model-ACME-Packet.nmis 1995da55a0ef6d1e c0e170755b7a60e4 -Model-AIX.nmis 778c98f5612308f9 dd42ee01eb159fa6 e308c1d1d3579fe4 8ca922e4d2831e81 +Model-AIX.nmis e325d86b84c490bf dd42ee01eb159fa6 e308c1d1d3579fe4 8ca922e4d2831e81 778c98f5612308f9 Model-AKCP-sensor.nmis c7598cb871049d36 3bd748115ad3a368 Model-APC-pdu-ap7900.nmis 05009c11f70f473a Model-APC-pdu-ap7932.nmis 27cacc3d874a1ef6 Model-APC-pdu.nmis f2c1bdb598e2ff3b Model-APC-ups.nmis e42db2965c704060 5c935b3a13c2daa6 Model-Accelar.nmis 7c09daabe4cd3f43 8063b383449c4fbd 473e4a629d6e7210 -Model-AlcatelASAM.nmis 50c1fdaa738fb401 83e3f4595c6719e6 acda353c60ca0a18 d2d5d9b2142e1989 +Model-AlcatelASAM.nmis 05d31dc406b41af5 83e3f4595c6719e6 acda353c60ca0a18 d2d5d9b2142e1989 50c1fdaa738fb401 Model-AlcatelASAMv2.nmis 60bd4fe058528bd8 Model-Alcoma.nmis 750123d82f0525c1 363404f6741b1ab6 7716542b9fa6ba59 Model-AristaSwitch.nmis 61521e62e243c3dc 56a548305c5cc437 d050d520cb81b95c +Model-BTI-7000.nmis a359908ab5bdaddc 54fa2081ba36ff46 +Model-BTI-7800.nmis 7335b6cb886f60b8 e3da5cfdd3cba22c +Model-BTI-OPS.nmis 1951ea24c83a6ab8 c8ce180bbb663559 Model-BayStack.nmis 5b9f81e347a8b5bc 20f3b44e91ca4cdc d555e9a6b526af07 Model-CGESM.nmis 742d7815c7d195c8 c072cde690149c1a Model-Catalyst4000.nmis 740e972af7c50276 8f91e536545afbb7 01ad784e33149cfd @@ -424,56 +447,58 @@ sub compute_signature Model-Catalyst6000.nmis e75673ed0cc5efa5 66c21f3797aeec56 bcb8bf00683e3618 Model-CatalystIOS.nmis ab4ec0aa503c45d6 58b4ca6c33caad09 c5c9cda6ab2fdc7c f57e45b916ce26a3 f1930865b4ca5157 4557e893e26d5ff2 5b8e80284af5b825 Model-CatalystIOSXE.nmis 996b59b22db57a9b -Model-Checkpoint.nmis 11ee0fa8b92eb80c 7fc1b9eacdb8db09 +Model-Checkpoint.nmis 5def04c2930552a3 7fc1b9eacdb8db09 11ee0fa8b92eb80c Model-Cisco10000.nmis f5cbfb02c29948f0 ecddd7f865832a10 -Model-Cisco7600.nmis 0d6618463bf1d98a 1a7e513d2fbadbbf 5789061a4af028ef ac742c2cbc7e5459 c240d49947ff9116 6f2ef6f864779f9e +Model-Cisco7600.nmis c385a498494f21ba 1a7e513d2fbadbbf 5789061a4af028ef ac742c2cbc7e5459 c240d49947ff9116 0d6618463bf1d98a 6f2ef6f864779f9e Model-CiscoAP.nmis 5ab2c2ebd0f905af dfa57f28dd05977b e293759250cee0b9 Model-CiscoASA.nmis 53aeca5f0d06e67b 24b86e0caf1d0bcf f6d6a0b5bf847d18 Model-CiscoASR.nmis 435239f4be2976c5 2896fd06036c36d5 6120ac508c4a3304 b32c1f0856744a39 fd8f1582493bab18 Model-CiscoATM.nmis 43b0f957952d745b 9a998b778a74bf97 c1761e63506775e5 Model-CiscoCSS.nmis 74e45b3e7419f40e 8c3f3db30cd6e486 -Model-CiscoDSL.nmis dabf2d96ae489d7f 374f511affd96c83 6ed6606e1f0ab42c 8e0d14738ab68def 1cac042659fc2e2c 1d66ceac998e50e7 58c7487e5aea2bc0 66a52da60962b0d4 +Model-CiscoDSL.nmis 084b887d98e64682 374f511affd96c83 6ed6606e1f0ab42c 8e0d14738ab68def 1cac042659fc2e2c 1d66ceac998e50e7 58c7487e5aea2bc0 66a52da60962b0d4 dabf2d96ae489d7f Model-CiscoDefault.nmis bceed295301338cf 4bc694b868f257b4 Model-CiscoGeneric.nmis 7d766076ec3446ae 9f159c3f25a627b9 f589324fb67f9793 Model-CiscoIOSXE.nmis fbe5563477ffdb80 72b549bdcc0804e9 0cb858418067430b 11f60115dd4cdfad e85f66570c90c7fd -Model-CiscoIOSXR.nmis 8a531fb238516665 e12b72b784ad1c5f 459049c58d8319dc 1754b6d20d61a74a c1d83637f22d1aea 71c999e29bc3b680 a1affb7cf1ae5495 3fb8ea566f34b6e4 ac6cc4e079004be4 -Model-CiscoNXOS.nmis eabfe0a428f6aa5e aeac57c06f2a8af6 5688a246aed01961 161ae6c49c6f1b3d a00d69038b17aa39 c80d009aa3d6cf1b 155625644907cfdc d80d5e84a5f55776 bc4df1a19cdd8e9b +Model-CiscoIOSXR.nmis 50feb5a84dc20a4d e12b72b784ad1c5f 459049c58d8319dc 1754b6d20d61a74a c1d83637f22d1aea 71c999e29bc3b680 a1affb7cf1ae5495 3fb8ea566f34b6e4 8a531fb238516665 ac6cc4e079004be4 +Model-CiscoNXOS.nmis 4faba51a761da835 aeac57c06f2a8af6 5688a246aed01961 161ae6c49c6f1b3d a00d69038b17aa39 c80d009aa3d6cf1b 155625644907cfdc d80d5e84a5f55776 bc4df1a19cdd8e9b eabfe0a428f6aa5e b1111378863a7d68 Model-CiscoPIX.nmis 475b6fe16f5f55d4 59f330a515797f53 Model-CiscoRouter.nmis f4cb68fb386f54c4 54e66e08f36229d9 83ff33224c7c63c0 1bd439a8e31cb653 0bc12598dda8c8a6 24ca17edafeae0b8 d552abe84ee95e51 06598a8a95e10b4e Model-CiscoVG.nmis bba37e84817dfed5 Model-Default-HC.nmis a8825ef25dbf4890 7d9ba9976f553b4b fbd901de4a88285a Model-Default.nmis f74f795c534ea46a 72f68603c0a271fe 4af750909edab9c8 Model-EES.nmis 2f96a9629bc08555 811546e34d172b7c -Model-ESXi.nmis ee3448c3a54c790b 15d304b1fdb12a21 a63a9f2762236032 42fe42c0ebac16f2 22e357c9c5511597 08bef15329da3766 92e38fd76606be7e 29e0d149f479f4ac +Model-ESXi.nmis d0171f0413b205e6 15d304b1fdb12a21 a63a9f2762236032 42fe42c0ebac16f2 22e357c9c5511597 08bef15329da3766 92e38fd76606be7e 29e0d149f479f4ac ee3448c3a54c790b Model-Eltek.nmis d9a12800c54d106a -Model-Ericsson-PPX.nmis cc9951f1266a07e4 a0b961cc776ffe20 +Model-Ericsson-PPX.nmis d9f4a4428f707d83 a0b961cc776ffe20 cc9951f1266a07e4 a56c2b7afd820b2b Model-ExtremeXOS.nmis 629d9792edb85b26 -Model-Fortinet-FG.nmis f00703b88a28683d +Model-Fortinet-FG.nmis 391acc38e62953fd f00703b88a28683d Model-FoundrySwitch.nmis ec76524338356ee6 bf168d70d1475fa3 421488c17e87d430 e6cf10185dce40b5 Model-FreeBSD.nmis d4f17f11cd2a5a01 4981e0655e48efe9 3a9abc00d41fbc74 Model-FrogFoot.nmis ae0e401e64cd6b45 dc6744887fbd2a49 4a6a66d4499724df 9a50575536718dd8 9cc289f58f1e3c3e bbe76f043957d1ba -Model-Furukawa-OLT.nmis 7489e8225813e7d9 4536d1b306296d78 261f3f8d53d8fbfc b14044e8a752afcd +Model-Furukawa-OLT.nmis 8f003cd197e09770 4536d1b306296d78 261f3f8d53d8fbfc b14044e8a752afcd 7489e8225813e7d9 Model-FutureSoftware.nmis 92096009cb54d5d0 cc2be4402dd800b9 -Model-GE-QS941.nmis 5cab506ad33ae94e 61b3564fc34b7534 +Model-GE-QS941.nmis 64cf727e20bfe0f5 61b3564fc34b7534 5cab506ad33ae94e Model-Generic.nmis 9f6e87c2dced82ce e2352bb53b48d526 6421c79212254318 -Model-Huawei-MA5600.nmis 9e752b89778e1542 +Model-HP-Procurve.nmis d640db73cf1a2489 +Model-Huawei-MA5600.nmis daa2a0723e5e7330 9e752b89778e1542 9e232402e57166e7 Model-Huawei-NetEngine.nmis 34ac1c5aaec02f3e Model-HuaweiRouter.nmis cbcb03fd241ba9e3 -Model-Juniper-ERX.nmis d66d891e14df1c12 -Model-JuniperRouter.nmis 099442285b2aa30b 95f973a102b99e2d be241f66adf340a6 acc55c2d6bcd8bcc aaff6a36a2c133d7 +Model-Juniper-ERX.nmis bad56f405ba7e1f0 d66d891e14df1c12 +Model-JuniperRouter.nmis 8b2db9e2629de479 95f973a102b99e2d be241f66adf340a6 acc55c2d6bcd8bcc aaff6a36a2c133d7 099442285b2aa30b 56c0fdd31f49ae1c Model-JuniperSwitch.nmis 893ae4f8de3c0773 c5614f8a914c8f5f 6d6aeba8055ea46f e2b6c09997041fc4 681bfbf3d57f235d -Model-LucentStinger.nmis 5dbd8bbf0b64ba76 04ad6e9cf916afe8 a3b1c1c39a15a9a0 cc1b7c35eb9e0a21 +Model-LucentStinger.nmis 2e349b7b7d1b6247 04ad6e9cf916afe8 a3b1c1c39a15a9a0 cc1b7c35eb9e0a21 5dbd8bbf0b64ba76 Model-MGE-ups.nmis ebaca909788ba8ee 78d62d126c66870a Model-MW-HP-GbE2c.nmis 50645e1358f2ff62 5e57f9290870e2d8 50c4f4f3d4e2c364 Model-MW-HP.nmis 32b3ed500d4c4a90 819a5ca08a073521 11c677ca4856af85 Model-MW-Intel.nmis 5cafb6dfa62191ce deb74f6313ead88c c52e3076b8ff51dc Model-MW-Juniper.nmis 0ab8ebdf3e23a6c8 61b9fba4cb792f82 664d25120b038e46 -Model-MikroTik.nmis c2ac6bd4b9045cd3 ff4009ac1488aab1 a353c76c562abc23 f8d9d7eb0925d15a 763ac63ebdf8f155 f5e69106f5b1cbc5 -Model-MikroTikRouter.nmis afa45e050d9e2ad3 +Model-MikroTik.nmis 8027b5c755f728e2 ff4009ac1488aab1 a353c76c562abc23 f8d9d7eb0925d15a 763ac63ebdf8f155 f5e69106f5b1cbc5 c2ac6bd4b9045cd3 +Model-MikroTikRouter.nmis 6e7f5c821b5f15c7 afa45e050d9e2ad3 Model-Mimosa.nmis 10672b91765dafdb 85bbe1cd540bdafd f96793bf3d691117 Model-Netgear-GS108T.nmis caa1f22f37fe1652 a97b7d693220f705 324868d459b73597 Model-Netgear-GS724T.nmis fc8bcdd017f3016a a4a0581c2c07b4bb 1454c0c9656baeb8 Model-Netgear-Manual.nmis e50a79fc13613fa0 5efe06d9638880e3 a2412f592876f41f +Model-Netscreen-ScreenOS.nmis 521190f2f6a929b6 Model-NovelSat.nmis 0c9d20c5c4c8345c Model-ONS15454.nmis 4831558a9668d56b 6546eb655567451f bb7ed270a661625a Model-OmniSwitch.nmis 95c870d2b7f0c439 858ae29f81cb4357 98c10c891ca47415 45ac9fe2f91bf84e @@ -489,17 +514,17 @@ sub compute_signature Model-SSII-3Com.nmis 76e2893b5d737bc8 bf19f17592baa368 3d382177424312fa Model-SciAtl.nmis 541d6b96a30fa040 28df9bfea43081bd Model-ServersCheck.nmis 8d041035d4a31ff3 2483e5817a5b9189 -Model-SunSolaris.nmis 41d3c15e0452b799 6f6df4401bb2c2cf 7d9f7259b89baaab f57fc30e03387722 +Model-SunSolaris.nmis 7476ff9ea55ab2c9 6f6df4401bb2c2cf 7d9f7259b89baaab f57fc30e03387722 41d3c15e0452b799 Model-Trango.nmis e7757060bc9e7a3e 6a64f79ce2e84472 693579019215606f f89d8e7ab685639f Model-Ubiquiti.nmis 8276b22cb044033f c9362b682401e7f9 -Model-Windows2000.nmis 57eefa482cc0428c f587ee08be43714d 008fd598141b9db8 d76cc6b3694e64ec 137091fec315d4cc e2a598507e943b77 -Model-Windows2003.nmis 3f42c421efed5077 a48090e9f95d56ab a9c8c6d00b50e39d 38ee3399177897b0 655cde0646c8fa9a fd6cb7524b7e1313 86e17d027c1f419e +Model-Windows2000.nmis 9cb6f3d553264a2b f587ee08be43714d 008fd598141b9db8 d76cc6b3694e64ec 137091fec315d4cc e2a598507e943b77 57eefa482cc0428c +Model-Windows2003.nmis 9fa95ec7acd0f00d a48090e9f95d56ab a9c8c6d00b50e39d 38ee3399177897b0 655cde0646c8fa9a fd6cb7524b7e1313 3f42c421efed5077 86e17d027c1f419e Model-Windows2008-wmi.nmis e4141731668b6f89 6ea61c9717c467c9 Model-Windows2008.nmis f67ca6859cf50312 19c6d7c29651edd0 8539686ff342df9f 2d25f7ec52d9fefd e8b3c48b420816f0 fd48d9d46ee0c0a9 9b8d5f0e921ff75a f4b15aa1cb629bd5 Model-Windows2008R2.nmis e319c992858b7773 Model-Windows2012.nmis bbf007ce847a32ae a800a44f0982e13f e7faddc8d0a6bf0a fd2be8102188edd3 8b9fa59350b6257b e3515295559253a6 Model-ZyXEL-GS.nmis 72f30722cd3e6665 b7eeb6155653a42b 8666c252de0c306b 15e94a542a7fa785 -Model-ZyXEL-IES.nmis 7b098aa3c8afa7de 593c69cbe0391cf3 0da840e65c219e00 3ab9322fece09797 4e5954eff8347757 +Model-ZyXEL-IES.nmis 25aaccabbd188208 593c69cbe0391cf3 0da840e65c219e00 3ab9322fece09797 4e5954eff8347757 7b098aa3c8afa7de Model-ZyXEL-MGS.nmis c3ea5aec5b903e8e bff9ef1e5d0a70d8 94ca1a1be8a5eeee 701cf09b9a9dae1e -Model-net-snmp.nmis b6518274fab46b78 a78ed1067f7f14ab e321e3f8a79b25c0 13f1d8c3e10ebebc 997fc7bd3be516be 70491c897fe8d828 e106c9b396e76944 d24bab000b0a6fbe b4d10d3789afa1a6 5d97f9cf73a61919 aa24077be26e5897 -Model.nmis af53a22555c57f63 bece80b7b44d959b 34592112596682e2 a6443ed36ccd2120 c91082df42a88c17 fc6e00d8485d47c7 85b6e9852b359133 0b8ce0fbc6085bea fc31c4ba46c1f4be b8427208bee2fc4d 11d418a22fc2adfb 3c7c7f1471f80e2c efb216ab07a50fd0 d0c4c790f815e46a +Model-net-snmp.nmis c9b5fa32bd1cd51f a78ed1067f7f14ab e321e3f8a79b25c0 13f1d8c3e10ebebc 997fc7bd3be516be 70491c897fe8d828 e106c9b396e76944 d24bab000b0a6fbe b4d10d3789afa1a6 5d97f9cf73a61919 b6518274fab46b78 aa24077be26e5897 +Model.nmis ba5cdf626ee516c1 bece80b7b44d959b 34592112596682e2 a6443ed36ccd2120 c91082df42a88c17 fc6e00d8485d47c7 85b6e9852b359133 0b8ce0fbc6085bea fc31c4ba46c1f4be b8427208bee2fc4d 11d418a22fc2adfb 3c7c7f1471f80e2c efb216ab07a50fd0 d0c4c790f815e46a af53a22555c57f63 diff --git a/admin/upgrade_tables.pl b/admin/upgrade_tables.pl index 0af9e6c..f6f583a 100755 --- a/admin/upgrade_tables.pl +++ b/admin/upgrade_tables.pl @@ -29,7 +29,7 @@ # ***************************************************************************** # # this helper upgrades table files where safe to do so -our $VERSION="8.6.1G"; +our $VERSION="8.6.2a"; use strict; use Digest::MD5; # good enough @@ -181,6 +181,7 @@ sub compute_signature Table-Access.nmis 40fa00017376ca51 Table-BusinessServices.nmis 539adae4fed03ff2 Table-CircuitGroups.nmis 9e00f871315bd959 545ea59abb4473ad +Table-Config.nmis 3d825194acdc9597 Table-Contacts.nmis 50894801c64f3628 3039d313daf0209d Table-Customers.nmis 6503aea40218241e Table-Enterprise.nmis b954e3ca75233bce @@ -189,7 +190,8 @@ sub compute_signature Table-Links.nmis 485da1859e3cc036 3f6a43471d7e4cfe Table-Locations.nmis 2d63c7fa7954e92a bd24e107f8158818 2e2e263c0c08d268 Table-Logs.nmis e70cc904a9d923e5 -Table-Nodes.nmis ef34b637d02ba140 32815befe46ad8e6 c536bfe56a073e41 703420fa55f06eb6 5b144d5f598eb815 013e95cef5802716 80d4c960b5570f70 a8c9b78dd1ad3910 +Table-Nodes.nmis 948b836801802bf9 32815befe46ad8e6 c536bfe56a073e41 703420fa55f06eb6 5b144d5f598eb815 013e95cef5802716 80d4c960b5570f70 a8c9b78dd1ad3910 ef34b637d02ba140 7f1a618619af76f7 +Table-Polling-Policy.nmis 7abb03f496c1943f Table-Portal.nmis f092a05f0f7a2bbb Table-PrivMap.nmis dccc46beb3506fa8 Table-ServiceStatus.nmis be3cd0dfc5c22efa @@ -199,4 +201,4 @@ sub compute_signature Table-Users.nmis e2d4055294ae58e2 Table-cmdbModels.nmis ea9eceefcf31c9ed Table-ifTypes.nmis aed6f62c2aa89d8a -Tables.nmis 07f47ede46bd4779 91d30cf4bfaf25aa 33c5300087f34abb 8d44d78f24b62c7c +Tables.nmis fcd319f68f81fee2 91d30cf4bfaf25aa 33c5300087f34abb 07f47ede46bd4779 8d44d78f24b62c7c diff --git a/bin/fpingd.pl b/bin/fpingd.pl index 879bd9a..6d1597c 100755 --- a/bin/fpingd.pl +++ b/bin/fpingd.pl @@ -27,48 +27,47 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -our $VERSION = "8.6.0G"; +our $VERSION = "8.6.2G"; use FindBin qw($Bin); use lib "$FindBin::Bin/../lib"; use strict; use POSIX qw(setsid); +use Time::HiRes; use Socket; -use NMIS; -use func; use Data::Dumper; use Fcntl qw(:DEFAULT :flock); use File::Basename; use Test::Deep::NoTest; +use Statistics::Lite; + +use NMIS; +use func; +my $me = basename($0); -# Variables for command line munging -my ( $restart, $fpingexit, $debug) = (); my %nvp = getArguements(@ARGV); -my %INFO; -my $qripaddr = qr/\d+\.\d+\.\d+\.\d+/; +my $restartwanted = getbool($nvp{restart}); +my $killwanted = getbool($nvp{kill}); -if (!getbool($nvp{kill}) and !getbool($nvp{restart}) - or (@ARGV == 1 and $ARGV[0] =~ /^-{1,2}(h(elp)?|\?)$/ )) { - my $ext = getExtension(); - my $base = basename($0); - die "$base Version $VERSION +if (!($restartwanted xor $killwanted) # want one or the other, not both and not none + or (@ARGV == 1 and $ARGV[0] =~ /^-{1,2}(h(elp)?|\?)$/ )) +{ + die( "$me Version $VERSION -Usage: $base [debug=true|false] [logging=true|false] [conf=alt.config] +Usage: $me [debug=true|false] [logging=true|false] [conf=alt.config] Command line options are: - restart=true - kill any running daemon(s) and restarts! - debug=true - print status to console and logfile + restart=true - kill any running daemon(s) and restarts + debug=true - print diagnostics to console kill=true - kill any running daemon(s) and exit. Does not launch a new daemon! logging=true - creates a log file 'fpingd.log' in the standard nmis log directory - conf=*.$ext - specify an alternative Conf.$ext file. -a new daemon is started ONLY with restart=true -default is no logging, no debug\n"; +a new daemon is started ONLY with restart=true.\n"); } # load configuration table -my $C = loadConfTable(conf=>$nvp{conf},debug=>$nvp{debug}); +my $C = loadConfTable(debug => $nvp{debug}); # check if nmis is locked, if so shut down immediately. my $lockoutfile = $C->{''}."/NMIS_IS_LOCKED"; @@ -79,123 +78,88 @@ "Set the configuration variable \"global_collect\" to \"true\" to re-enable.\n\n")) } -## setting debug levels -$debug = setDebug($nvp{debug}); -my $logfile = $C->{'fpingd_log'}; -my $runfile = "/var/run/nmis-fpingd.pid"; - -#---------------------------------------- -# check that fping is available! -my $fpingver = `fping -v`; -if ($? >> 8) -{ - &debug("fping binary executable not found, please install fping utility"); - logMsg("ERROR fping binary executable not found, please install fping utility"); - exit(1); -} -else -{ - $fpingver =~ s/^.*Version (\d+\.\d+).*$/$1/s; - &debug("found fping version $fpingver"); -} - +# fixme: logging logic is horribly bad, logmsg goes to nmis.log but is not used for much, +# logging and debug are horribly intermixed in the local debug function; +# debug=0 but logging=1 implies debug level 1. +my $debug = setDebug($nvp{debug}); +my $logfile = $C->{'fpingd_log'}; +my $pidfile = "/var/run/nmis-fpingd.pid"; -#---------------------------------------- - -# Process Control -# check for a running fpingd +# check for any running fpingd instance my $alreadyrunning; -if ( -f $runfile ) { - open(F, "<$runfile"); +if ( -f $pidfile ) +{ + open(F, "<$pidfile") or die "failed to read $pidfile: $!\n"; $alreadyrunning = ; chomp $alreadyrunning; close(F); } -if ( $alreadyrunning and ( getbool($nvp{kill}) or getbool($nvp{restart}) )) + +if ( int($alreadyrunning) # it's a number + and $alreadyrunning != $$ # and it's not me (should be impossible) + and kill(0, $alreadyrunning) # and it's really alive + and ( $killwanted or $restartwanted )) # and we're to get rid of it { - killall($alreadyrunning); + kill('TERM', $alreadyrunning); # polite then firm + sleep(1); + kill('KILL', $alreadyrunning); + unlink($pidfile); + + debug("Killed process $alreadyrunning"); } +exit(0) if ($killwanted); -if (defined $nvp{kill} and getbool($nvp{kill})) +# check that fping is available! +my $fpingver = `fping -v`; +if ($? >> 8) { - debug("Killed process $FindBin::Script, deleted $runfile"); - exit(0); + debug("fping executable not found, please install fping!"); + logMsg("ERROR fping executable not found, please install fping!"); + exit(1); +} +else +{ + $fpingver =~ s/^.*Version (\d+\.\d+).*$/$1/s; + debug("found fping version $fpingver"); } +my $cachetimeout = 900; # remember ip addresses for 15 minutes +my $timeout = $C->{fastping_timeout} || 300 ; +my $length = $C->{fastping_packet} || 56 ; +$length = 24 if ( $length < 24 ); # minimum packet size is 20 + 4 bytes +my $retries = $C->{fastping_retries} || 3; +my $count = $C->{fastping_count} || 3; -#---------------------------------------- -# setup fping calling parameters - -# sysadmin should restart us, if these changed -# set your fping cmd string here. -# values will be subsituted from nmis.conf, or defaults used -# this one for linux: http://fping.sourceforge.net/ -# 'timeout' is subbed for '$timeout' later, etc.. - +# we want fping to do the work, no need to avg/min/max ourselves +# -c X produces: 'somenode : xmt/rcv/%loss = 5/5/0%, min/avg/max = 0.73/0.86/0.97' +my @fpingcmd = ("fping", "-t", $timeout, "-c", $count, "-q", + "-r", $retries, "-b", $length) ; -my $fpingcmd; -# use 'C' [uppercase] for correct parsing of results. -# per-target statistics are displayed in a format designed for automated response-time statistics gathering. -# use this command for fping if script run as user root -if ( $< == 0 ) { - # root user - $fpingcmd = 'fping -t timeout -C count -i 1 -p 1 -q -r retries -b length' ; -} else { - # use this command for fping if ruunning as non-root. - # fping: You need i >= 10, p >= 20, r < 20, and t >= 50 - $fpingcmd = 'fping -t timeout -C count -i 10 -p 20 -q -r retries -b length'; -} +push @fpingcmd, ($< == 0? + # use this command for fping if script run as user root + ("-i", 1, "-p", 1) + : # non-root requires i >= 10, p >= 20, r < 20, and t >= 50 + ("-i", 10, "-p", 20)); -# set fping defaults, equal to ping.pm -my $timeout = $C->{fastping_timeout} ? $C->{fastping_timeout} : 300 ; -my $length = $C->{fastping_packet} ? $C->{fastping_packet} : 56 ; -my $retries = $C->{fastping_retries} ? $C->{fastping_retries} : 3; -my $count = $C->{fastping_count} ? $C->{fastping_count} : 3; -my $sleep = $C->{fastping_sleep} ? $C->{fastping_sleep} : 60; -my $nodepoll = $C->{fastping_node_poll} ? $C->{fastping_node_poll} : 300; +# how many nodes per fping invocation +my $maxnodes = $C->{fastping_node_poll} || 200; # should we write a raw event log without stateful deduplication? -my $raweventlog = $C->{fastping_stateless_log} || ''; - -# fping requires a minimum of 24 byte packets -if ( $length < 24 ) { $length = 24 } - -$fpingcmd =~ s/timeout/$timeout/; -$fpingcmd =~ s/length/$length/; -$fpingcmd =~ s/retries/$retries/; -$fpingcmd =~ s/count/$count/; +my $raweventlog = $C->{fastping_stateless_log}; my $ext = getExtension(dir=>'var'); -&debug( "logfile = $FindBin::Bin/../logs/fpingd.log") if defined $nvp{logging}; -&debug( "logging not enabled - set cmdline option \'logging=true\' if logging required") if !defined $nvp{logging}; -&debug( "pidfile = $runfile"); -&debug( "ping result file = $FindBin::Bin/../var/fping.$ext"); -&debug( "fping cmd: $fpingcmd"); +debug( "logfile = $FindBin::Bin/../logs/fpingd.log") if defined $nvp{logging}; +debug( "logging not enabled - set cmdline option \'logging=true\' if logging required") if !defined $nvp{logging}; +debug( "pidfile = $pidfile"); +debug( "ping result file = $FindBin::Bin/../var/fping.$ext"); +debug( "fping cmd: ".join(" ",@fpingcmd)); #--------------------------------------- # process control -# setup a trap for fatal signals, setting a flag to indicate we need to gracefully exit. -$SIG{INT} = \&catch_zap; -$SIG{TERM} = \&catch_zap; -$SIG{HUP} = \&catch_zap; - -# set ourselves as a daemon -#--------------------------------------------------------- -POSIX::setsid() or die "Can't start new session: $!"; -chdir('/') or die "Can't chdir to /: $!"; - - # for debugging, undocumented: argument foreground=true if (!getbool($nvp{"foreground"})) { - # Reopen stdout, stdin with /dev/null; stderr to the fpingd logfile - # if we dont reopen, the calling terminal will wait, and nmis.pl daemon control will hang - open(STDIN, "<", "/dev/null") or die "cannot reopen stdin: $!\n"; - open(STDOUT, ">", "/dev/null") or die "cannot reopen stdout: $!\n"; - open(STDERR, ">>", $logfile) or die "cannot open stdout to $logfile: $!\n"; - setFileProtDiag(file => $logfile); - if (!defined(my $pid = fork)) { die "cannot fork: $!\n"; @@ -203,25 +167,35 @@ elsif ($pid) { # parent: exits + debug("daemon $pid was started."); exit (0); } + # child continues with the actual work + POSIX::setsid() or die "Can't start new session: $!"; + chdir('/') or die "Can't chdir to /: $!"; + + # handler sets a flag to indicate we need to exit. + $SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&catch_zap; + + # Reopen stdout, stdin with /dev/null; stderr to the fpingd logfile + # if we dont reopen, the calling terminal will wait, and nmis.pl daemon control will hang + open(STDIN, "<", "/dev/null") or die "cannot reopen stdin: $!\n"; + open(STDOUT, ">", "/dev/null") or die "cannot reopen stdout: $!\n"; + open(STDERR, ">>", $logfile) or die "cannot open stdout to $logfile: $!\n"; + setFileProtDiag(file => $logfile); } # Announce our presence via a PID file -open(PID, ">$runfile") or warn "\t Could not create $runfile: $!\n"; +open(PID, ">$pidfile") or die "Could not create $pidfile: $!\n"; print PID $$; close PID; -&debug("daemon started, pidfile $runfile created with pid: $$"); -logMsg("INFO daemon fpingd started, pidfile $runfile created with pid: $$"); -umask 0; +debug("daemon started"); +logMsg("INFO daemon fpingd started, pidfile $pidfile created with pid: $$"); -if ( !getbool($C->{daemon_fping_dns_cache},"invert") ) { - logMsg("INFO daemon fpingd will cache DNS for improved name resolution"); -} -else { - logMsg("WARNING daemon fpingd will not CACHE DNS, use under adult supervision"); -} +logMsg( !getbool($C->{daemon_fping_dns_cache},"invert")? + "INFO daemon fpingd will use and cache DNS for improved name resolution" + : "WARNING daemon fpingd will not use DNS, use under adult supervision!" ); # remember the original script location plus the parameter that we want to push through for restart @@ -229,287 +203,277 @@ my $origscript = $FindBin::RealBin."/".$FindBin::Script; # we want to keep any params, except kill my @restartparams = map { "$_=".$nvp{$_}; } (grep($_ ne "kill", keys %nvp)); +$0 = $me; -# set our name so that rc scripts can figure out who we are. -$0 = $FindBin::Script; -$restart = 1; +my (%state, # nodename -> ip, lastping, nextping, nextdns, avg, loss + $preveventcfg, # change detection + $prevmaincfg, # change detection - loadconftable is not mtime-aware... + $mustexit); +while (!$mustexit) +{ + my $now = Time::HiRes::time; + my $escalatables; -# loop until a sighandler set $fpingexit -# normally fastping does NOT return anyway, so that while is pretty unnecessary -fastping() while (!$fpingexit); -exit 0; + # nmis locked? nothing for this daemon to do + my $lockoutfile = $C->{''}."/NMIS_IS_LOCKED"; + if (-f $lockoutfile or getbool($C->{global_collect},"invert")) + { + logMsg("WARN fping is terminating because NMIS is disabled!"); + die ("Attention: fping is terminating because NMIS is disabled!\n"); + } + # react to actual changes to events config by restarting, as that'd affect the notify/checkEvent code + my $eventconfig = loadTable(dir => 'conf', name => 'Events'); + my $mainconfig = loadTable(dir => 'conf', name => 'Config'); -# loop over nodes, ping them; wait a little then continue -sub fastping -{ - my $nodelist; - my $read_cnt = 0; - my $start_time; - my $prevlnt; + my $whichchanged = (defined($preveventcfg) && !eq_deeply($preveventcfg, $eventconfig) ? + "Events List" : + (defined($prevmaincfg) && !eq_deeply($prevmaincfg,$mainconfig) ? + "Config" : undef)); + if ($whichchanged) + { + logMsg("INFO fpingd will restart, $whichchanged has changed"); + exec($origscript,@restartparams); + die "$0 couldn't restart itself: $!\n"; # shouldn't be reached + } + $preveventcfg = $eventconfig; + $prevmaincfg = $mainconfig; + + # nodes, polling-policy: reread every cycle, cached + my $policies = loadTable(dir => 'conf', name => "Polling-Policy") || {}; + my $lnt = loadLocalNodeTable() || {}; - # cannot use loadGenericTable as that checks and clashes with db_events_sql - my $oldeventconfig = loadTable(dir => 'conf', name => 'Events'); - my $qr_parse_result = qr/^.*\s+:(?:(?: \d+\.\d+)|(?: -)){1,$count}$/; + # first: find the candidate nodes (and ditch deleted/disabled ones) + for my $maybegoner (keys %state) + { + delete $state{$maybegoner} + if (ref($lnt->{$maybegoner}) ne "HASH" + or !getbool($lnt->{$maybegoner}->{active}) + or !getbool($lnt->{$maybegoner}->{ping})); + } - while(1) + my @todos; + for my $noderec (values %$lnt) { - $start_time = time(); + next if (!getbool($noderec->{active}) or !getbool($noderec->{ping})); + + my $thisstate = $state{ $noderec->{name} } ||= { + name => $noderec->{name}, + # dynamically managed: ip, lastping,nextping, policy , nextdns, avg min max loss + }; + # what needs filling in, what could change between poll cycles? + $thisstate->{host} = $noderec->{host}; + $thisstate->{policy} = $noderec->{polling_policy} || 'default'; + + # honor fixed ip address given + # fixme: fping doesn't do ipv6, we would have to use fping6 for that. + if ($thisstate->{host} =~ /^\d+\.\d+\.\d+\.\d+$/) + { + $thisstate->{ip} = $thisstate->{host}; + } + # allowed to resolve name and cache address? + elsif (!getbool($C->{daemon_fping_dns_cache},"invert")) + { + if (!$thisstate->{ip} or $now >= $thisstate->{nextdns}) + { + # fixme: this returns only v4 addresses, see fping6 comment above + $thisstate->{ip} = NMIS::resolveDNStoAddr($thisstate->{host}) # may be undef + || $thisstate->{host}; # again falling back to leaving this to fping + debug("refreshed dns for $thisstate->{host} to $thisstate->{ip}" + .($thisstate->{nextdns}? sprintf(", was due %.2fs ago", $now - $thisstate->{nextdns}):"")) + if ($debug > 1); + $thisstate->{nextdns} = $now + $cachetimeout; + } + } + # otherwise leave it to fping to Do Something with the host (name or whatever) + else + { + $thisstate->{ip} = $thisstate->{host}; + } - my $lnt = loadLocalNodeTable(); - if ($prevlnt && !eq_deeply($lnt, $prevlnt)) + # second: find out which ones are due for pinging this cycle + if (!$thisstate->{nextping} or $thisstate->{nextping} <= $now) { - debug("Nodes list has changed, reloading after the next sleep"); - logMsg("INFO fpingd will reload the Nodes list as it has changed"); - $read_cnt = 1 if ($read_cnt > 1); # reload no later than after this run + debug("will ping node $thisstate->{name} this cycle" + . ($thisstate->{nextping}? sprintf(", was due %.2fs ago", $now - $thisstate->{nextping}): "" )) + if ($debug > 1); + push @todos, $thisstate->{name}; } - $prevlnt = $lnt; + } - if ($read_cnt-- <= 0) { - # check every 10 runs for update of Node table - not on every cycle as it involves lots of dns! - debug("Rereading Nodes list"); - $nodelist = readNodes(); - $read_cnt = 10; + my @thistime = @todos; # todos is consumed + # third: ping the ones in need, in chunks if necessary + while (my @chunk = splice(@todos, 0, $maxnodes)) + { + my %ip2node; + my @cmd = @fpingcmd; + for my $nodename (@chunk) + { + my $thisip = $state{$nodename}->{ip}; + + push @cmd, $thisip; + $ip2node{$thisip} = $nodename; # for associating the results } + debug("about to run: ".join(" ",@cmd)); - my %ping_result = (); # clear hash + # pity that fping reports on stderr, NOT stdout; we need the shell redirection (or a child and replumb) + if (!open(FROMFPING, "-|", join(" ", @cmd, "2>&1"))) + { + logMsg("ERROR failed to run fping: $!"); + die "Failed to run fping: $!\n"; + } - foreach my $row (sort keys %{$nodelist}) + while (my $line = ) { - my $nodes = $nodelist->{$row}; - &debug("\nfping $row about to ping :\t$nodes"); - if ( open(IN, "$fpingcmd $nodes 2>&1 |") ) { - - while () { - chomp; - - my ( $flag, $hostname, $r, @rlist, $min, $max, $avg, $loss, $tot, $count); - $flag = $loss = $count = $min = $max = $avg = $tot = 0; - - # possible results are: - # host1 : - - - - # host2 : 0.18 0.19 0.19 - # host3 : 0.22 - 0.33 second ping timeout - # or rubbbish - &debug( "fping returned:\t$_") if $debug > 1; - if (/$qr_parse_result/) { - - ($hostname, $r) = split /:/, $_ ; # split on : into name, list of results - $hostname = trim($hostname); - $r = trim($r); - - foreach my $s ( split / /, $r ) { - $s = trim($s); - if ( $s eq '-' ) { - $loss++; - } else { - $min = $s if !$flag; # seed $min on first pass - $max = $s if !$flag; - $flag++; - - # result - set min, max, and count for total - - $min = $s if $s < $min; - $max = $s if $s > $max; - $tot += $s; - } - $count++; - } - if ( $loss eq $count ) { # nothing... - $min = $max= $avg = ''; - $loss = 100; - } else { - $avg = sprintf "%.2f", $tot / $count; - $loss = int( ($loss/$count) * 100 ); - } - } else { - logMsg("INFO fping returned=$_"); - ### 2012-02-24 keiths, update from Till Dierkesmann to handle ICMP oddness - my $pingedhost=$_; - $pingedhost=~s/(.*)(\D)(\d+\.\d+\.\d+\.\d+)(.*)/$3/; - if(!defined $hostname or $hostname eq "") { - logMsg("INFO Hostname seems to be $pingedhost"); - $hostname=$pingedhost; - } - } - - - &debug( localtime( time() ) . " $hostname : $min, $max, $avg, $loss"); - - # get node name back from host - if ( not exists $INFO{$hostname}{node} ) { - ## 2011-12-07 keiths, changing error to be more accurate. - logMsg("ERROR hostname $hostname not found in FPING results"); - } else { - # save only used info by nmis.pl - $ping_result{$INFO{$hostname}{node}} = { - 'loss' => $loss, - 'avg' => $avg, - 'lastping' => "". localtime(time()) - }; - #Other possible items can be added from here. - #$ping_result{$INFO{$hostname}{node}} = { - # 'loss' => $loss, - # 'min' => $min, - # 'avg' => $avg, - # 'max' => $max, - # 'ip' => $hostname, - # 'lastping' => "" . localtime( time() ) - # }; - } + chomp $line; + # goodnode : xmt/rcv/%loss = 5/5/0%, min/avg/max = 0.89/0.96/1.05 + # badnode : xmt/rcv/%loss = 5/0/100% + # or nothing for unresolvable. + debug("fping returned: $line") if ($debug > 2); + + my ($hostname,$loss,$min,$avg,$max) = ($1,$2,$3,$4,$5,$6) + if ($line =~ m!^\s*(\S+)\s*:\s*xmt/rcv/%loss\s*=\s*\d+/\d+/(\d+)%(?:,\s*min/avg/max\s*=\s*(\d+(?:\.\d+)?)/(\d+(?:\.\d+)?)/(\d+(?:\.\d+)?))?$!); + + if ($hostname # parseable? + && $ip2node{$hostname} # known? + && $loss =~ /^\d+$/ ) # structure good enough for the reachability at least? + { + my $thisstate = $state{ $ip2node{$hostname} }; + # what does the policy say about the interval? + # policy present, use that; fall back to 60 seconds otherwise + my $interval = ref($policies->{ $thisstate->{policy} }) eq "HASH"? + $policies->{ $thisstate->{policy} }->{ping} : 60; # seconds + + # supports NNN (seconds) or MMMU with U being s, m, h or d, fractional NNN or MMM also ok + if ($interval =~ /^\s*(\d+(\.\d+)?)([smhd])$/) + { + my ($rawvalue, $unit) = ($1, $3); + $interval = $rawvalue * ($unit eq 'm'? 60 : $unit eq 'h'? 3600 : $unit eq 'd'? 86400 : 1); } - close IN; - ### logMsg("INFO run time of fping is ".(time()-$start_time)." sec. pinged ".(scalar keys %ping_result)." nodes"); + # the regex extraction sets missing to blank string, would prefer undef or number + $thisstate->{loss} = int($loss); + $thisstate->{avg} = $avg ne ""? 0+$avg : undef; + $thisstate->{min} = $min ne ""? 0+$min : undef; + $thisstate->{max} = $max ne ""? 0+$max : undef; + + $thisstate->{lastping} = $now; + $thisstate->{nextping} = $now + $interval; + + debug("parsed result for node $ip2node{$hostname}: ".Dumper($thisstate)) if ($debug > 2); } else { - logMsg("ERROR could not open pipe to fping: $!"); - exit 1; + debug("ERROR fping result \"$line\" was not parseable!"); + logMsg("ERROR result \"$line\" was not parseable!"); } - } # foreach row in nodelist + } + close FROMFPING; + } + # all pinging done, let's dump the state onto disk as soon as we can + writeTable(dir => 'var', name => "nmis-fping", data => \%state); - # Loop over results and send out up or down events + # fourth: analyse the results we've got this time, trigger nmis operations as needed + for my $nodename (@thistime) + { + my $thisstate = $state{$nodename}; - foreach my $nd ( keys %ping_result ) + # write raw events if requested - regardless of state change or not! + # note that this is a debugging aid only. + if ($raweventlog) { - # write raw events if requested - regardless of state change or not! - # note that this is a debugging aid only. - if ($raweventlog ne '') + my @problems; + if (!open(RF,">>$raweventlog")) { - my @problems; - if (!open(RF,">>$raweventlog")) - { - push @problems, "ERROR could not open $raweventlog: $!"; - } - else - { - if (!flock(RF, LOCK_EX)) - { - push @problems, "ERROR could not lock $raweventlog: $!"; - } - else - { - my $down = $ping_result{$nd}{'loss'} == 100; - my $event = "Node ".($down? "Down":"Up"); - my $level = $down? "Critical":"Normal"; # fixme this is not as precise as the stateful - my $details = $down? "Ping failed" : "Ping succeeded, loss=$ping_result{$nd}{'loss'}%"; - - print RF join(",", time, $nd, $event, $level, '', $details),"\n"; - close RF or push(@problems,"ERROR could now write to or close $raweventlog: $!"); - } - } - map { logMsg($_) } (@problems); # DO NOT run logMsg inside a critical section or while holding a lock! + push @problems, "ERROR could not open $raweventlog: $!"; } - - # only submit changes in status to the event system, or submit all if restart - if ( $ping_result{$nd}{'loss'} == 100 ) + else { - &debug( "[" . localtime( time() ) . - "]\tPinging Failed $nd is NOT REACHABLE, returned loss=$ping_result{$nd}{'loss'}%"); - $INFO{$nd}{name} = ''; # maybe DNS changed - - # hold-off time is past? - if ($INFO{$nd}{postpone_time} >= $INFO{$nd}{postpone}) - { - if ( $restart ) - { - fpingNotify($nd); - } - elsif ( not eventExist($nd, "Node Down", undef) ) - { - # Device is DOWN, was up, as no entry in event database - &debug("\t$nd is now DOWN, was UP, Updating event database"); - fpingNotify($nd); - } - else - { - # was down, and still is down... - # uncomment this if you want to force an update each run - intensive !!! - # fpingNotify($nd); - } - } - else # still within the hold-off period + if (!flock(RF, LOCK_EX)) { - $ping_result{$nd}{'loss'} = 0; # simulate ok until postpone time elapsed - } - $INFO{$nd}{postpone_time} += 70; # add minute - } - else # not 100% loss - { - # node pingable - $INFO{$nd}{postpone_time} = 0; # reset - &debug( "[" . localtime( time() ) . "]\t$nd is PINGABLE: returned min/avg/max = $ping_result{$nd}{'min'}/$ping_result{$nd}{'avg'}/$ping_result{$nd}{'max'} ms loss=$ping_result{$nd}{'loss'}%"); - - if ( $restart ) - { - fpingCheckEvent($nd); + push @problems, "ERROR could not lock $raweventlog: $!"; } else { - # check the event existence AND its currency! - my $event_exists = eventExist($nd, "Node Down", undef); - my $erec = eventLoad(filename => $event_exists) if ($event_exists); - - if ($event_exists and $erec and getbool($erec->{current})) - { - # Device was down is now UP! - # Only post the status if the event database records as currently down - &debug("\t$nd is now UP, was DOWN, Updating event database"); - fpingCheckEvent($nd); - } - elsif ( !$event_exists ) { - # was up, and still is up... - # uncomment this if you want to force an update each run - intensive !!! - # fpingCheckEvent($nd); - } + my $down = ($thisstate->{loss} == 100); + my $event = "Node ".($down? "Down":"Up"); + my $level = $down? "Critical":"Normal"; + my $details = $down? "Ping failed" : "Ping succeeded, loss=$thisstate->{loss}%"; + + print RF join(",", time, $nodename, $event, $level, '', $details),"\n"; + close RF or push(@problems,"ERROR could now write to or close $raweventlog: $!"); } } + map { logMsg($_) } (@problems); # DO NOT run logMsg inside a critical section or while holding a lock! } - # At this point, %ping_result is a hash populated by ping results keyed by NMIS host names - # Write the hash out to a file - writeTable(dir=>'var',name=>"nmis-fping",data=>\%ping_result ); - - # check if the config is still unchanged, if not restart (but only after firstrun) - # ditto for the events config - my $newconf = loadConfTable(conf=>$nvp{conf},debug=>$nvp{debug}); - # re-check for nmis locked - my $lockoutfile = $newconf->{''}."/NMIS_IS_LOCKED"; - if (-f $lockoutfile or getbool($newconf->{global_collect},"invert")) + # submit changes in status to the nmis event system + if ( $thisstate->{loss} == 100 ) { - logMsg("WARN fping is terminating because NMIS is disabled!"); - die ("Attention: fping is terminating because NMIS is disabled!\n"); + debug("Node $nodename is NOT REACHABLE, fping reported loss=$thisstate->{loss}%"); + # for unreachable nodes where we're caching the dns-ip assocition, we mark it as 'recheck dns' + # for fixed-ip nodes this is not relevant + undef $thisstate->{nextdns}; + + if (!eventExist($nodename, "Node Down", undef)) + { + # Device is DOWN, was up, as no entry in event database + debug("$nodename is now DOWN, was UP, updating event database"); + fpingNotify($nodename); + ++$escalatables; + } } + else # node somewhat pingable, not 100% loss + { + debug("$nodename is pingable: returned min/avg/max = $thisstate->{min}/$thisstate->{avg}/$thisstate->{max}ms loss=$thisstate->{loss}%"); - # cannot use loadGenericTable as that checks and clashes with db_events_sql - my $eventconfig = loadTable(dir => 'conf', name => 'Events'); + # check the event existence AND its currency! + my $event_exists = eventExist($nodename, "Node Down", undef); + my $erec = eventLoad(filename => $event_exists) if ($event_exists); - my $whichchanged = !eq_deeply($oldeventconfig, $eventconfig) ? - "Events List" : !eq_deeply($C,$newconf) ? "Config" : undef; - if ($whichchanged) - { - debug("$whichchanged has changed, will restart after this sleep"); - logMsg("INFO fpingd will restart after this sleep, $whichchanged has changed"); - sleep(int(5-rand(10)) + $sleep); # standard interval +/- 5 sec - logMsg("INFO fpingd is restarting now"); - exec($origscript,@restartparams); - die "$0 couldn't restart itself: $!\n"; # shouldn't be reached + if ($event_exists and $erec and getbool($erec->{current})) + { + # Device was down is now UP! + # Only post the status if the event database records as currently down + debug("$nodename is now UP, was DOWN, updating event database"); + fpingCheckEvent($nodename); + ++$escalatables; + } } + } - # sleep for a while - $restart = 0; # first run done - &debug("sleeping ..."); - # Generate random # from 1-10 + $C->{fastping_sleep} - - ### 2013-02-14 keiths, run the NMIS escalation process for faster outage notifications. - my $lines = `$C->{''}/nmis.pl type=escalate debug=$debug`; + # if desired and useful, run the NMIS escalation process here + # for faster outage notifications; also runs as part of collect, + # or can be run via cron. + if ($escalatables && getbool($C->{daemon_fping_run_escalation})) + { + # but we certainly don't wait for it to finish + fork || exec("$C->{''}/nmis.pl","type=escalate"); + } - sleep(int(5-rand(10)) + $sleep); + # we're done for this cycle, so how long should we sleep? + # who's next? don't show gazillions of nodes unless debug level is really high + if ($debug > 5) + { + my @soontonever = sort { $state{$a}->{nextping} <=> $state{$b}->{nextping} } (keys %state); + debug("next ping ordering:\n\t" + . join("\n\t", map { sprintf("%s in %.2fs", $_, $state{$_}->{nextping}-$now) } (@soontonever))); + } + my $nextone = Statistics::Lite::min( map { $_->{nextping} } (values %state)); + # if we don't know anything we'll just sleep for 10 seconds + my $naptime = ($nextone && $nextone - $now > 0)? int(0.5 + $nextone - $now) : 10; + if ($naptime > 0) + { + debug("sleeping $naptime seconds"); + reaper(); # unlikely that the escalate is done already but bsts, reaping costs nothing + sleep($naptime); } + reaper(); } +exit 0; + # check-and-remove existing node down event # args: node name @@ -517,7 +481,7 @@ sub fastping sub fpingCheckEvent { my $node = shift; - &debug("\tUpdating event database via sub checkEvent() host: $node event: Node Up"); + debug("\tUpdating event database via sub checkEvent() host: $node event: Node Up"); my $S = Sys::->new; $S->init(name => $node, snmp => 'false'); @@ -537,7 +501,7 @@ sub fpingNotify { my $node = shift; - &debug("\tUpdating event database via sub notify() host: $node event: Node Down"); + debug("\tUpdating event database via sub notify() host: $node event: Node Down"); my $S = Sys::->new; $S->init(name=>$node, snmp=>'false'); @@ -549,127 +513,54 @@ sub fpingNotify context => { type => "node" }); } -sub trim { - my $s = shift; - return '' if ! $s; - $s =~ s/^\s+//; - $s =~ s/\s+$//; - return $s; -} +sub debug +{ + my (@msgs) = @_; + + print STDOUT returnDateStamp(), " $me\[$$] ", @msgs, "\n" + if ($debug); -sub debug { - print STDOUT "\tfpinger: $_[0]\n" if $debug; if ( $nvp{logging} ) { - open LOG, ">>$logfile" or warn "Can't write to $logfile: $!"; - print LOG returnDateStamp(). " ". $_[0] ."\n"; + open LOG, ">>$logfile" + or warn "Can't write to $logfile: $!"; + print LOG returnDateStamp(), " $me\[$$] ", @msgs, "\n"; close LOG; setFileProtDiag(file => $logfile); } } -sub catch_zap { - $fpingexit++; - &debug("I was killed by $_[0]"); - logMsg("INFO daemon fpingd killed by $_[0]", - do_not_lock => 1); - unlink $runfile; - die "I was killed by $_[0]: $!\n"; -} +sub catch_zap +{ + my ($sig) = @_; -# kill all given processes except me..! -sub killall { - my (@shootthem) = @_; + $mustexit++; - foreach my $p (@shootthem) - { - next if !$p or $p eq $$; - kill 9, $p; - &debug("killed running process pid $p "); - } + debug("I was killed by $sig"); + logMsg("INFO daemon fpingd killed by $sig", + do_not_lock => 1); + unlink $pidfile; } -# read node info from file. maybe cached, or from db -# returns sorted list of ips to ping, chunked into rows -sub readNodes +# this is a general-purpose reaper of zombies +# args: none, returns: hash of process ids -> statuses that were reaped +# +# you can use this to just periodically collect zombies, +# or as a signal handler, but: +# +# PLEASE NOTE: if you attach it to $SIG{CHLD}, then +# this CAN AND WILL interfere with getting exit codes from +# backticks, system, and open-with-pipe, because the child handler +# can run before the perl standard wait() for these ipc ops, +# hence $? becomes -1 because the wait() was preempted. +# +sub reaper { - my @hosts; - my $NT = loadLocalNodeTable(); # from (cached) file or db - - foreach my $nd (sort keys %{$NT} ) { - if ( getbool($NT->{$nd}{active}) and getbool($NT->{$nd}{ping})) { - if ( $INFO{$nd}{name} eq '' or $NT->{$nd}{host} ne $INFO{$nd}{org_host}) { - # new entry or changed host address - $INFO{$nd}{org_host} = $NT->{$nd}{host}; # remember original for changes - $INFO{$nd}{name} = $nd; # remember name of node - - # Optionally Caching DNS, improved performance but makes development harder :-) - if ( !getbool($C->{daemon_fping_dns_cache},"invert") ) { - if ($NT->{$nd}{host} =~ /$qripaddr/) { - $INFO{$NT->{$nd}{host}}{node} = $nd; - $INFO{$nd}{host} = $NT->{$nd}{host}; - } - # get ip address - elsif ((my $addr = resolveDNStoAddr($NT->{$nd}{host}))) { - $INFO{$addr}{node} = $nd; # for backwards search - $INFO{$nd}{host} = $addr; - } else { - logMsg("ERROR cannot resolve host=$NT->{$nd}{host} from node $nd to IP address using OS (e.g. DNS or /etc/hosts)"); - next; - } - } - else { - #maintain cache for consistency - $INFO{$NT->{$nd}{host}}{node} = $nd; # for backwards search - $INFO{$nd}{host} = $NT->{$nd}{host}; - } - } - # feature - # if node is not more pingeble then wait 'postpone' time (seconds) to generate event - $INFO{$nd}{postpone} = 0; - if ($NT->{$nd}{postpone} ne "") { - if ($NT->{$nd}{postpone} =~ /d+/) { - $INFO{$nd}{postpone} = $NT->{$nd}{postpone}; # in seconds - } else { - logMsg("ERROR ($nd) value of postpone in table Nodes must be numeric value (seconds)"); - } - } - push @hosts,$INFO{$nd}{host}; - } - else { - &debug("readNodes, skipping fping of $nd, $NT->{$nd}{host}"); - } - } - - if ( ! @hosts ) { - &debug("No nodes found to ping"); - logMsg("INFO no nodes found in Node table to ping, exit daemon"); - exit 1; - } else { - &debug("Read Nodelist, @hosts"); - } + my %exparrots; - ### 2012-02-22 keiths, fping $nodepoll nodes at time, exceeding command line of 4098 bytes - my $nodelist; - my $row = 0; - my $hostcount = 0; - my @shorthosts; - for my $host (sort @hosts) { - ++$hostcount; - if ( $hostcount < $nodepoll ) { - push(@shorthosts,$host); - } - else { - push(@shorthosts,$host); - &debug("Splitting nodes into chunks of $nodepoll nodes: @shorthosts"); - $nodelist->{$row} = join(' ',@shorthosts); # string of hosts separated by space - ++$row; - $hostcount = 0; - @shorthosts = (); - } + while ((my $pid = waitpid(-1, POSIX::WNOHANG)) > 0) + { + $exparrots{$pid} = $?; } - # put the left over nodes into nodelist! - $nodelist->{$row} = join(' ',@shorthosts); - - return $nodelist; + return %exparrots; } diff --git a/bin/ipslad.pl b/bin/ipslad.pl index e446b72..c010880 100755 --- a/bin/ipslad.pl +++ b/bin/ipslad.pl @@ -127,7 +127,9 @@ chomp $pid; if ($pid != $$) { logIpsla("IPSLAD: pidfile exists killing the pidfile process $pid"); - kill 9, $pid; + kill('TERM',$pid); + sleep(1); + kill('KILL', $pid); unlink($pidfile); logIpsla("IPSLAD: pidfile $pidfile deleted"); } diff --git a/bin/nmis.pl b/bin/nmis.pl index f5565bc..9db4ca7 100755 --- a/bin/nmis.pl +++ b/bin/nmis.pl @@ -66,6 +66,12 @@ $Data::Dumper::Indent = 1; +if ( @ARGV == 1 && $ARGV[0] eq "--version" ) +{ + print "version=$NMIS::VERSION\n"; + exit 0; +} + # Variables for command line munging my %nvp = getArguements(@ARGV); @@ -123,7 +129,7 @@ # all arguments are now stored in nvp (name value pairs) my $type = lc $nvp{type}; -my $node = lc $nvp{node}; +my $node = $nvp{node}; my $rmefile = $nvp{rmefile}; my $runGroup = $nvp{group}; my $sleep = $nvp{sleep}; @@ -144,7 +150,7 @@ Proc::Queue::size($maxThreads); # changing limit of concurrent processes Proc::Queue::trace(0); # trace mode on Proc::Queue::debug(0); # debug is off -Proc::Queue::delay(0.02); # set 20 milliseconds as minimum delay between fork calls, reduce to speed collect times +Proc::Queue::delay(0); # if no type given, just run the command line options if ( $type eq "" ) { @@ -170,9 +176,11 @@ &NMIS::upgrade_events_structure; # ditto for nodeconf &NMIS::upgrade_nodeconf_structure; +# and for outages +&NMIS::upgrade_outages; if ($type =~ /^(collect|update|services)$/) { - runThreads(type=>$type,node=>$node,mthread=>$mthread,mthreadDebug=>$mthreadDebug); + runThreads(type=>$type, node=>$node, mthread=>$mthread, mthreadDebug=>$mthreadDebug); } elsif ( $type eq "escalate") { runEscalate(); printRunTime(); } # included in type=collect elsif ( $type eq "config" ) { checkConfig(change => "true"); } @@ -181,10 +189,10 @@ elsif ( $type eq "apache" ) { printApache(); } elsif ( $type eq "apache24" ) { printApache24(); } elsif ( $type eq "crontab" ) { printCrontab(); } -elsif ( $type eq "summary" ) { nmisSummary(); printRunTime(); } # included in type=collect +elsif ( $type eq "summary" ) { nmisSummary(); printRunTime(); } # MIGHT be included in type=collect elsif ( $type eq "rme" ) { loadRMENodes($rmefile); } -elsif ( $type eq "threshold" ) { runThreshold($node); printRunTime(); } # included in type=collect -elsif ( $type eq "master" ) { nmisMaster(); printRunTime(); } # included in type=collect +elsif ( $type eq "threshold" ) { runThreshold($node); printRunTime(); } # USUALLY included in type=collect +elsif ( $type eq "master" ) { nmisMaster(); printRunTime(); } # MIGHT be included in type=collect elsif ( $type eq "groupsync" ) { sync_groups(); } elsif ( $type eq "purge" ) { my $error = purge_files(); die "$error\n" if $error; } else { checkArgs(); } @@ -201,11 +209,11 @@ my $node_select = $args{'node'}; my $mthread = getbool($args{mthread}); my $mthreadDebug = getbool($args{mthreadDebug}); - my $debug_watch; dbg("Starting, operation is $type"); - # first thing: do a selftest and cache the result. this takes about five seconds (for the process stats) + # do a selftest and cache the result, but not too often + # this takes about five seconds (for the process stats) # however, DON'T do one if nmis is run in handle-just-this-node mode, which is usually a debugging exercise # which shouldn't be delayed at all. ditto for (possibly VERY) frequent type=services if (!$node_select and $type ne "services") @@ -214,7 +222,6 @@ setFileProtDirectory($C->{''}, 1); # do recurse setFileProtDirectory($C->{''}, 0); # no recursion required - info("Starting selftest (takes about 5 seconds)..."); my $varsysdir = $C->{''}."/nmis_system"; if (!-d $varsysdir) { @@ -223,45 +230,56 @@ } my $selftest_cache = "$varsysdir/selftest"; - # check the current state, to see if a perms check is due? once every 2 hours my $laststate = readFiletoHash(file => $selftest_cache, json => 1); + # check if a selftest is due? once every 15 minutes + my $wantselftestnow = 1 if (ref($laststate) ne "HASH" + || !defined($laststate->{lastupdate}) + || ($laststate->{lastupdate} + 900 < time)); + # check the current state, to see if a perms check is due? once every 2 hours my $wantpermsnow = 1 if (ref($laststate) ne "HASH" || !defined($laststate->{lastupdate_perms}) || $laststate->{lastupdate_perms} + 7200 < time); - - my ($allok, $tests) = func::selftest(config => $C, delay_is_ok => 'true', - perms => $wantpermsnow, - report_database_status => \$selftest_dbdir_status); - - # keep the old permissions state if this test did not run a permissions test - # hardcoded test name isn't great, though. - if (!$wantpermsnow) + if ($wantselftestnow) { - $laststate ||= { tests => [] }; + info("Starting selftest (takes about 5 seconds)..."); + my ($allok, $tests) = func::selftest(config => $C, delay_is_ok => 'true', + perms => $wantpermsnow, + report_database_status => \$selftest_dbdir_status); - my ($oldstate) = grep($_->[0] eq "Permissions", @{$laststate->{tests}}); # there will at most one - if (defined $oldstate) + # keep the old permissions state if this test did not run a permissions test + # hardcoded test name isn't great, though. + if (!$wantpermsnow) { - my ($targetidx) = grep($tests->[$_]->[0] eq "Permissions", (0..$#{$tests})); - if (defined $targetidx) - { - $tests->[$targetidx] = $oldstate; - } - else + $laststate ||= { tests => [] }; + + my ($oldstate) = grep($_->[0] eq "Permissions", @{$laststate->{tests}}); # there will at most one + if (defined $oldstate) { - push @$tests, $oldstate; + my ($targetidx) = grep($tests->[$_]->[0] eq "Permissions", (0..$#{$tests})); + if (defined $targetidx) + { + $tests->[$targetidx] = $oldstate; + } + else + { + push @$tests, $oldstate; + } + $allok = 0 if ($oldstate->[1]); # not ok until that's cleared } - $allok = 0 if ($oldstate->[1]); # not ok until that's cleared } - } - writeHashtoFile(file => $selftest_cache, json => 1, - data => { status => $allok, - lastupdate => time, - lastupdate_perms => ($wantpermsnow? time - : $laststate? $laststate->{lastupdate_perms} : undef), - tests => $tests }); - info("Selftest completed (status ".($allok?"ok":"FAILED!")."), cache file written"); + writeHashtoFile(file => $selftest_cache, json => 1, + data => { status => $allok, + lastupdate => time, + lastupdate_perms => ($wantpermsnow? time + : $laststate? $laststate->{lastupdate_perms} : undef), + tests => $tests }); + info("Selftest completed (status ".($allok?"ok":"FAILED!")."), cache file written"); + } + else + { + info("Skipping selftest, last run at ". returnDateStamp($laststate->{lastupdate})); + } } # load all the files we need here @@ -273,7 +291,7 @@ # create uuids for all nodes that might still need them # this changes the local nodes table! - if (my $changed_nodes = createNodeUUID()) + if (my $changed_nodes = NMIS::UUID::createNodeUUID()) { $NT = loadLocalNodeTable(); dbg("table Local Node reloaded after uuid updates",2); @@ -298,10 +316,7 @@ } dbg("all relevant tables loaded"); - my $debug_global = $C->{debug}; my $debug = $C->{debug}; - my $PIDFILE; - my $pid; # used for plotting major events on world map in 'Current Events' display $C->{netDNS} = 0; @@ -316,68 +331,8 @@ } } - runDaemons(); # start daemon processes + runDaemons(); # (re)start daemon processes - ### test if we are still running, or zombied, and cron will email somebody if we are - ### collects should not run past 5mins - if they do we have a problem - ### updates can run past 5 mins, BUT no two updates should run at the same time - ### for potentially frequent type=services we don't do any of these. - if ( $type eq 'collect' or $type eq "update") - { - # unrelated but also for collect and update only - @active_plugins = &load_plugins; - - # first find all other nmis collect processes - my $others = func::find_nmis_processes(type => $type, config => $C); - - # if this is a collect and if told to ignore running processes (ignore_running=1/t), - # then only warn about processes and don't shoot them. - # the same should be done if this is an interactive run with info or debug - if (($type eq "collect" and ( getbool($nvp{ignore_running}) - or $C->{debug} or $C->{info} )) - or ($type eq "update" and ($C->{debug} or $C->{info}))) - { - for my $pid (keys %{$others}) - { - logMsg("INFO ignoring old process $pid that is still running: $type, $others->{$pid}->{node}, started at ".returnDateStamp($others->{$pid}->{start})); - } - } - else - { - my $eventconfig = loadTable(dir => 'conf', name => 'Events'); - my $event = "NMIS runtime exceeded"; - my $thisevent_control = $eventconfig->{$event} || { Log => "true", Notify => "true", Status => "true"}; - - # if not told otherwise, shoot the others politely - for my $pid (keys %{$others}) - { - print STDERR "Error: killing old NMIS $type process $pid which has not finished!\n"; - logMsg("ERROR killing old NMIS $type process $pid which has not finished!"); - - kill("TERM",$pid); - - # and raise an event to inform the operator - unless told NOT to - # ie: either disable_nmis_process_events is set to true OR the event control Log property is set to false - if ((!defined $C->{disable_nmis_process_events} or !getbool($C->{disable_nmis_process_events}) - and getbool($thisevent_control->{Log}))) - { - # logging this event as the node name so it shows up as a problem with the node - logEvent(node => $others->{$pid}->{node}, - event => $event, - level => "Warning", - element => $others->{$pid}->{node}, - details => "Killed process $pid, $type of $others->{$pid}->{node}, started at " - .returnDateStamp($others->{$pid}->{start})); - } - } - if (keys %{$others}) # for the others to shut down cleanly - { - my $grace = 5; - logMsg("INFO sleeping for $grace seconds to let old NMIS processes clean up"); - sleep($grace); - } - } - } # the signal handler handles termination more-or-less gracefully, # and knows about critical sections @@ -412,143 +367,348 @@ my $maxruntime = defined($C->{max_child_runtime}) && $C->{max_child_runtime} > 0 ? $C->{max_child_runtime} : 0; - # don't run longer than X seconds for the main process, only if in non-thread mode or specific node - alarm($maxruntime) if ($maxruntime && (!$mthread or $node_select)); - - my @list_of_handled_nodes; # for any after_x_plugin() functions - if ($node_select eq "") - { - # operate on all nodes, sort the nodes so we get consistent polling cycles - # sort could be more sophisticated if we like, eg sort by core, dist, access or group - foreach my $onenode (sort keys %{$NT}) { - # This will allow debugging to be turned on for a - # specific node where there is a problem - if ( $onenode eq "$debug_watch" ) { - $debug = "true"; - } else { $debug = $debug_global; } - - # KS 16 Mar 02, implementing David Gay's requirement for deactiving - # a node, ie keep a node in nodes.csv but no collection done. - # also if $runGroup set, only do the nodes for that group. - if ( $runGroup eq "" or $NT->{$onenode}{group} eq $runGroup ) { - if ( getbool($NT->{$onenode}{active}) ) { - ++$nodecount; - push @list_of_handled_nodes, $onenode; - - # One process for each node until maxThreads is reached. - # This loop is entered only if the commandlinevariable mthread=true is used! - if ($mthread) - { - my $pid=fork; - if ( defined ($pid) and $pid==0) { + my (@list_of_handled_nodes, # for any after_x_plugin() functions + @todo_nodes, # for the actual update/polling work + @cand_nodes, + %whichflavours); # attempt smmp, wmi or both - # this will be run only by the child - if ($mthreadDebug) { - print "CHILD $$-> I am a CHILD with the PID $$ processing $onenode\n"; - } + # what to work on? one named node, or the nodes that are members of a given group or all nodes + # iff active and the polling policy agrees, that is... + @cand_nodes = $node_select? $node_select + : $runGroup? grep($_->{group} eq $runGroup, keys %$NT) : sort keys %$NT; - # don't run longer than X seconds - alarm($maxruntime) if ($maxruntime); - &$meth(name=>$onenode); - alarm(0) if ($maxruntime); - # all the work in this thread is done now this child will die. - if ($mthreadDebug) { - print "CHILD $$-> $onenode will now exit\n"; - } + # get the polling policies and translate into seconds (for rrd file options) + my $policies = loadTable(dir => 'conf', name => "Polling-Policy") || {}; + my %intervals = ( default => { ping=> 60, snmp => 300, wmi => 300 }); + # translate period specs X.Ys, A.Bm, etc. into seconds + for my $polname (keys %$policies) + { + next if (ref($policies->{$polname}) ne "HASH"); + for my $subtype (qw(snmp wmi ping)) + { + my $interval = $policies->{$polname}->{$subtype}; + if ($interval =~ /^\s*(\d+(\.\d+)?)([smhd])$/) + { + my ($rawvalue, $unit) = ($1, $3); + $interval = $rawvalue * ($unit eq 'm'? 60 : $unit eq 'h'? 3600 : $unit eq 'd'? 86400 : 1); + } + $intervals{$polname}->{$subtype} = $interval; # now in seconds + } + } - # killing child - exit 0; - } # end of child - else - { - # parent - my $others = func::find_nmis_processes(config => $C); - my $procs_now = 1 + scalar keys %$others; # the current process isn't returned - $maxprocs = $procs_now if $procs_now > $maxprocs; - } - } - else - { - # iterate over nodes in this process, if mthread is false - &$meth(name=>$onenode); - } - } #if active - else { - dbg("Skipping as $onenode is marked 'inactive'"); + # find all other nmis processes of the same type + my $otherprocesses = func::find_nmis_processes(type => $type, config => $C) + if ($type eq "update" or $type eq "collect"); # relevant only for these + my %problematic; + + if ($type eq "update" or $type eq "services") + { + @todo_nodes = grep(getbool($NT->{$_}->{active}), @cand_nodes); + } + else + { + # find out what nodes are due as per polling policy - also honor force, + # and any in-progress polling that hasn't finished yet... + my $now = time; + for my $maybe (@cand_nodes) + { + next if (ref($NT->{$maybe}) ne "HASH" or !getbool($NT->{$maybe}->{active})); + # save it back for the xyz-node file, and cgi-bin/network... + my $polname = ($NT->{$maybe}->{polling_policy} ||= "default"); + dbg("Node $maybe is using polling policy \"$polname\""); + + # unfortunately we require the nodeinfo data to make the candidate-or-not decision... + my $ninfo = loadNodeInfoTable($maybe); + + my $lastpolicy = $ninfo->{system}->{last_polling_policy}; + my $lastsnmp = $ninfo->{system}->{last_poll_snmp}; + my $lastwmi = $ninfo->{system}->{last_poll_wmi}; + + # that's it for completed polls - for in-progress uncompleted we need other time logic, + # overriding these markers from the active process' start time + my @isinprogress = grep($otherprocesses->{$_}->{node} && + $otherprocesses->{$_}->{node} eq $maybe, + keys %$otherprocesses); + map { $problematic{$maybe} = $_; } (@isinprogress); + + if (!getbool($nvp{force}) and @isinprogress) + { + # there should be at most one, we ignore any unexpected others + my $otherstart = $otherprocesses->{ $isinprogress[0] }->{start}; + dbg("Node $maybe: collect in progress, using process start $otherstart instead of last_poll markers"); + $lastsnmp = $lastwmi = $otherstart; + $lastpolicy = $polname; # and no policy change triggering either... + } + + # handle the case of a changed polling policy: move all rrd files + # out of the way, and poll now + # note that this does NOT work with non-standard common-database structures + if (defined($lastpolicy) && $lastpolicy ne $polname) + { + logMsg("Node $maybe is changing polling policy, from \"$lastpolicy\" to \"$polname\", due for polling at $now"); + my $lcnode = lc($maybe); + my $curdir = $C->{'database_root'}."/nodes/$lcnode"; + my $backupdir = "$curdir.policy-$lastpolicy.".time(); + + if (!-d $curdir) + { + logMsg("WARN Node $maybe doesn't have RRD files under $curdir!"); + } + else + { + rename($curdir,$backupdir) or logMsg("WARN failed to mv rrd files for $maybe: $!"); } - } #if runGroup - } # foreach $onenode + push @todo_nodes, $maybe; + $whichflavours{$maybe}->{wmi} = $whichflavours{$maybe}->{snmp} = 1; # and ignore the last-xyz markers + } + elsif (getbool($nvp{force})) + { + dbg("force is enabled, Node $maybe will be polled at $now"); + push @todo_nodes, $maybe; + $whichflavours{$maybe}->{wmi} = $whichflavours{$maybe}->{snmp} = 1; # and ignore the last-xyz markers + } + # nodes that have not been pollable since forever: run at most once hourly + elsif (!$ninfo->{system}->{nodeModel} or $ninfo->{system}->{nodeModel} eq "Model") + { + my $lasttry = $ninfo->{system}->{last_poll} // 0; + my $nexttry = ($lasttry && ($now - $lasttry) <= 30*86400)? ($lasttry + 3600 * 0.95) : $now; + dbg("Node $maybe has no valid nodeModel, never polled successfully, demoting to hourly check, last attempt $lasttry, next $nexttry"); + if ($nexttry <= $now) + { + push @todo_nodes, $maybe; + $whichflavours{$maybe}->{wmi} = $whichflavours{$maybe}->{snmp} = 1; + } + } + # logic for collect now or later: candidate if no past successful collect whatsoever, + # or if either of the two worked and was done long enough ago. + # + # if no history is known for a source, then disregard it for the now-or-later logic + # but DO enable it for trying! + # note that collect=false, i.e. ping-only nodes need to be excepted, + elsif (!defined($lastsnmp) && !defined($lastwmi) + && getbool($NT->{$maybe}->{collect})) + { + dbg("Node $maybe has neither last_poll_snmp nor last_poll_wmi, due for poll at $now"); + push @todo_nodes, $maybe; + $whichflavours{$maybe}->{wmi} = $whichflavours{$maybe}->{snmp} = 1; + } + else + { + # for collect false/pingonly nodes the single 'generic' collect run counts, + # and the 'snmp' policy is applied + if (!getbool($NT->{$maybe}->{collect})) + { + $lastsnmp = $ninfo->{system}->{last_poll} // 0; + dbg("Node $maybe is non-collecting, applying snmp policy to last check at $lastsnmp"); + } + + # accept delta-previous-now interval if it's at least 95% of the configured interval + # strict 100% would mean that we might skip a full interval when polling takes longer + my $nextsnmp = ($lastsnmp // 0) + $intervals{$polname}->{snmp} * 0.95; + my $nextwmi = ($lastwmi // 0) + $intervals{$polname}->{wmi} * 0.95; - # only do the child process cleanup if we have mthread enabled - if ($mthread) { - # cleanup - # wait this will block until children are done - 1 while wait != -1; + # only flavours which worked in the past contribute to the now-or-later logic + if ((defined($lastsnmp) && $nextsnmp <= $now ) + || (defined($lastwmi) && $nextwmi <= $now)) + { + dbg("Node $maybe is due for poll at $now, last snmp: ".($lastsnmp//"never") + .", last wmi: ".($lastwmi//"never") + . ", next snmp: ".($lastsnmp ? (($now - $nextsnmp)."s ago"):"n/a") + .", next wmi: ".($lastwmi? (($now - $nextwmi)."s ago"):"n/a")); + push @todo_nodes, $maybe; + + # but if we've decided on polling, then DO try flavours that have not worked in the past! + # nextwmi <= now also covers the case of undefined lastwmi... + $whichflavours{$maybe}->{wmi} = ($nextwmi <= $now); + $whichflavours{$maybe}->{snmp} = ($nextsnmp <= $now); + } + else + { + dbg("Node $maybe is NOT due for poll at $now, last snmp: ".($lastsnmp//"never") + .", last wmi: ".($lastwmi//"never") + . ", next snmp: ".($lastsnmp? $nextsnmp :"n/a") + .", next wmi: ".($lastwmi? $nextwmi :"n/a")); + } + } } } - else + + # anything to do? + if (!@todo_nodes) + { + info("Found no nodes due for $type."); + logMsg("Found no nodes due for $type."); + return; + } + + logMsg("INFO Selected nodes for $type: ".join(" ", sort @todo_nodes)); + $mthread = 0 if (@todo_nodes <= 1); # multiprocessing makes no sense with just one todo node + + # now perform process safety operations + # test if there are any collect processes running for any of the todo nodes + # for updates, just test + ### updates can run past 5 mins, BUT no two updates should run at the same time + ### for potentially frequent type=services we don't do any of these. + if ( $type eq 'collect' or $type eq "update") { - # specific node is given to work on, threading not relevant - if ( (my $node = checkNodeName($node_select))) { # ignore lc & uc - if ( getbool($NT->{$node}{active}) ) { - ++$nodecount; - push @list_of_handled_nodes, $node; - &$meth(name=>$node); + # unrelated but also for collect and update only + @active_plugins = &load_plugins; + + # if this is a collect and if told to ignore running processes (ignore_running=1/t), + # then only warn about processes and don't shoot them. + if ($type eq "collect" and getbool($nvp{ignore_running})) + { + for my $pid (keys %$otherprocesses) + { + logMsg("INFO ignoring old $type process $pid that is still running: $otherprocesses->{$pid}->{node}, started at ".returnDateStamp($otherprocesses->{$pid}->{start})); } - else { - dbg("Skipping as $node_select is marked 'inactive'"); + } + else + { + my $eventconfig = loadTable(dir => 'conf', name => 'Events'); + my $event = "NMIS runtime exceeded"; + my $thisevent_control = $eventconfig->{$event} || { Log => "true", Notify => "true", Status => "true"}; + + # if not told otherwise, shoot the others politely + my $needgrace; + for my $node (@todo_nodes) + { + my $pid = $problematic{$node}; + next if (!$pid); + + $needgrace = 1; + print STDERR "Error: killing old NMIS $type process $pid ($otherprocesses->{$pid}->{node}) which has not finished!\n" + if (getbool($C->{verbose_nmis_process_events})); + + logMsg("ERROR killing old NMIS $type process $pid ($otherprocesses->{$pid}->{node}) which has not finished!"); + kill("TERM",$pid); + + # and raise an event to inform the operator - unless told NOT to + # ie: either disable_nmis_process_events is set to true OR the event control Log property is set to false + if ((!defined $C->{disable_nmis_process_events} + or !getbool($C->{disable_nmis_process_events}) + and getbool($thisevent_control->{Log}))) + { + # logging this event as the node name so it shows up as a problem with the node + logEvent(node => $otherprocesses->{$pid}->{node}, + event => $event, + level => "Warning", + element => $otherprocesses->{$pid}->{node}, + details => "Killed process $pid, $type of $otherprocesses->{$pid}->{node}, started at " + .returnDateStamp($otherprocesses->{$pid}->{start})); + } + } + + if ($needgrace) # give the others a moment to shut down cleanly + { + my $grace = 2; + logMsg("INFO sleeping for $grace seconds to let old NMIS processes clean up"); + sleep($grace); } } - else { - print "\t Invalid node $node_select No node of that name!\n"; - return; + } + + for my $onenode (@todo_nodes) + { + ++$nodecount; + push @list_of_handled_nodes, $onenode; + + # One process per node, until maxThreads is reached (then block and wait) + if ($mthread) + { + my $pid=fork; + if ( defined ($pid) and $pid==0) + { + # this will be run only by the child + print "CHILD $$-> I am a CHILD with the PID $$ processing $onenode\n" + if ($mthreadDebug); + + # don't run longer than X seconds + alarm($maxruntime) if ($maxruntime); + my @methodargs = ( name => $onenode, + policy => $intervals{$NT->{$onenode}->{polling_policy} || "default"} ); + # try both flavours if force is on + push @methodargs, (wantsnmp => getbool($nvp{force}) || $whichflavours{$onenode}->{snmp}, + wantwmi => getbool($nvp{force}) || $whichflavours{$onenode}->{wmi}) + if ($type eq "collect"); # flavours irrelevant for update + &$meth(@methodargs); + + + # all the work in this thread is done now this child will die. + print "CHILD $$-> $onenode is done, exiting\n" + if ($mthreadDebug); + exit 0; + } # end of child + else + { + # parent + my $others = func::find_nmis_processes(config => $C); + my $procs_now = 1 + scalar keys %$others; # the current process isn't returned + $maxprocs = $procs_now if $procs_now > $maxprocs; + } + } + else + { + # just one node or no multi-processing wanted -> work in this process. + alarm($maxruntime); + my @methodargs = ( name => $onenode, + policy => $intervals{$NT->{$onenode}->{polling_policy} || "default"} ); + # try both flavours if force is on + push @methodargs, (wantsnmp => getbool($nvp{force}) || $whichflavours{$onenode}->{snmp}, + wantwmi => getbool($nvp{force}) || $whichflavours{$onenode}->{wmi},) + if ($type eq "collect"); # flavours irrelevant for update + &$meth(@methodargs); + alarm(0) if ($maxruntime); } } - alarm(0) if ($maxruntime && (!$mthread or $node_select)); + # outermost parent process: collects exit codes + if ($mthread) + { + print "PARENT $$-> waiting for child processes to complete...\n" + if ($mthreadDebug); + # wait blockingly until all worker children are done + 1 while wait != -1; + } - dbg("### continue normally ###"); my $collecttime = Time::HiRes::time(); - my $S; # on update prime the interface summary if ( $type eq "update" ) { - ### 2013-08-30 keiths, restructured to avoid creating and loading large Interface summaries getNodeAllInfo(); # store node info in /nmis-nodeinfo.xxxx - if ( !getbool($C->{disable_interfaces_summary}) ) { + if ( !getbool($C->{disable_interfaces_summary}) ) + { getIntfAllInfo(); # concatencate all the interface info in /nmis-interfaces.xxxx runLinks(); } } - # some collect post-processing, but only if running on all nodes - elsif ( $type eq "collect" and $node_select eq "" ) + # some collect post-processing + elsif ($type eq "collect") { - $S = Sys->new; # object nmis-system + $S = Sys->new; $S->init(); my $NI = $S->ndinfo; delete $NI->{database}; # remove pre-8.5.0 key as it's not used anymore - ### 2011-12-29 keiths, adding a general purpose master control thing, run reliably every poll cycle. - if ( getbool($C->{'nmis_master_poll_cycle'}) or !getbool($C->{'nmis_master_poll_cycle'},"invert") ) { + # do some masterly type things + if (!getbool($C->{'nmis_master_poll_cycle'},"invert") # if not false + && getbool($C->{server_master})) + { my $pollTimer = NMIS::Timing->new; dbg("Starting nmisMaster"); - nmisMaster() if getbool($C->{server_master}); # do some masterly type things. - - logMsg("Poll Time: nmisMaster, ". $pollTimer->elapTime()) if ( defined $C->{log_polling_time} and getbool($C->{log_polling_time})); - } - else { - dbg("Skipping nmisMaster with configuration 'nmis_master_poll_cycle' = $C->{'nmis_master_poll_cycle'}"); + nmisMaster(); + logMsg("Poll Time: nmisMaster, ". $pollTimer->elapTime()) if ( getbool($C->{log_polling_time})); } - if ( getbool($C->{'nmis_summary_poll_cycle'}) or !getbool($C->{'nmis_summary_poll_cycle'},"invert") ) { + # calculate and cache the summary stats + if (!getbool($C->{'nmis_summary_poll_cycle'},"invert") # if not false + && getbool($C->{cache_summary_tables})) + { dbg("Starting nmisSummary"); - nmisSummary() if getbool($C->{cache_summary_tables}); # calculate and cache the summary stats - } - else { - dbg("Skipping nmisSummary with configuration 'nmis_summary_poll_cycle' = $C->{'nmis_summary_poll_cycle'}"); + nmisSummary(); } dbg("Starting runMetrics"); @@ -574,7 +734,7 @@ runEscalate(); # nmis collect runtime, process counts and save - my $D; + my $D = {}; $D->{collect}{value} = $collecttime - $starttime; $D->{collect}{option} = 'gauge,0:U'; $D->{total}{value} = Time::HiRes::time() - $starttime; @@ -685,7 +845,9 @@ sub catch_zap { # do NOT lock the logfile logMsg("INFO Process $$ ($0) was killed by signal $rs", 1); - die "Process $$ ($0) was killed by signal $rs\n"; + die "Process $$ ($0) was killed by signal $rs\n" + if (getbool($C->{verbose_nmis_process_events})); + exit 0; } } @@ -718,8 +880,8 @@ sub doUpdate # create the update lock now. my $lockHandle = createPollLock(type => "update", conf => $C->{conf}, node => $name); - # lets change our name, so a ps will report who we are - iff not debugging. - $0 = "nmis-".$C->{conf}."-update-$name" if (!$C->{debug}); + # lets change our name, so a ps will report who we are + $0 = "nmis-".$C->{conf}."-update-$name"; my $S = Sys->new; # create system object # loads old node info (unless force is active), and the DEFAULT(!) model (always!), @@ -742,6 +904,19 @@ sub doUpdate my $NI = $S->ndinfo; my $NC = $S->ndcfg; + # look for any current outages with options.nostats set, + # and set a marker in nodeinfo so that updaterrd writes nothing but 'U' + my $outageres = NMIS::check_outages(sys => $S, time => time); + if (!$outageres->{success}) + { + logMsg("ERROR Failed to check outage status for $name: $outageres->{error}"); + } + else + { + $NI->{admin}->{outage_nostats} = ( List::Util::any { ref($_->{options}) eq "HASH" + && $_->{options}->{nostats} } @{$outageres->{current}} )? 1 : 0; + } + if (!getbool($nvp{force})) { $S->readNodeView; # from prev. run, but only if force isn't active @@ -804,7 +979,7 @@ sub doUpdate } } $S->close; # close snmp session if one is open - $NI->{system}{lastUpdatePoll} = time(); + $NI->{system}{last_update} = time(); } my $reachdata = runReach(sys=>$S, delayupdate => 1); # don't let it make the rrd update, we want to add updatetime! @@ -945,11 +1120,27 @@ sub doServices info("================================"); info("Starting services, node $name"); - # lets change our name, so a ps will report who we are, iff not debugging - $0 = "nmis-".$C->{conf}."-services-$name" if (!$C->{debug}); + # lets change our name, so a ps will report who we are + $0 = "nmis-".$C->{conf}."-services-$name"; my $S = Sys->new; $S->init(name => $name); + my $NI = $S->ndinfo; + + # look for any current outages with options.nostats set, + # and set a marker in nodeinfo so that updaterrd writes nothing but 'U' + my $outageres = NMIS::check_outages(sys => $S, time => time); + if (!$outageres->{success}) + { + logMsg("ERROR Failed to check outage status for $name: $outageres->{error}"); + } + else + { + $NI->{admin}->{outage_nostats} = ( List::Util::any { ref($_->{options}) eq "HASH" + && $_->{options}->{nostats} } + @{$outageres->{current}}) ? 1:0; + } + dbg("node=$name ".join(" ", map { "$_=".$S->ndinfo->{system}->{$_} } (qw(group nodeType nodedown snmpdown wmidown)))); $S->readNodeView; # init does not load the node view, but runservices updates view data! @@ -964,20 +1155,24 @@ sub doServices return; } +# sub doCollect { my %args = @_; - my $name = $args{name}; + my ($name,$wantsnmp,$wantwmi, $policy) = @args{"name","wantsnmp","wantwmi","policy"}; + my $starttime = time; my $pollTimer = NMIS::Timing->new; info("================================"); - info("Starting collect, node $name"); + info("Starting collect, node $name, want SNMP: ".($wantsnmp?"yes":"no") + .", want WMI: ".($wantwmi?"yes":"no")); - # Check for both update and collect LOCKs - if ( existsPollLock(type => "update", conf => $C->{conf}, node => $name) ) { - print STDERR "Error: running collect but update lock exists for $name which has not finished!\n"; - logMsg("WARNING running collect but update lock exists for $name which has not finished!"); + # Check for both update and collect locks, but complain only for collect lock + # with polling frequently, an existing update lock is very very likely + if ( existsPollLock(type => "update", conf => $C->{conf}, node => $name) ) + { + logMsg("INFO skipping collect for node $name because of active update lock"); return; } if ( existsPollLock(type => "collect", conf => $C->{conf}, node => $name) ) @@ -989,11 +1184,16 @@ sub doCollect # create the poll lock now. my $lockHandle = createPollLock(type => "collect", conf => $C->{conf}, node => $name); - # lets change our name, so a ps will report who we are - iff not debugging - $0 = "nmis-".$C->{conf}."-collect-$name" if (!$C->{debug}); + # lets change our name, so a ps will report who we are + $0 = "nmis-".$C->{conf}."-collect-$name"; - my $S = Sys->new; # create system object - if (! $S->init(name=>$name) ) # init will usually load node info data, model etc, returns 1 if _all_ is ok + my $S = Sys->new; # create system object, does next to nothing + # init will usually load node info data, model etc, returns 1 if _all_ is ok + if (! $S->init(name=>$name, + snmp => $wantsnmp, + wmi => $wantwmi, + policy => $policy, + debug => $C->{debug} ) ) { dbg("Sys init for $name failed: ".join(", ", map { "$_=".$S->status->{$_} } (qw(error snmp_error wmi_error)))); @@ -1011,8 +1211,22 @@ sub doCollect my $NC = $S->ndcfg; $S->readNodeView; # s->init does NOT load that, but we need it as we're overwriting some view info + # look for any current outages with options.nostats set, + # and set a marker in nodeinfo so that updaterrd writes nothing but 'U' + my $outageres = NMIS::check_outages(sys => $S, time => time); + if (!$outageres->{success}) + { + logMsg("ERROR Failed to check outage status for $name: $outageres->{error}"); + } + else + { + $NI->{admin}->{outage_nostats} = ( List::Util::any { ref($_->{options}) eq "HASH" + && $_->{options}->{nostats} } + @{$outageres->{current}} ) ? 1 : 0; + } + # run an update if no update poll time is known - if ( !exists($NI->{system}{lastUpdatePoll}) or !$NI->{system}{lastUpdatePoll}) + if ( !exists($NI->{system}{last_update}) or !$NI->{system}{last_update}) { info("no cached node data available, running an update now"); doUpdate(name=>$name); @@ -1042,7 +1256,8 @@ sub doCollect } # returns 1 if one or more sources have worked, also updates snmp/wmi down states in nodeinfo - my $updatewasok = updateNodeInfo(sys=>$S); + # and sets the relevant last_poll_xyz markers + my $updatewasok = updateNodeInfo(sys=>$S, time_marker => $starttime); my $curstate = $S->status; # updatenodeinfo does NOT disable faulty sources! # was snmp ok? should we bail out? note that this is interpreted to apply to ALL sources being down simultaneously, @@ -1224,18 +1439,8 @@ sub runPing info("Starting $S->{name} ($host) with timeout=$timeout retries=$retries packet=$packet"); - # fixme: invalid condition, root is generally NOT required for ping anymore! - if ($<) - { - # not root and update, assume called from www interface - $pingresult = 100; - dbg("SKIPPING Pinging as we are NOT running with root privileges"); - } - else - { - ( $ping_min, $ping_avg, $ping_max, $ping_loss) = ext_ping($host, $packet, $retries, $timeout ); - $pingresult = defined $ping_min ? 100 : 0; # ping_min is undef if unreachable. - } + ( $ping_min, $ping_avg, $ping_max, $ping_loss) = ext_ping($host, $packet, $retries, $timeout ); + $pingresult = defined $ping_min ? 100 : 0; # ping_min is undef if unreachable. } # at this point ping_{min,avg,max,loss} and pingresult are all set @@ -1274,7 +1479,7 @@ sub runPing # info for web page $V->{system}{lastUpdate_value} = returnDateStamp(); $V->{system}{lastUpdate_title} = 'Last Update'; - $NI->{system}{lastUpdateSec} = time(); + $NI->{system}{last_poll} = time(); } else { @@ -1604,6 +1809,9 @@ sub getNodeInfo $V->{system}{netType_value} = $NI->{system}{netType}; $V->{system}{netType_title} = 'Net'; + # override the sysLocation title to indicate that it is coming from SNMP sysLocation + $V->{system}{sysLocation_title} = 'SNMP Location'; + # get the current ip address if the host property was a name if ((my $addr = resolveDNStoAddr($NI->{system}{host}))) { @@ -3132,9 +3340,13 @@ sub updateNodeInfo my $result; my $exit = 1; + my $time_marker = $args{time_marker} || time; + info("Starting Update Node Info, node $S->{name}"); - # clear the node reset indication from the last run - $NI->{system}->{node_was_reset}=0; + # clear any node reset indication from the last run + delete $NI->{admin}->{node_was_reset}; + # node reset marker is now under admin + delete $NI->{system}->{node_was_reset}; # save what we need now for check of this node my $sysObjectID = $NI->{system}{sysObjectID}; @@ -3145,24 +3357,36 @@ sub updateNodeInfo # this returns 0 iff none of the possible/configured sources worked, sets details my $loadsuccess = $S->loadInfo(class=>'system', model=>$model); + # polling policy needs saving regardless of success/failure + $NI->{system}->{last_polling_policy} = $NC->{node}->{polling_policy} || 'default'; + # handle dead sources, raise appropriate events my $curstate = $S->status; for my $source (qw(snmp wmi)) { - # ok if enabled and no errors - if ($curstate->{"${source}_enabled"} && !$curstate->{"${source}_error"}) - { - my $sourcename = uc($source); - $RI->{"${source}result"} = 100; - HandleNodeDown(sys=>$S, type => $source, up => 1, details => "$sourcename ok"); - } - # not ok if enabled and error - elsif ($curstate->{"${source}_enabled"} && $curstate->{"${source}_error"}) + if ($curstate->{"${source}_enabled"}) { - HandleNodeDown(sys=>$S, type => $source, details => $curstate->{"${source}_error"} ); - $RI->{"${source}result"} = 0; + # ok if enabled and no errors + if (!$curstate->{"${source}_error"}) + { + my $sourcename = uc($source); + $RI->{"${source}result"} = 100; + HandleNodeDown(sys=>$S, type => $source, up => 1, details => "$sourcename ok"); + + # record a _successful_ collect for the different sources, + # the collect now-or-later logic needs that, not just attempted at time x + $NI->{system}->{"last_poll_$source"} = $time_marker; + + } + # not ok if enabled and error + else + { + HandleNodeDown(sys=>$S, type => $source, details => $curstate->{"${source}_error"} ); + $RI->{"${source}result"} = 0; + + } } - # don't care about nonenabled sources, sys won't touch them nor set errors, RI stays whatever it was + # we don't care about nonenabled sources, sys won't touch them nor set errors, RI stays whatever it was } if ($loadsuccess) @@ -3184,13 +3408,17 @@ sub updateNodeInfo # a new control to minimise when interfaces are added, # if disabled {custom}{interface}{ifNumber} eq "false" then don't run getIntfInfo when intf changes my $doIfNumberCheck = ( exists($S->{mdl}->{custom}) && exists($S->{mdl}->{custom}->{interface}) # do not autovivify - && !getbool($S->{mdl}->{custom}->{interface}->{ifNumber})); + && getbool($S->{mdl}->{custom}->{interface}->{ifNumber})); if ($doIfNumberCheck and $ifNumber != $NI->{system}{ifNumber}) { logMsg("INFO ($NI->{system}{name}) Number of interfaces changed from $ifNumber now $NI->{system}{ifNumber}"); getIntfInfo(sys=>$S); # get new interface table } + elsif (!$doIfNumberCheck and $ifNumber != $NI->{system}{ifNumber}) + { + logMsg("INFO ($NI->{system}{name}) Ignoring the number of interfaces changed from $ifNumber now $NI->{system}{ifNumber} based on custom interface modelling"); + } my $interface_max_number = $C->{interface_max_number} ? $C->{interface_max_number} : 5000; if ($ifNumber > $interface_max_number ) { @@ -3218,9 +3446,9 @@ sub updateNodeInfo details => "Old_sysUpTime=$sysUpTime New_sysUpTime=$NI->{system}{sysUpTime}", context => { type => "node" } ); - # now stash this info in the node info object, to ensure we insert one set of U's into the rrds + # now stash this info in the node info object, to ensure we insert ONE set of U's into the rrds # so that no spikes appear in the graphs - $NI->{system}{node_was_reset}=1; + $NI->{admin}->{node_was_reset}=1; } $V->{system}{sysUpTime_value} = $NI->{system}{sysUpTime}; @@ -3228,7 +3456,7 @@ sub updateNodeInfo $V->{system}{lastUpdate_value} = returnDateStamp(); $V->{system}{lastUpdate_title} = 'Last Update'; - $NI->{system}{lastUpdateSec} = time(); + $NI->{system}{last_poll} = $time_marker; # get and apply any nodeconf override if such exists for this node my $node = $NI->{system}{name}; @@ -3345,15 +3573,20 @@ sub processAlerts { method => "Alert", type => $alert->{type}, - property => $alert->{test}, + property => $alert->{test}, # fixme inconsistent! thresholds use the threshold name here... event => $alert->{event}, - index => undef, #$args{index}, + index => $alert->{index}, level => $tresult, status => $statusResult, - element => $alert->{ds}, + element => $alert->{ds}, # for simple alerts (==type test) this is the sole useful context value => $alert->{value}, - updated => time() - } + updated => time(), + # contect for finding the originator in the model + source => $alert->{source}, # snmp or wmi + section => $alert->{section}, + # name does not exist for simple alerts, let's synthesize it from ds + name => $alert->{alert} || $alert->{ds}, + }; } } @@ -4718,27 +4951,51 @@ sub runServer $S->{reach}{cpu} = mean(@{$S->{reach}{cpuList}}); } + # keep the old storage data around a bit longer, as fallback if loadinfo fails + my $oldstorage = $NI->{storage}; delete $NI->{storage}; - if ($M->{storage} ne '') { - # get storage info + + if ($M->{storage} ne '') + { my $disk_cnt = 1; my $storageIndex = $SNMP->getindex('hrStorageIndex'); my $hrFSMountPoint = undef; my $hrFSRemoteMountPoint = undef; my $fileSystemTable = undef; - foreach my $index (keys %{$storageIndex}) { - if ($S->loadInfo(class=>'storage',index=>$index,model=>$model)) { - my $D = $NI->{storage}{$index}; + + foreach my $index (keys %{$storageIndex}) + { + # this saves any retrieved info under ni->{storage} + my $wasloadable = $S->loadInfo(class=>'storage',index=>$index,model=>$model); + if (!$wasloadable) + { + logMsg("ERROR failed to retrieve storage info for index=$index, continuing with OLD data!"); + $NI->{storage}->{$index} ||= $oldstorage->{$index}; # and restore the old data, better than nothing + } + else + { + my $D = $NI->{storage}{$index}; # new data + + ### 2017-02-13 keiths, handling larger disk sizes by converting to an unsigned integer + $D->{hrStorageSize} = unpack("I", pack("i", $D->{hrStorageSize})); + $D->{hrStorageUsed} = unpack("I", pack("i", $D->{hrStorageUsed})); + info("storage $D->{hrStorageDescr} Type=$D->{hrStorageType}, Size=$D->{hrStorageSize}, Used=$D->{hrStorageUsed}, Units=$D->{hrStorageUnits}"); - if (($M->{storage}{nocollect}{Description} ne '' and $D->{hrStorageDescr} =~ /$M->{storage}{nocollect}{Description}/ ) - or $D->{hrStorageSize} <= 0) { + + if (($M->{storage}{nocollect}{Description} ne '' + and $D->{hrStorageDescr} =~ /$M->{storage}{nocollect}{Description}/ ) + or $D->{hrStorageSize} <= 0) + { delete $NI->{storage}{$index}; - } else { + } + else + { if ( $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.4' # hrStorageFixedDisk or $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.10' # hrStorageNetworkDisk - ) { - undef %Val; + ) + { + my %Val; my $hrStorageType = $D->{hrStorageType}; $Val{hrDiskSize}{value} = $D->{hrStorageUnits} * $D->{hrStorageSize}; $Val{hrDiskUsed}{value} = $D->{hrStorageUnits} * $D->{hrStorageUsed}; @@ -4749,7 +5006,8 @@ sub runServer push(@{$S->{reach}{diskList}},$diskUtil); $D->{hrStorageDescr} =~ s/,/ /g; # lose any commas. - if ((my $db = updateRRD(sys=>$S,data=>\%Val,type=>"hrdisk",index=>$index))) { + if ((my $db = updateRRD(sys=>$S,data=>\%Val,type=>"hrdisk",index=>$index))) + { $NI->{graphtype}{$index}{hrdisk} = "hrdisk"; $D->{hrStorageType} = 'Fixed Disk'; $D->{hrStorageIndex} = $index; @@ -4761,7 +5019,8 @@ sub runServer logMsg("ERROR updateRRD failed: ".getRRDerror()); } - if ( $hrStorageType eq '1.3.6.1.2.1.25.2.1.10' ) { + if ( $hrStorageType eq '1.3.6.1.2.1.25.2.1.10' ) + { # only get this snmp once if we need to, and created an named index. if ( not defined $fileSystemTable ) { $hrFSMountPoint = $SNMP->getindex('hrFSMountPoint'); @@ -4779,7 +5038,7 @@ sub runServer } ### 2014-08-28 keiths, fix for VMware Real Memory as HOST-RESOURCES-MIB::hrStorageType.7 = OID: HOST-RESOURCES-MIB::hrStorageTypes.20 elsif ( $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.2' or $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.20') { # Memory - undef %Val; + my %Val; $Val{hrMemSize}{value} = $D->{hrStorageUnits} * $D->{hrStorageSize}; $Val{hrMemUsed}{value} = $D->{hrStorageUnits} * $D->{hrStorageUsed}; @@ -4799,7 +5058,7 @@ sub runServer } # in net-snmp, virtualmemory is used as type for both swap and 'virtual memory' (=phys + swap) elsif ( $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.3') { # VirtualMemory - undef %Val; + my %Val; my ($itemname,$typename)= ($D->{hrStorageDescr} =~ /Swap/i)? (qw(hrSwapMem hrswapmem)):(qw(hrVMem hrvmem)); @@ -4829,7 +5088,7 @@ sub runServer elsif ( $D->{hrStorageType} eq '1.3.6.1.2.1.25.2.1.1' # StorageOther and $D->{hrStorageDescr} =~ /^(Memory buffers|Cached memory)$/i) { - undef %Val; + my %Val; my ($itemname,$typename) = ($D->{hrStorageDescr} =~ /^Memory buffers$/i)? (qw(hrBufMem hrbufmem)) : (qw(hrCacheMem hrcachemem)); @@ -4849,6 +5108,7 @@ sub runServer logMsg("ERROR updateRRD failed: ".getRRDerror()); } } + # storage type not recognized? else { delete $NI->{storage}{$index}; @@ -4866,14 +5126,16 @@ sub runServer $S->{reach}{disk} = mean(@{$S->{reach}{diskList}}); } - # convert date value to readable string - sub snmp2date { - my @tt = unpack("C*", shift ); - return eval(($tt[0] *256) + $tt[1])."-".$tt[2]."-".$tt[3].",".$tt[4].":".$tt[5].":".$tt[6].".".$tt[7]; - } info("Finished"); } # end runServer +# converts date value to readable string +sub snmp2date +{ + my @tt = unpack("C*", shift ); + return (($tt[0] *256) + $tt[1]) ."-".$tt[2]."-".$tt[3].",".$tt[4].":".$tt[5].":".$tt[6].".".$tt[7]; +} + #========================================================================================= @@ -4901,7 +5163,6 @@ sub runServices my $cpu; my $memory; - my $msg; my %services; # hash to hold snmp gathered service status. my %status; # hash to collect generic/non-snmp service status @@ -4918,8 +5179,8 @@ sub runServices if ($snmp_allowed and getbool($NT->{$node}{active}) and getbool($NT->{$node}{collect}) - and grep(exists($ST->{$_}) && $ST->{$_}->{Service_Type} eq "service", - split(/,/, $NT->{$NI->{system}{name}}->{services})) ) + and grep(ref($ST->{$_}) eq "HASH" && $ST->{$_}->{Service_Type} eq "service", + split(/,/, $NT->{ $NI->{system}{name} }->{services})) ) { info("node has SNMP services to check"); @@ -5001,10 +5262,17 @@ sub runServices # specific services to be tested are saved in a list - these are rrd-collected, too. # note that this also covers the snmp-based services my $didRunServices = 0; - for my $service ( split /,/ , $NT->{$NI->{system}{name}}{services} ) + for my $service ( split /,/ , $NT->{ $NI->{system}{name} }->{services} ) { + my $thisservice = $ST->{$service}; # check for invalid service table data - next if ($service eq '' or $service =~ /n\/a/i or $ST->{$service}{Service_Type} =~ /n\/a/i); + next if (!$service + or $service =~ m!^n/a$!i + or ref($thisservice) ne "HASH" + or $thisservice->{Service_Type} =~ m!^n/a$!i); + + my ($name, $servicename, $servicetype) + = @{$thisservice}{"Name","Service_Name","Service_Type"}; # are we supposed to run this service now? # load the service status and check the last run time @@ -5014,8 +5282,9 @@ sub runServices && $previous{$C->{server_name}}->{$service}->{$node})? $previous{$C->{server_name}}->{$service}->{$node}->{last_run} : 0; - my $serviceinterval = $ST->{$service}->{Poll_Interval} || 300; # 5min - my $msg = "Service $service on $node (interval \"$serviceinterval\") last ran at ".returnDateStamp($lastrun).", "; + my $serviceinterval = $thisservice->{Poll_Interval} || 300; + my $msg = "Service $name on $node (interval \"$serviceinterval\") last ran at " + . returnDateStamp($lastrun).", "; if ($serviceinterval =~ /^\s*(\d+(\.\d+)?)([mhd])$/) { my ($rawvalue, $unit) = ($1, $3); @@ -5042,14 +5311,13 @@ sub runServices } # make sure that the rrd heartbeat is suitable for the service interval! my $serviceheartbeat = ($serviceinterval * 3) || 300*3; - $didRunServices = 1; # make sure this gets reinitialized for every service! my $gotMemCpu = 0; my %Val; - info("Checking service_type=$ST->{$service}{Service_Type} name=$ST->{$service}{Name} service_name=$ST->{$service}{Service_Name}"); + info("Checking service name=$name type=$servicetype service_name=$servicename"); # clear global hash each time around as this is used to pass results to rrd update my $ret = 0; @@ -5061,12 +5329,14 @@ sub runServices # DNS: lookup whatever Service_name contains (fqdn or ip address), # nameserver being the host in question - if ( $ST->{$service}{Service_Type} eq "dns" ) { + if ( $servicetype eq "dns" ) + { use Net::DNS; - my $lookfor = $ST->{$service}{Service_Name}; + my $lookfor = $servicename; + if (!$lookfor) { - dbg("Service_Name for $NI->{system}{host} must be a FQDN or IP address"); - logMsg("ERROR, ($NI->{system}{name}) Service_name for service=$service must contain an FQDN or IP address"); + dbg("Service_Name for service $service must be a FQDN or IP address"); + logMsg("ERROR, ($NI->{system}{name}) Service_name for service=$service must be a FQDN or IP address"); next; } my $res = Net::DNS::Resolver->new; @@ -5090,10 +5360,9 @@ sub runServices # now the 'port' service checks, which rely on nmap # - tcp would be easy enough to do with a plain connect, but udp accessible-or-closed needs extra smarts - elsif ( $ST->{$service}{Service_Type} eq "port" ) + elsif ( $servicetype eq "port" ) { - $msg = ''; - my ( $scan, $port) = split ':' , $ST->{$service}{Port}; + my ( $scan, $port) = split ':' , $thisservice->{Port}; my $nmap = ( $scan =~ /^udp$/i ? "nmap -sU --host_timeout 3000 -p $port -oG - $NI->{system}{host}" : "nmap -sT --host_timeout 3000 -p $port -oG - $NI->{system}{host}" ); @@ -5105,11 +5374,9 @@ sub runServices logMsg($errmsg); info($errmsg); } - while () - { - $msg .= $_; # this retains the newlines - } + my $nmap_out = join("", ); close(NMAP); + my $exitcode = $?; # if the pipe close doesn't wait until the child is gone (which it may do...) # then wait and collect explicitely @@ -5122,21 +5389,21 @@ sub runServices logMsg("ERROR, NMAP ($nmap) returned exitcode ".($exitcode >> 8). " (raw $exitcode)"); info("$nmap returned exitcode ".($exitcode >> 8). " (raw $exitcode)"); } - if ($msg =~ /Ports: $port\/open/) + if ($nmap_out =~ /Ports: $port\/open/) { $ret = 1; - info("NMAP reported success for port $port: $msg"); - logMsg("INFO, NMAP reported success for port $port: $msg") if ($C->{debug} or $C->{info}); + info("NMAP reported success for port $port: $nmap_out"); + logMsg("INFO, NMAP reported success for port $port: $nmap_out") if ($C->{debug} or $C->{info}); } else { $ret = 0; - info("NMAP reported failure for port $port: $msg"); - logMsg("INFO, NMAP reported failure for port $port: $msg") if ($C->{debug} or $C->{info}); + info("NMAP reported failure for port $port: $nmap_out"); + logMsg("INFO, NMAP reported failure for port $port: $nmap_out") if ($C->{debug} or $C->{info}); } } # now the snmp services - but only if snmp is on - elsif ( $ST->{$service}{Service_Type} eq "service" + elsif ( $servicetype eq "service" and getbool($NT->{$node}{collect})) { # only do the SNMP checking if and when you are supposed to! @@ -5149,8 +5416,8 @@ sub runServices and !getbool($NI->{system}{snmpdown}) and !getbool($NI->{system}{nodedown}) ) ) { - my $wantedprocname = $ST->{$service}{Service_Name}; - my $parametercheck = $ST->{$service}{Service_Parameters}; + my $wantedprocname = $servicename; + my $parametercheck = $thisservice->{Service_Parameters}; if (!$wantedprocname and !$parametercheck) { @@ -5189,7 +5456,7 @@ sub runServices $cpu = 0; $memory = 0; $gotMemCpu = 1; - logMsg("INFO, service $ST->{$service}{Name} is down, " + logMsg("INFO, service $name is down, " .(@matchingpids? "only non-running processes" : "no matching processes")); } @@ -5207,7 +5474,7 @@ sub runServices # dbg("cpu: ".join(" + ",map { $services{$_}->{hrSWRunPerfCPU} } (@livingpids)) ." = $cpu"); # dbg("memory: ".join(" + ",map { $services{$_}->{hrSWRunPerfMem} } (@livingpids)) ." = $memory"); - info("INFO, service $ST->{$service}{Name} is up, ".scalar(@livingpids)." running process(es)"); + info("INFO, service $name is up, ".scalar(@livingpids)." running process(es)"); } } else { @@ -5216,36 +5483,37 @@ sub runServices } } # now the sapi 'scripts' (similar to expect scripts) - elsif ( $ST->{$service}{Service_Type} eq "script" ) + elsif ( $servicetype eq "script" ) { - ### lets do the user defined scripts - my $scripttext; - if (!open(F, "$C->{script_root}/$service")) - { - dbg("ERROR, can't open script file for $service: $!"); - } - else - { - $scripttext=join("",); - close(F); + # OMK-3237, use sensible and non-clashing config source: + # now service_name sets the script file name, temporarily falling back to $service + my $scriptfn = "$C->{script_root}/". $servicename || $service; + if (!open(F, $scriptfn)) + { + dbg("ERROR, can't open script file for $service: $!"); + } + else + { + my $scripttext=join("",); + close(F); - my $timeout = ($ST->{$service}->{Max_Runtime} > 0)? - $ST->{$service}->{Max_Runtime} : 3; + my $timeout = ($thisservice->{Max_Runtime} > 0)? + $thisservice->{Max_Runtime} : 3; - ($ret,$msg) = sapi($NI->{system}{host}, - $ST->{$service}{Port}, - $scripttext, - $timeout); - dbg("Results of $service is $ret, msg is $msg"); - } + ($ret,my $sapi_out) = sapi($NI->{system}{host}, + $thisservice->{Port}, + $scripttext, + $timeout); + # the sapi thing can pass back raw protocol data... + dbg("Results of service $name ($servicetype, $servicename) is $ret, msg is '$sapi_out'"); + } } # 'real' scripts, or more precisely external programs # which also covers nagios plugins - https://nagios-plugins.org/doc/guidelines.html - elsif ( $ST->{$service}{Service_Type} =~ /^(program|nagios-plugin)$/ ) + elsif ( $servicetype =~ /^(program|nagios-plugin)$/ ) { $ret = 0; - my $svc = $ST->{$service}; - if (!$svc->{Program} or !-x $svc->{Program}) + if (!$thisservice->{Program} or !-x $thisservice->{Program}) { info("ERROR, service $service defined with no working Program to run!"); logMsg("ERROR service $service defined with no working Program to run!"); @@ -5253,17 +5521,17 @@ sub runServices } # exit codes and output handling differ - my $flavour_nagios = ($svc->{Service_Type} eq "nagios-plugin"); + my $flavour_nagios = ($servicetype eq "nagios-plugin"); # check the arguments (if given), substitute node.XYZ values my $finalargs; - if ($svc->{Args}) + if ($thisservice->{Args}) { - $finalargs = $svc->{Args}; + $finalargs = $thisservice->{Args}; # don't touch anything AFTER a node.xyz, and only subst if node.xyz is the first/only thing, # or if there's a nonword char before node.xyz. $finalargs =~ s/(^|\W)(node\.([a-zA-Z0-9_-]+))/$1$NI->{system}{$3}/g; - dbg("external program args were $svc->{Args}, now $finalargs"); + dbg("external program args were $thisservice->{Args}, now $finalargs"); } my $programexit = 0; @@ -5275,23 +5543,25 @@ sub runServices eval { my @responses; - my $svcruntime = defined($svc->{Max_Runtime}) && $svc->{Max_Runtime} > 0? - $svc->{Max_Runtime} : 0; + my $svcruntime = defined($thisservice->{Max_Runtime}) && $thisservice->{Max_Runtime} > 0? + $thisservice->{Max_Runtime} : 0; local $SIG{ALRM} = sub { die "alarm\n"; }; alarm($svcruntime) if ($svcruntime); # setup execution timeout # run given program with given arguments and possibly read from it - # program is disconnected from stdin; stderr goes into a tmpfile and is collected separately for diagnostics + # program is disconnected from stdin; stderr goes into a tmpfile + # and is collected separately for diagnostics + my $stderrsink = POSIX::tmpnam(); # good enough, no atomic open required - dbg("running external program '$svc->{Program} $finalargs', " - .(getbool($svc->{Collect_Output})? "collecting":"ignoring")." output"); - $pid = open(PRG,"$svc->{Program} $finalargs $stderrsink |"); + dbg("running external program '$thisservice->{Program} $finalargs', " + .(getbool($thisservice->{Collect_Output})? "collecting":"ignoring")." output"); + $pid = open(PRG,"$thisservice->{Program} $finalargs $stderrsink |"); if (!$pid) { alarm(0) if ($svcruntime); # cancel any timeout - info("ERROR, cannot start service program $svc->{Program}: $!"); - logMsg("ERROR: cannot start service program $svc->{Program}: $!"); + info("ERROR, cannot start service program $thisservice->{Program}: $!"); + logMsg("ERROR: cannot start service program $thisservice->{Program}: $!"); } else { @@ -5308,13 +5578,13 @@ sub runServices open(UNWANTED, $stderrsink); my $badstuff = join("", ); chomp($badstuff); - logMsg("WARNING: Service program $svc->{Program} returned unexpected error output: \"$badstuff\""); - info("Service program $svc->{Program} returned unexpected error output: \"$badstuff\""); + logMsg("WARNING: Service program $thisservice->{Program} returned unexpected error output: \"$badstuff\""); + info("Service program $thisservice->{Program} returned unexpected error output: \"$badstuff\""); close(UNWANTED); } unlink($stderrsink); - if (getbool($svc->{Collect_Output})) + if (getbool($thisservice->{Collect_Output})) { # nagios has two modes of output *sigh*, |-as-newline separator and real newlines # https://nagios-plugins.org/doc/guidelines.html#PLUGOUTPUT @@ -5374,7 +5644,8 @@ sub runServices } # units: s,us,ms = seconds, % percentage, B,KB,MB,TB bytes, c a counter - if ($value_with_unit =~ /^([0-9\.]+)(s|ms|us|%|B|KB|MB|GB|TB|c)$/) + # negative values are possible, e.g. ntp offset... + if ($value_with_unit =~ /^([0-9\.-]+)(s|ms|us|%|B|KB|MB|GB|TB|c)$/) { my ($numericval,$unit) = ($1,$2); dbg("performance data for label '$k': raw value '$value_with_unit'"); @@ -5416,11 +5687,11 @@ sub runServices if ($@ and $@ eq "alarm\n") { - kill($pid); # get rid of the service tester, it ran over time... - info("ERROR, service program $svc->{Program} exceeded Max_Runtime of $svc->{Max_Runtime}s, terminated."); - logMsg("ERROR: service program $svc->{Program} exceeded Max_Runtime of $svc->{Max_Runtime}s, terminated."); + kill('TERM', $pid); # get rid of the service tester, it ran over time... + info("ERROR, service program $thisservice->{Program} exceeded Max_Runtime of $thisservice->{Max_Runtime}s, terminated."); + logMsg("ERROR: service program $thisservice->{Program} exceeded Max_Runtime of $thisservice->{Max_Runtime}s, terminated."); $ret=0; - kill("SIGKILL",$pid); + kill("KILL",$pid); } else { @@ -5445,7 +5716,7 @@ sub runServices } else { - logMsg("WARNING: service program $svc->{Program} terminated abnormally!"); + logMsg("WARNING: service program $thisservice->{Program} terminated abnormally!"); $ret = 0; } } @@ -5462,15 +5733,14 @@ sub runServices # let external programs set the responsetime if so desired $responsetime = $timer->elapTime if (!defined $responsetime); $status{$service}->{responsetime} = $responsetime; - $status{$service}->{name} = $ST->{$service}{Name}; # same as $service + $status{$service}->{name} = $name; # normally the same as $service # external programs return 0..100 directly, rest has 0..1 - my $serviceValue = ( $ST->{$service}{Service_Type} =~ /^(program|nagios-plugin)$/ )? + my $serviceValue = ( $servicetype =~ /^(program|nagios-plugin)$/ )? $ret : $ret*100; $status{$service}->{status} = $serviceValue; - #logMsg("Updating $node Service, $ST->{$service}{Name}, $ret, gotMemCpu=$gotMemCpu"); - $V->{system}{"${service}_title"} = "Service $ST->{$service}{Name}"; + $V->{system}{"${service}_title"} = "Service $name"; $V->{system}{"${service}_value"} = $serviceValue == 100 ? 'running' : $serviceValue > 0? "degraded" : 'down'; $V->{system}{"${service}_color"} = $serviceValue == 100 ? 'white' : $serviceValue > 0? "orange" : 'red'; @@ -5485,49 +5755,53 @@ sub runServices # let's raise or clear service events based on the status if ( $snmpdown ) # only set IFF this is an snmp-based service AND snmp is broken/down. { - dbg("$ST->{$service}{Service_Type} $ST->{$service}{Name} is not checked, snmp is down"); + dbg("service $service ($servicename) not checked: snmp is down"); $V->{system}{"${service}_value"} = 'unknown'; $V->{system}{"${service}_color"} = 'gray'; $serviceValue = ''; } elsif ( $serviceValue == 100 ) # service is fully up { - dbg("$ST->{$service}{Service_Type} $ST->{$service}{Name} is available ($serviceValue)"); + dbg("service $service ($servicename) is available ($serviceValue)"); # all perfect, so we need to clear both degraded and down events - checkEvent(sys=>$S, event=>"Service Down", level=>"Normal", element => $ST->{$service}{Name}, + checkEvent(sys=>$S, event=>"Service Down", level=>"Normal", + element => $name, details=> ($status{$service}->{status_text}||"") ); - checkEvent(sys=>$S, event=>"Service Degraded", level=>"Warning", element => $ST->{$service}{Name}, + checkEvent(sys=>$S, event=>"Service Degraded", level=>"Warning", + element => $name, details=> ($status{$service}->{status_text}||"") ); } elsif ($serviceValue > 0) # service is up but degraded { - dbg("$ST->{$service}{Service_Type} $ST->{$service}{Name} is degraded ($serviceValue)"); + dbg("service $service ($servicename) is degraded ($serviceValue)"); # is this change towards the better or the worse? # we clear the down (if one exists) as it's not totally dead anymore... - checkEvent(sys=>$S, event=>"Service Down", level=>"Fatal", element => $ST->{$service}{Name}, + checkEvent(sys=>$S, event=>"Service Down", level=>"Fatal", + element => $name, details=> ($status{$service}->{status_text}||"") ); # ...and create a degraded notify(sys => $S, event => "Service Degraded", level => "Warning", - element => $ST->{$service}{Name}, + element => $name, details=> ($status{$service}->{status_text}||""), context => { type => "service" } ); } else # Service is down { - dbg("$ST->{$service}{Service_Type} $ST->{$service}{Name} is down"); + dbg("service $service ($servicename) is down"); # clear the degraded event # but don't just eventDelete, so that no state engines downstream of nmis get confused! - checkEvent(sys=>$S, event=>"Service Degraded", level=>"Warning", element => $ST->{$service}{Name}, + checkEvent(sys=>$S, event=>"Service Degraded", level=>"Warning", + element => $name, details=> ($status{$service}->{status_text}||"") ); # and now create a down event notify(sys=>$S, event=>"Service Down", level => "Fatal", - element=>$ST->{$service}{Name}, + element => $name, details=> ($status{$service}->{status_text}||""), context => { type => "service" } ); } @@ -5602,13 +5876,17 @@ sub runServices # now update the per-service status file $status{$service}->{service} ||= $service; # service and node are part of the fn, but possibly mangled... $status{$service}->{node} ||= $node; - $status{$service}->{name} ||= $ST->{$service}->{Name}; # that can be all kinds of stuff, depending on the service type + $status{$service}->{name} ||= $name; # generally the same as $service + # save our server name with the service status, for distributed setups $status{$service}->{server} = $C->{server_name}; - # AND ensure the service has a uuid, a recreatable V5 one from config'd namespace+server+service+node's uuid - $status{$service}->{uuid} = NMIS::UUID::getComponentUUID($C->{server_name}, $service, $NI->{system}->{uuid}); - $status{$service}->{description} ||= $ST->{$service}->{Description}; # but that's free-form + # AND ensure the service has a uuid, a recreatable V5 one from config'd + # namespace+server+service+node's uuid + $status{$service}->{uuid} = NMIS::UUID::getComponentUUID($C->{server_name}, + $service, $NI->{system}->{uuid}); + + $status{$service}->{description} ||= $thisservice->{Description}; # but that's free-form $status{$service}->{last_run} ||= time; my $error = saveServiceStatus(service => $status{$service}); @@ -5714,43 +5992,48 @@ sub runAlerts my $level=$CA->{$sect}{$alrt}{level}; - # check the thresholds - # fixed thresholds to fire at level not one off, and threshold falling was just wrong. + # check the thresholds, in appropriate order + # report normal if below level for warning (for threshold-rising, or above for threshold-falling) + # debug-warn and ignore a level definition for 'Normal' - overdefined and buggy! if ( $CA->{$sect}{$alrt}{type} =~ /^threshold/ ) { - if ( $CA->{$sect}{$alrt}{type} eq "threshold-rising" ) { - if ( $test_value <= $CA->{$sect}{$alrt}{threshold}{Normal} ) { - $test_result = 0; - $level = "Normal"; - } - else { - my @levels = qw(Fatal Critical Major Minor Warning); - foreach my $lvl (@levels) { - if ( $test_value >= $CA->{$sect}{$alrt}{threshold}{$lvl} ) { - $test_result = 1; - $level = $lvl; - last; - } - } - } - } - elsif ( $CA->{$sect}{$alrt}{type} eq "threshold-falling" ) { - if ( $test_value >= $CA->{$sect}{$alrt}{threshold}{Normal} ) { - $test_result = 0; - $level = "Normal"; - } - else { - my @levels = qw(Warning Minor Major Critical Fatal); - foreach my $lvl (@levels) { - if ( $test_value <= $CA->{$sect}{$alrt}{threshold}{$lvl} ) { - $test_result = 1; - $level = $lvl; - last; - } - } - } - } - info("alert result: Normal=$CA->{$sect}{$alrt}{threshold}{Normal} test_value=$test_value test_result=$test_result level=$level",2); + dbg("Warning: ignoring deprecated threshold level Normal for alert \"$alrt\"!") + if (defined($CA->{$sect}->{$alrt}->{threshold}->{'Normal'})); + + my @matches; + # to disable particular levels, set their value to the same as the desired one + # comparison code looks for all matches and picks the worst/highest severity match + if ( $CA->{$sect}{$alrt}{type} eq "threshold-rising" ) + { + # from not-bad to very-bad, for skipping skippable levels + @matches = grep( $test_value >= $CA->{$sect}->{$alrt}->{threshold}->{$_}, + (qw(Warning Minor Major Critical Fatal))); + } + elsif ( $CA->{$sect}{$alrt}{type} eq "threshold-falling" ) + { + # from not-bad to very bad, again, same rationale + @matches = grep($test_value <= $CA->{$sect}->{$alrt}->{threshold}->{$_}, + (qw(Warning Minor Major Critical Fatal))); + } + else + { + logMsg("ERROR: skipping unknown alert type \"$CA->{$sect}{$alrt}{type}\"!"); + next; + } + + # no matches for above threshold (for rising)? then "Normal" + # ditto for matches below threshold (for falling) + if (!@matches) + { + $level = "Normal"; + $test_result = 0; + } + else + { + $level = $matches[-1]; # we want the highest severity/worst matching one + $test_result = 1; + } + info("alert result: test_value=$test_value test_result=$test_result level=$level",2); } # and now save the result, for both tests and thresholds (source of level is the only difference) @@ -5881,8 +6164,7 @@ sub HandleNodeDown # performs various node health status checks # optionally! updates rrd # args: sys, delayupdate (default: 0), -# if delayupdate is set, this DOES NOT update the -#type 'health' rrd (to be done later, with total polltime) +# if delayupdate is set, this DOES NOT update the type 'health' rrd (to be done later, with total polltime) # returns: reachability data (hashref) sub runReach { @@ -6007,8 +6289,29 @@ sub runReach } elsif ( $reach{availability} eq "" ) { $reach{availability} = $intAvailValueWhenDown; } - my ($outage,undef) = outageCheck(node=>$S->{node},time=>time()); - dbg("Outage for $S->{name} is $outage"); + # the REAL node name is required, NOT the lowercased variant! + my ($outage,undef) = outageCheck(node => $S->{name}, time=>time()); + dbg("Outage status for $S->{name} is ". ($outage || "")); + $reach{outage} = $outage eq "current"? 1 : 0; + # raise a planned outage event, or close it + if ($outage eq "current") + { + notify(sys=>$S, + event=> "Planned Outage Open", + level => "Warning", + element => "", + details=> "", # filled in by notify + context => { type => "node" }, ); + + } + else + { + checkEvent(sys=>$S, event=>"Planned Outage Open", + level=> "Normal", + element=> "", + details=> "" ); + } + # Health should actually reflect a combination of these values # ie if response time is high health should be decremented. if ( $pingresult == 100 and $snmpresult == 100 ) { @@ -6278,7 +6581,13 @@ sub runReach } $reach{health} = ($reach{health} > 100) ? 100 : $reach{health}; + + # massaged result with rrd metadata my %reachVal; + + $reachVal{outage} = { value => $reach{outage}, + option => "gauge,0:1" }; + $reachVal{reachability}{value} = $reach{reachability}; $reachVal{availability}{value} = $reach{availability}; $reachVal{responsetime}{value} = $reach{responsetime}; @@ -7015,7 +7324,7 @@ sub runEscalate # if an planned outage is in force, keep writing the start time of any unack event to the current start time # so when the outage expires, and the event is still current, we escalate as if the event had just occured my ($outage,undef) = outageCheck(node=>$thisevent->{node},time=>time()); - dbg("Outage for $thisevent->{node} is $outage"); + dbg("Outage status for $thisevent->{node} is ". ($outage || "")); if ( $outage eq "current" and getbool($thisevent->{ack},"invert") ) { $thisevent->{startdate} = time(); @@ -8160,33 +8469,31 @@ sub printCrontab PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin ###################################################### -# NMIS8 Config -###################################################### -# Run Full Statistics Collection -*/5 * * * * $usercol $C->{''}/bin/nmis.pl type=collect mthread=true -# ###################################################### -# Optionally run a more frequent Services-only Collection -# */3 * * * * $usercol $C->{''}/bin/nmis.pl type=services mthread=true +# Run (selective) Statistics and Service Status Collection often +* * * * * $usercol $C->{''}/bin/nmis.pl type=collect mthread=true ; $C->{''}/bin/nmis.pl type=services mthread=true + ###################################################### -# Run Summary Update every 2 minutes -*/2 * * * * $usercol $C->{''}/bin/nmis.pl type=summary -##################################################### -# Run the interfaces 4 times an hour with Thresholding on!!! -# if threshold_poll_cycle is set to false, then enable cron based thresholding -#*/5 * * * * $usercol nice $C->{''}/bin/nmis.pl type=threshold +# Run Summary Update every 5 minutes +*/5 * * * * $usercol $C->{''}/bin/nmis.pl type=summary + + ###################################################### # Run the update once a day 30 20 * * * $usercol nice $C->{''}/bin/nmis.pl type=update mthread=true + ###################################################### -# Log Rotation is now handled with /etc/logrotate.d/nmis, which -# the installer offers to setup using install/logrotate*.conf -# +# Run the thresholding four times an hour +# only necessary if threshold_poll_cycle is set to false +#*/15 * * * * $usercol nice $C->{''}/bin/nmis.pl type=threshold + # backup configuration, models and crontabs once a day, and keep 30 backups 22 8 * * * $usercol $C->{''}/admin/config_backup.pl $C->{''} 30 -################################################## + +###################################################### # purge old files every few days 2 2 */3 * * $usercol $C->{''}/bin/nmis.pl type=purge -######################################## + +###################################################### # Save the Reports, Daily Monthly Weekly 9 0 * * * $usercol $C->{''}/bin/run-reports.pl day all 9 1 * * 0 $usercol $C->{''}/bin/run-reports.pl week all @@ -8402,7 +8709,7 @@ sub checkArgs [conf=] Optional alternate configuation file in conf directory [node=] Run operations on a single node; [group=] Run operations on all nodes in the named group; - [force=true|false] Makes an update operation run from scratch, without optimisations + [force=true|false] Makes operations run from scratch, ignoring interval policies [debug=true|false|0-9] default=false - Show debugging information [rmefile=] RME file to import. [mthread=true|false] default=$C->{nmis_mthread} - Enable Multithreading or not; @@ -8503,8 +8810,9 @@ sub doSummaryBuild # check if threshold level available, thresholdname must be equal to type if (exists $M->{threshold}{name}{$tp}) { - ($stshlth{$NI->{system}{nodeType}}{$nd}{$nm}{$i}{level},undef,undef) = - getThresholdLevel(sys=>$S,thrname=>$tp,stats=>$sts,index=>$i); + # fixme: errors are ignored... + my $goodies = getThresholdLevel(sys=>$S,thrname=>$tp,stats=>$sts,index=>$i); + $stshlth{$NI->{system}{nodeType}}{$nd}{$nm}{$i}{level} = $goodies->{level}; } # save values foreach my $stsname (@{$M->{summary}{statstype}{$tp}{sumname}{$nm}{stsname}}) { @@ -8527,8 +8835,9 @@ sub doSummaryBuild # check if threshold level available, thresholdname must be equal to type if (exists $M->{threshold}{name}{$tp}) { - ($stshlth{$NI->{system}{nodeType}}{$nd}{"${tp}_level"},undef,undef) = - getThresholdLevel(sys=>$S,thrname=>$tp,stats=>$sts,index=>''); + # fixme errors are ignored + my $goodies = getThresholdLevel(sys=>$S,thrname=>$tp,stats=>$sts,index=>''); + $stshlth{$NI->{system}{nodeType}}{$nd}{"${tp}_level"} = $goodies->{level}; } foreach my $nm (keys %{$M->{summary}{statstype}{$tp}{sumname}}) { @@ -8761,6 +9070,7 @@ sub doThreshold my $thisevent_control = $events_config->{$eventKey} || { Log => "true", Notify => "true", Status => "true"}; # if this is an alert and it is older than 1 full poll cycle, delete it from status. + # fixme: this logic is broken with variable polling if ( $S->{info}{status}{$statusKey}{updated} < time - 500) { delete $S->{info}{status}{$statusKey}; } @@ -8805,7 +9115,12 @@ sub doThreshold if ( defined $C->{log_polling_time} and getbool($C->{log_polling_time})) { my $polltime = $pollTimer->elapTime(); - logMsg("Poll Time: $polltime"); + if ( $name ) { + logMsg("Poll Time: $name, $polltime"); + } + else { + logMsg("Poll Time: $polltime"); + } } func::update_operations_stamp(type => "threshold", start => $starttime, stop => Time::HiRes::time()) if ($type eq "threshold"); # not if part of collect @@ -8924,11 +9239,12 @@ sub runThrHld } } - my ($level,$value,$thrvalue,$reset) = getThresholdLevel(sys=>$S, - thrname=>$nm, - stats=>$stats, - index=>$index, - item=>$item); + my $goodies = getThresholdLevel(sys=>$S, + thrname=>$nm, + stats=>$stats, + index=>$index, + item=>$item); + # get 'Proactive ....' string of Model my $event = $S->parseString(string=>$M->{threshold}{name}{$nm}{event}, index=>$index); @@ -8957,20 +9273,30 @@ sub runThrHld } thresholdProcess(sys=>$S, - type=>$type, # crucial for event context + type=>$type, # crucial for context event=>$event, - level=>$level, + level => $goodies->{level}, element=>$element, # crucial for context details=>$details, - value=>$value, - thrvalue=>$thrvalue, - reset=>$reset, + value => $goodies->{level_value}, + thrvalue => $goodies->{level_threshold}, + reset=> $goodies->{reset}, thrname=>$nm, # crucial for context index=>$index, # crucial for context - class=>$class); # crucial for context + class=>$class, # crucial for context + level_select => $goodies->{level_select}, + ); } } +# args: sys, thrname, stats, index, item; pretty much all required +# returns hashref with keys: +# level (=textual level), +# level_value (= numeric value) +# level_threshold (=comparison value that caused this level to be chosen), +# level_select (=default or key of the threshold level set that was chosen), +# reset (=?), +# error (n/a if things work). sub getThresholdLevel { my %args = @_; @@ -8983,43 +9309,60 @@ sub getThresholdLevel my $index = $args{index}; my $item = $args{item}; - my $val; - my $level; - my $thrvalue; + my $val; # hash of level cutoffs to compare against + my $level; # text + my $thrvalue; # numeric value + my $level_select; dbg("Start threshold=$thrname, index=$index item=$item"); - # find subsection with threshold values in Model + # look for applicable level selection set in model + return { error => "no threshold=$thrname entry found in Model=$NI->{system}{nodeModel}" } + if (ref($M->{threshold}{name}{$thrname}) ne "HASH" + or ref($M->{threshold}{name}{$thrname}{select}) ne "HASH" + or !keys %{$M->{threshold}{name}{$thrname}{select}}); # at least ONE level must be there + + # which level selector works for this thing? check in order of the level_select keys my $T = $M->{threshold}{name}{$thrname}{select}; - foreach my $thr (sort {$a <=> $b} keys %{$T}) { + foreach my $thr (sort {$a <=> $b} keys %{$T}) + { next if $thr eq 'default'; # skip now the default values - if (($S->parseString(string=>"($T->{$thr}{control})?1:0",index=>$index,item=>$item))){ + if (($S->parseString(string=>"($T->{$thr}{control})?1:0",index=>$index,item=>$item))) + { $val = $T->{$thr}{value}; + $level_select = $thr; dbg("found threshold=$thrname entry=$thr"); last; } } - # if not found and there are default values available get this now - if ($val eq "" and $T->{default}{value} ne "") { + # if nothing found and there are default values available, use these + if (!defined($val) and $T->{default}{value} ne "") + { $val = $T->{default}{value}; - dbg("found threshold=$thrname entry=default"); + $level_select = "default"; + dbg("found threshold=$thrname entry=default"); } - if ($val eq "") { - logMsg("ERROR, no threshold=$thrname entry found in Model=$NI->{system}{nodeModel}"); - return; + # still no luck? eror out + if (!defined($val)) + { + logMsg("ERROR, $thrname in Model=$NI->{system}{nodeModel} has no select!"); + return { error => "$thrname in Model=$NI->{system}{nodeModel} has no select!" }; } - my $value; # value of doSummary() + my $value; # numeric value of doSummary() my $reset = 0; # item is the attribute name of summary stats of Model $value = $stats->{$M->{threshold}{name}{$thrname}{item}} if $index eq ""; $value = $stats->{$index}{$M->{threshold}{name}{$thrname}{item}} if $index ne ""; dbg("threshold=$thrname, item=$M->{threshold}{name}{$thrname}{item}, value=$value"); - # check unknow value + # check unknown/nonnumeric value, treat it as normal if ($value =~ /NaN/i) { - dbg("INFO, illegal value $value, skipped"); - return ("Normal",$value,$reset); + dbg("INFO, illegal value $value, skipped."); + return { level => "Normal", + reset => $reset, + level_select => $level_select, + level_value => $value }; } ### all zeros policy to disable thresholding - match and return 'normal' @@ -9033,7 +9376,9 @@ sub getThresholdLevel and defined $val->{major} and defined $val->{critical} and defined $val->{fatal}) { - return ("Normal",$value,$reset); + return { level => "Normal", level_value => $value, + level_select => $level_select, + reset => $reset }; } # Thresholds for higher being good and lower bad @@ -9064,12 +9409,24 @@ sub getThresholdLevel elsif ( $value >= $val->{critical} and $value < $val->{fatal} ) { $level = "Critical"; $thrvalue = $val->{critical}; } elsif ( $value >= $val->{fatal} ) { $level = "Fatal"; $thrvalue = $val->{fatal}; } } - if ( $level eq "") { + + # fixme: why is level normal returned if the threshold config is broken?? + if (!defined $level) + { logMsg("ERROR no policy found, threshold=$thrname, value=$value, node=$S->{name}, model=$NI->{system}{nodeModel} section threshold"); - $level = "Normal"; + + return { error => "no policy found, threshold=$thrname, value=$value, node=$S->{name}, model=$NI->{system}{nodeModel} section threshold", + level => "Normal", + level_value => $value, + level_select => $level_select, + reset => $reset }; } dbg("result threshold=$thrname, level=$level, value=$value, thrvalue=$thrvalue, reset=$reset"); - return ($level,$value,$thrvalue,$reset); + return { level => $level, + level_value => $value, + level_threshold => $thrvalue, + reset => $reset, + level_select => $level_select }; } sub thresholdProcess @@ -9117,17 +9474,19 @@ sub thresholdProcess $statusKey = "$args{thrname}--$index--$args{class}" if defined $args{class} and $args{class}; + # save the threshold in the status thingy $S->{info}{status}{$statusKey} = { method => "Threshold", - type => $args{type}, - property => $args{thrname}, + type => $args{type}, # context + property => $args{thrname}, # context, but fixme: why called property? event => $args{event}, index => $args{index}, level => $args{level}, + level_select => $args{level_select}, # for context: which level select set was used status => $statusResult, element => $args{element}, value => $args{value}, - updated => time() + updated => time(), } } } diff --git a/bin/nmis.sh b/bin/nmis.sh index a2e3dda..838d0ea 100755 --- a/bin/nmis.sh +++ b/bin/nmis.sh @@ -253,7 +253,7 @@ fi if [ "$2" = "collect" ] then - $nmis type=collect "$node" info=true $DEBUG $MODEL + $nmis type=collect "$node" info=true force=true $DEBUG $MODEL exit 0 fi diff --git a/bin/schedule_outage.pl b/bin/schedule_outage.pl deleted file mode 100755 index 0d5aec4..0000000 --- a/bin/schedule_outage.pl +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/perl -# -# Copyright (C) Opmantek Limited (www.opmantek.com) -# -# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM -# -# This file is part of Network Management Information System ("NMIS"). -# -# NMIS is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# NMIS 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with NMIS (most likely in a file named LICENSE). -# If not, see -# -# For further information on NMIS or for a license other than GPL please see -# www.opmantek.com or email contact@opmantek.com -# -# User group details: -# http://support.opmantek.com/users/ -# -# ***************************************************************************** -use strict; -use FindBin; -use lib "$FindBin::Bin/../lib"; -use NMIS; -use func; -use File::Basename; -use POSIX; -use Data::Dumper; - -# arguments: node=name or group=name, start=hhmm, end=hhmm, comment=text, -# verbose=0/1 -my %a = getArguements(@ARGV); - -my $outages = loadTable(dir => "conf", name => "Outage"); -my $nodes = loadNodeTable; - -die "usage: ".basename($0) - ." {node|group}=name start=HH:MM end=HH:MM [options] - comment=text: sets comment text for outage - verbose={0|1}: verbose output\n\n" - if (!$a{start} || !$a{end} - || !( $a{group} xor $a{node} ) - || $a{start} !~ /^\d+:\d+$/ || $a{end} !~ /^\d+:\d+$/ ); - -die "unknown node \"$a{node}\" given, aborting.\n" - if ($a{node} && !$nodes->{$a{node}}); - -my @now = localtime; -my @hm = split(/:/, $a{start}); - -die "start time \"$a{start}\" unparseable\n" - if ($hm[0]>=24 || $hm[1]>=60); - -my $begtime = POSIX::mktime(0,$hm[1],$hm[0],@now[3..5]); -@hm = split(/:/, $a{end}); - -die "start time \"$a{start}\" unparseable\n" - if ($hm[0]>=24 || $hm[1]>=60); - -my $endtime = POSIX::mktime(0,$hm[1],$hm[0],@now[3..5]); - -# if the endtime is earlier than the begin, add one day to end -if ( $endtime < $begtime ) -{ - $endtime += 86400; -} -# if the time specified is earlier than now, add one day -if ( $begtime < time ) -{ - $begtime += 86400; - $endtime += 86400; -} - -my @candidates = $a{node}? ($a{node}) - : (grep { $nodes->{$_}->{group} eq $a{group} } keys %{$nodes}); - -die "group \"$a{group}\" has no members, aborting.\n" if (!@candidates); - - -print "Outage window is from ".localtime($begtime)." to ". - localtime($endtime)."\n" if ($a{verbose}); - -for my $node (@candidates) -{ - $outages->{ join("-",$node,$begtime,$endtime) } = { - node => $node, - start => $begtime, - end => $endtime, - change => $a{comment} }; - - print "Applying to node $node\n" - if ($a{verbose}); -} - -writeTable(dir=> "conf", name => "Outage", data => $outages); -exit 0; - diff --git a/bin/traplog.pl b/bin/traplog.pl index 376921d..67000c7 100755 --- a/bin/traplog.pl +++ b/bin/traplog.pl @@ -1,6 +1,6 @@ #!/usr/bin/perl # -# Copyright 1999-2014 Opmantek Limited (www.opmantek.com) +# Copyright 1999-2017 Opmantek Limited (www.opmantek.com) # # ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM # @@ -28,80 +28,105 @@ # # ***************************************************************************** # -# Intentionally left distant from NMIS Code, as we want it to run +# Intentionally left distant from NMIS Code, as we want it to run # really fast with minimal use statements. # +our $VERSION="1.1.0"; + use strict; use FindBin; use lib "$FindBin::Bin/../lib"; use Socket; +use Getopt::Std; +use POSIX qw(); -my $trapfilter = qr/foobardiddly/; +my %options; +getopts("nf:t:", \%options) or die "Usage: $0 [-n] [-f filterregex] [-t targetfile]\n-n: disable dns lookups\-f regex: suppress traps matching this regex\n-t targetfile: write to this file, defualt: ../logs/nmis8/trap.log\n\n"; -# allow a logfile to be explicitely specified as first/only argument, -# but fall back to the 'usual' location -my $filename = $ARGV[0] || "$FindBin::Bin/../logs/trap.log"; +my $filename = $options{t} || "$FindBin::RealBin/../logs/trap.log"; +my $trapfilter = $options{f}? qr/$options{f}/ : undef; # snmptrapd feeds us, one per line, this: the hostname and 'ip # address' of the sending party, and the var bindings in the form of # oid space value. -# +# # note that: the ip address is not just the raw ip address, but a # connection string of the form 'UDP: [1.2.3.4]:33608->[5.6.7.8]', # where 1.2.3.4 is the other party and 5.6.7.8 is this box. # +# newer snmptrapd versions (5.7 etc) provide a different connection string, +# with ports included: 'UDP: [192.168.88.253]:50177->[192.168.88.7]:162' +# +# note: with traplogd running with -n (no dns), BOTH hostname and ipaddress lines +# are of the connection string format! +# # note also that the sending party may NOT be the originator if the # trap was forwarded, but merely indicates the last hop. it is # therefore necessary to check the variable # SNMP-COMMUNITY-MIB::snmpTrapAddress.0 as well, which holds the # originating agent's address. -# + my @buffer; my $hostname = ; my $ipaddress = ; chomp ($hostname, $ipaddress); -# Traps received without DNS PTR are coming as hostname -#2015-04-11T09:32:13 UDP: [192.168.1.249]:57047->[192.168.1.7] SNMPv2-MIB::sysUpTime.0=38:13:55:01.68 SNMPv2-MIB::snmpTrapOID.0=CISCO-CONFIG-MAN-MIB::ciscoConfigManEvent ....... -if ( $hostname eq "" and $ipaddress =~ /\[(\d+\.\d+\.\d+\.\d+)\]/ ) { - $hostname = $1; -} +# Traps received without DNS PTR can come in as hostname +# 2015-04-11T09:32:13 UDP: [192.168.1.249]:57047->[192.168.1.7] SNMPv2-MIB::sysUpTime.0=38:13:55:01.68 SNMPv2-MIB::snmpTrapOID.0=... +# furthermore, traps received with -n are coming in with both hostname +# and ipaddress set to the 'connection string' -$hostname = escapeHTML($hostname); -$ipaddress = escapeHTML($ipaddress); +if ( ($hostname eq "" or $hostname =~ /^UDP:\s*/ ) + and $ipaddress =~ /^UDP:\s*\[(\d+\.\d+\.\d+\.\d+)\]/ ) +{ + my $addr = $hostname = $1; # address is better than raw string + my $newhostname = ($options{n}? undef : gethostbyaddr(inet_aton($addr), + AF_INET)); + if (defined $newhostname) + { + $hostname = $newhostname; + $ipaddress = $addr; + } +} -# the remainder is all variables -while (my $line = ) +# the remainder is all variables +while (my $line = ) { chomp $line; $line = escapeHTML($line); - my ($varname,$rest) = split(/\s+/,$line,2); + my ($varname,$rest) = split(/\s+/,$line,2); # the one and only variable we're specially interested in: if the trap - # originator doesn't match what snmptrapd reports, then we replace - # the hostname with the trap originator's hostname (if we can find one) + # originator address doesn't match what snmptrapd reported as address, + # then we replace the address and the hostname with the trap originator's + # hostname (if we can find one) if ($varname eq "SNMP-COMMUNITY-MIB::snmpTrapAddress.0" - and $ipaddress !~ /^UDP:\s*\[$rest\]/) + and $ipaddress !~ /^(UDP:\s*\[$rest\].+|$rest$)/) { - my $addrbin = inet_aton($rest); - my $newhostname = gethostbyaddr($addrbin, AF_INET); - if (defined $newhostname) - { - $hostname = $newhostname; - $ipaddress = $rest; - } + my $addrbin = inet_aton($rest); + $hostname = $rest; # hostname being ip address is better than nothing + my $newhostname = ($options{n}? undef : gethostbyaddr($addrbin, AF_INET)); + if (defined $newhostname) + { + $hostname = $newhostname; + $ipaddress = $rest; + } } push @buffer,"$varname=$rest"; } +$hostname = escapeHTML($hostname); +$ipaddress = escapeHTML($ipaddress); + my $out = join("\t",$hostname,$ipaddress,@buffer); -if ( $out !~ /$trapfilter/ ) { - # Open output file for sending stuff to - open (DATA, ">>$filename") || die "Cannot open the file $filename: $!\n"; - print DATA &returnDateStamp."\t$out\n"; - close(DATA); +# save the output if it's not filtered +if ( !defined($trapfilter) || $out !~ $trapfilter ) +{ + open (DATA, ">>$filename") || die "Cannot open the file $filename: $!\n"; + print DATA &returnDateStamp."\t$out\n"; + close(DATA); } exit 0; @@ -121,35 +146,20 @@ sub escapeHTML return $input; } -#Function which returns the time -sub returnDateStamp { +# Function which returns the time, iso8601-formatted, NON-locale-capable +sub returnDateStamp +{ my $time = shift; - if ( $time == 0 ) { $time = time; } - my $SEP = "T"; - my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst)=localtime($time); - # A Y2.07K problem - if ($year > 70) { $year=$year+1900; } - else { $year=$year+2000; } - #Increment Month! - ++$mon; - if ($mon<10) {$mon = "0$mon";} - if ($mday<10) {$mday = "0$mday";} - if ($hour<10) {$hour = "0$hour";} - if ($min<10) {$min = "0$min";} - if ($sec<10) {$sec = "0$sec";} - - # Do some sums to calculate the time date etc 2 days ago - $wday=('Sun','Mon','Tue','Wed','Thu','Fri','Sat')[$wday]; - - return "$year-$mon-$mday$SEP$hour:$min:$sec"; + $time ||= time; + + return POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime($time)); } # ***************************************************************************** # Copyright (C) Opmantek Limited (www.opmantek.com) # This program comes with ABSOLUTELY NO WARRANTY; -# This is free software licensed under GNU GPL, and you are welcome to +# This is free software licensed under GNU GPL, and you are welcome to # redistribute it under certain conditions; see www.opmantek.com or email # contact@opmantek.com # ***************************************************************************** - diff --git a/cgi-bin/config.pl b/cgi-bin/config.pl index 89c81e2..bdca18a 100755 --- a/cgi-bin/config.pl +++ b/cgi-bin/config.pl @@ -29,43 +29,47 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -# Auto configure to the /lib +use strict; +our $VERSION="8.6.2G"; + use FindBin; use lib "$FindBin::Bin/../lib"; -# -use strict; +use URI::Escape; +use CGI qw(:standard *table *Tr *td *form *Select *div); +use Net::IP; + use NMIS; use func; use csv; use DBfunc; -use URI::Escape; - -use CGI qw(:standard *table *Tr *td *form *Select *div); +use Auth; my $q = new CGI; # This processes all parameters passed via GET and POST my $Q = $q->Vars; # values in hash -my $C; +my $C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}); + +die "failed to load configuration!\n" if (!$C or ref($C) ne "HASH" or !keys %$C); -if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth # this cgi script defaults to widget mode ON my $wantwidget = exists $Q->{widget}? !getbool($Q->{widget}, "invert") : 1; my $widget = $wantwidget ? "true" : "false"; -# Before going any further, check to see if we must handle -# an authentication login or logout request - -# NMIS Authentication module -use Auth; - my $headeropts = {type=>'text/html',expires=>'now'}; my $AU = Auth->new(conf => $C); # Auth::Auth::new will reap init values from NMIS::config if ($AU->Require) { exit 0 unless $AU->loginout(type=>$Q->{auth_type},username=>$Q->{auth_username}, - password=>$Q->{auth_password},headeropts=>$headeropts) ; + password=>$Q->{auth_password},headeropts=>$headeropts) ; +} +else +{ + # that's the command line/debugger scenario, where we assume a full admin + $AU->SetUser("nmis"); } # $AU->CheckAccess, will send header and display message denying access if fails. @@ -77,25 +81,25 @@ #====================================================================== # select function - -# what shall we do - if ($Q->{act} eq 'config_nmis_menu') { displayConfig(); } elsif ($Q->{act} eq 'config_nmis_add') { addConfig(); } elsif ($Q->{act} eq 'config_nmis_edit') { editConfig(); } elsif ($Q->{act} eq 'config_nmis_delete') { deleteConfig(); } elsif ($Q->{act} eq 'config_nmis_doadd') { doAddConfig(); displayConfig(); -# edit submission action: if it returns 0, we do nothing (assuming it prints complains) +# edit submission action: if it returns 0, we do nothing (assuming it prints complaints) # if it returns 0 AND sets error_message in Q, then we show the toplevel config AND the error message in a bar -# if it returns 1 we shod the topplevel config -} elsif ($Q->{act} eq 'config_nmis_doedit') { if (doEditConfig() or $Q->{error_message}) { displayConfig(); } +# if it returns 1 we show the top level config +} elsif ($Q->{act} eq 'config_nmis_doedit') { + displayConfig() if (doEditConfig() or $Q->{error_message}); } elsif ($Q->{act} eq 'config_nmis_dodelete') { doDeleteConfig(); displayConfig(); } elsif ($Q->{act} eq 'config_nmis_dostore') { doStoreTable(); displayConfig(); } else { notfound(); } +exit 1; + sub notfound { - print header($headeropts); + print header(-status => 400, %$headeropts); pageStart(title => "NMIS Configuration", refresh => $Q->{refresh}) if (!$wantwidget); print "Config: ERROR, act=$Q->{act}, node=$Q->{node}, intf=$Q->{intf}\n"; @@ -103,13 +107,11 @@ sub notfound { pageEnd if (!$wantwidget); } -exit 1; - - # # display the Config of NMIS # -sub displayConfig{ +sub displayConfig +{ my %args = @_; my $section = $Q->{section}; @@ -165,9 +167,13 @@ sub displayConfig{ } +# very minimal escape of inputs that will break the html structure sub escape { my $k = shift; - $k =~ s//>/g; + $k =~ s/&/&/g; + $k =~ s//>/g; + return $k; } @@ -374,7 +380,14 @@ sub editConfig{ pageEnd if (!$wantwidget); } -sub doEditConfig { +# endpoint for edit operation, +# consumes one section+item = value argument; +# validates where possible and updates +# the configuration if ok +# +# returns 1 if ok, 0 otherwise +sub doEditConfig +{ my %args = @_; return 1 if (getbool($Q->{cancel})); @@ -388,74 +401,203 @@ sub doEditConfig { # check if DB <=> file change if ($section eq 'database' and $item =~ /^db.*sql$/ and $C->{$item} ne $value and ($C->{$item} ne '' - or getbool($value)) ) { + or getbool($value)) ) + { storeTable(section=>$section,item=>$item,value=>$value); return 0; } - else + + # that's the non-flattened raw hash + my ($CC,undef) = readConfData(conf=>$C->{conf}); + # that's the set of display and validation rules + my $configrules = loadCfgTable(table => "Config", user => $AU->{user}); + + # handle the comfy group_list editing and translate the separate values + # ditto for roletype, nettype and nodetype + if ($section eq "system" and ( $item =~ /^(group|roletype|nettype|nodetype)_list$/)) { - my ($CC,undef) = readConfData(conf=>$C->{conf}); + my $concept = $1; + my $conceptname = $concept eq "group"? "Group" + : $concept eq "roletype"? "Role Type" : $concept eq "nettype"? "Network Type" : "Node Type"; # uggly + my @existing = split(/\s*,\s*/, $CC->{$section}->{$item}); + + my $newthing = $Q->{"new$concept"}; + # add actions ONLY if the add button was used to submit + if ($Q->{edittype} eq "Add" and defined $newthing and $newthing ne '') + { + return validation_abort($conceptname, + "'$newthing' contains invalid characters. Spaces and commas are prohibited.") + if ($newthing =~ /[, ]/); + + push @existing, $newthing + if (!grep($_ eq $newthing, @existing)); + } - # handle the comfy group_list editing and translate the separate values - # ditto for roletype, nettype and nodetype - if ($section eq "system" and ( $item =~ /^(group|roletype|nettype|nodetype)_list$/)) + # delete actions ONLY if the delete button was used to submit + if ($Q->{edittype} eq "Delete") { - my $concept = $1; - my $conceptname = $concept eq "group"? "Group" - : $concept eq "roletype"? "Role Type" : $concept eq "nettype"? "Network Type" : "Node Type"; # uggly - my @existing = split(/\s*,\s*/, $CC->{$section}->{$item}); - - my $newthing = $Q->{"new$concept"}; - # add actions ONLY if the add button was used to submit - if ($Q->{edittype} eq "Add" and defined $newthing and $newthing ne '') + for my $deletable (grep(/^delete_${concept}_/, keys %$Q)) { - if ($newthing =~ /[, ]/) - { - $Q->{error_message} = "$conceptname name \"$newthing\" contains invalid characters. Spaces and commas are prohibited."; - return 0; - } + next if $Q->{$deletable} ne "nuke"; + my $deletablename = $deletable; + $deletablename =~ s/^delete_group_//; + my $unesc = uri_unescape($deletablename); - push @existing, $newthing - if (!grep($_ eq $newthing, @existing)); + @existing = grep($_ ne $unesc, @existing); } + } + + $value = join(",", sort @existing); + } - # delete actions ONLY if the delete button was used to submit - if ($Q->{edittype} eq "Delete") + my $thisrule = NMIS::findCfgEntry(section => $section, item => $item, table => $configrules); + if (ref($thisrule) eq "HASH" && ref($thisrule->{validate}) eq "HASH") + { + # supported validation mechanisms: + # "int" => [ min, max ], undef can be used for no min/max - rejects X < min or > max + # "float" => [ min, max, above, below ] - rejects X < min or X <= above, X > max or X >= below + # that's required to express 'positive float' === strictly above zero: [0 or undef,dontcare,0,dontcare] + # "regex" => qr//, + # "ip" => [ 4 or 6 or 4, 6], + # "resolvable" => [ 4 or 6 or 4, 6] - accepts ip of that type or hostname that resolves to that ip type + # "onefromlist" => [ list of accepted values ] or undef - if undef, 'value' list is used + # accepts exactly one value + # "multifromlist" => [ list of accepted values ] or undef, like fromlist but more than one + # accepts any number of values from the list, including none whatsoever! + # more than one rule possible but likely not very useful + for my $valtype (sort keys %{$thisrule->{validate}}) + { + my $valprops = $thisrule->{validate}->{$valtype}; + + if ($valtype eq "int" or $valtype eq "float") { - for my $deletable (grep(/^delete_${concept}_/, keys %$Q)) + return validation_abort($item, "'$value' is not an integer!") + if ($valtype eq "int" and int($value) ne $value); + return validation_abort($item, "'$value' is not a floating point number!") + # integer or full ieee floating point with optional exponent notation + if ($valtype eq "float" and $value !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/); + + my ($min,$max,$above,$below) = ref($valprops) eq "ARRAY"? @{$valprops} : (undef,undef,undef,undef); + return validation_abort($item, "$value below minimum $min!") + if (defined($min) and $value < $min); + return validation_abort($item,"$value above maximum $max!") + if (defined($max) and $value > $max); + + # integers don't subdivide infinitely precisely so above and below not needed + if ($valtype eq "float") { - next if $Q->{$deletable} ne "nuke"; - my $deletablename = $deletable; - $deletablename =~ s/^delete_group_//; - my $unesc = uri_unescape($deletablename); + return validation_abort($item, "$value is not above $above!") + if (defined($above) and $value <= $above); - @existing = grep($_ ne $unesc, @existing); + return validation_abort($item, "$value is not below $below!") + if (defined($below) and $value >= $below); } } + elsif ($valtype eq "regex") + { + my $expected = ref($valprops) eq "Regexp"? $valprops : qr//; # fallback will match anything + return validation_abort($item, "'$value' didn't match regular expression!") + if ($value !~ $expected); + } + elsif ($valtype eq "ip") + { + my @ipversions = ref($valprops) eq "ARRAY"? @$valprops : (4,6); - $value = join(",", sort @existing); - } + my $ipobj = Net::IP->new($value); + return validation_abort($item, "'$value' is not a valid IP address!") + if (!$ipobj); + return validation_abort($item, "'$value' is IP address of the wrong type!") + if (($ipobj->version == 6 and !grep($_ == 6, @ipversions)) + or $ipobj->version == 4 and !grep($_ == 4, @ipversions)); + } + elsif ($valtype eq "resolvable") + { + return validation_abort($item, "'$value' is not a resolvable name or IP address!") + if (!$value); - $CC->{$section}{$item} = $value; - writeConfData(data=>$CC); - return 1; + my @ipversions = ref($valprops) eq "ARRAY"? @$valprops : (4,6); + + my $alreadyip = Net::IP->new($value); + if ($alreadyip) + { + return validation_abort($item, "'$value' is IP address of the wrong type!") + if (!grep($_ == $alreadyip->version, @ipversions)); + # otherwise, we're happy... + } + else + { + my @addresses = NMIS::resolve_dns_name($value); + return validation_abort($item, "DNS failed to resolve '$value'!") + if (!@addresses); + + my @addr_objs = map { Net::IP->new($_) } (@addresses); + my $goodones; + for my $type (4,6) + { + $goodones += grep($_->version == $type, @addr_objs) if (grep($_ == $type, @ipversions)); + } + return validation_abort($item, + "'$value' does not resolve to an IP address of the right type!") + if (!$goodones); + } + } + elsif ($valtype eq "onefromlist" or $valtype eq "multifromlist") + { + # either explicit list of acceptables, or the 'value' config item + my @acceptable = ref($valprops) eq "ARRAY"? @$valprops : + ref($thisrule->{value}) eq "ARRAY"? @{$thisrule->{value}}: (); + return validation_abort($item, "no validation choices configured!") + if (!@acceptable); + + # for multifromlist assume that value is now comma-separated. *sigh* + # for onefromlist values with colon are utterly unspecial *double sigh* + my @mustcheckthese = ($valtype eq "multifromlist")? split(/,/, $value) : $value; + for my $oneofmany (@mustcheckthese) + { + return validation_abort($item, "'$oneofmany' is not in list of acceptable values!") + if (!List::Util::any { $oneofmany eq $_ } (@acceptable)); + } + } + else + { + return validation_abort($item, "unknown validation type \"$valtype\""); + } + } } + + # no validation or success, so let's update the config + + $CC->{$section}{$item} = $value; + writeConfData(data=>$CC); + return 1; } +# setup (negative) response in $Q's error attribute +# args: item, message +# returns: undef +sub validation_abort +{ + my ($item, $message) = @_; -sub deleteConfig { + $Q->{error_message} = "'$item' failed to validate: $message"; + return undef; +} + +# shows the deletion yes/no dialog +sub deleteConfig +{ my %args = @_; my $section = $Q->{section}; my $item = $Q->{item}; - #start of page print header($headeropts); pageStart(title => "NMIS Configuration", refresh => $Q->{refresh}) if (!$wantwidget); $AU->CheckAccess("Table_Config_rw"); + # that's the non-flattened raw hash my ($CC,undef) = readConfData(conf=>$C->{conf}); my $value = $CC->{$section}{$item}; @@ -490,15 +632,16 @@ sub deleteConfig { onclick=> '$("#cancelinput").val("true");' . ($wantwidget? "get('nmisconfig');" : 'submit();'), -value=>'Cancel'))); - print end_form; - -End_deleteConfig: - print end_table; + print end_form, end_table; pageEnd if (!$wantwidget); } -sub doDeleteConfig { +# deletes one config entry (identified by section and item), +# if allowed to: rejects deletion of items that are subject to validation rules +# returns: 1 if ok, undef if not; sets Q's error attribute in that case +sub doDeleteConfig +{ my %args = @_; return if (getbool($Q->{cancel})); @@ -508,11 +651,25 @@ sub doDeleteConfig { my $section = $Q->{section}; my $item = $Q->{item}; + # that's the non-flattened raw hash my ($CC,undef) = readConfData(conf=>$C->{conf}); + # that's the set of display and validation rules + my $configrules = loadCfgTable(table => "Config", user => $AU->{user}); - delete $CC->{$section}{$item}; + # check if that thing is under validation; if so, reject deletion + # possible future improvement: check if validation rule allows empty value + my $thisrule = NMIS::findCfgEntry(section => $section, item => $item, table => $configrules); + if (ref($thisrule) eq "HASH" && ref($thisrule->{validate}) eq "HASH") + { + $Q->{error_message} = "'$item' cannot be deleted: required by validation rule!"; + return undef; + } + + + delete $CC->{$section}{$item}; writeConfData(data=>$CC); + return 1; } sub addConfig{ diff --git a/cgi-bin/debug.pl b/cgi-bin/debug.pl index bd5e792..48e3d49 100755 --- a/cgi-bin/debug.pl +++ b/cgi-bin/debug.pl @@ -3,37 +3,37 @@ ## $Id: debug.pl,v 8.9 2012/01/10 01:49:11 keiths Exp $ # # Copyright (C) Opmantek Limited (www.opmantek.com) -# +# # ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM -# +# # This file is part of Network Management Information System (“NMIS”). -# +# # NMIS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # NMIS 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 General Public License for more details. -# +# # You should have received a copy of the GNU General Public License -# along with NMIS (most likely in a file named LICENSE). +# along with NMIS (most likely in a file named LICENSE). # If not, see -# +# # For further information on NMIS or for a license other than GPL please see -# www.opmantek.com or email contact@opmantek.com -# +# www.opmantek.com or email contact@opmantek.com +# # User group details: # http://support.opmantek.com/users/ -# +# # ***************************************************************************** # Auto configure to the /lib use FindBin; use lib "$FindBin::Bin/../lib"; -# +# use strict; use NMIS; use func; @@ -52,8 +52,8 @@ # load NMIS configuration table if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; -# if options, then called from command line -if ( $#ARGV > 0 ) { $C->{auth_require} = 0; } # bypass auth +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth # NMIS Authentication module use Auth; @@ -109,7 +109,7 @@ function createTOC() { // to do : gracefully handle if h2 is top level id and not h1 - + // configuration options var page_block_id = 'contentcenter'; // this is the id which contains our h1's etc var toc_page_position =-1; // used later to remember where in the page to put the final TOC @@ -127,7 +127,7 @@ myrow.appendChild(mycell); mytablebody.appendChild(myrow); y.appendChild(mytablebody); - + // create the two title strings so we can switch between the two later via the id var a = mycell.appendChild(document.createElement('span')); a.id = 'toc_hide'; @@ -138,12 +138,12 @@ a.style.display='none' a.innerHTML = 'Contents [show]'; a.style.textAlign='center'; - + var z = mycell.appendChild(document.createElement('div')); - + // set the id so we can show/hide this div block later z.id ='toc_contents'; - + var toBeTOCced = new Array(); for (var i=0;i 0) // if we dropped back down, clear out the upper numbers @@ -206,7 +206,7 @@ if (tmp_indent > 10) tmp.style.paddingLeft=tmp_indent -10+'px'; - + // if NOT h1 tag, add to toc if (!skip_first) { @@ -217,7 +217,7 @@ } else // else, act as if this item was never created. { - skip_first=false; + skip_first=false; // this is so the toc prefixes stay proper if the page starts with a h2 instead of a h1... we just reset the first heading to 0 --level; --counterArray[level]; @@ -241,7 +241,7 @@ - // now we work on auto-jumping to a specific target + // now we work on auto-jumping to a specific target // document.location.hash has the target we want to jump to if (document.location.hash.length >= 9) // we now it's gotta be atleast '#header_x' { @@ -321,9 +321,9 @@ printf( "
netloc url(-base => 1) %s", url(-base => 1 )); print "

Scripts URL

"; - + ########### -my $urlcgi = $q->url(-relative => 1); # Use relative url for all our script href - in effect our scriptname +my $urlcgi = $q->url(-relative => 1); # Use relative url for all our script href - in effect our scriptname my $urlbase = $q->url(-base=>1) . $C->{'url_base'}; # full url for static pages, img, gifs etc. my $urlcgibase = $q->url(-base=>1) . $C->{'cgi_url_base'}; # same for script base directory, use for handover to other scripts in same directory my $urlhost = $q->url(-base=>1); # same for script base directory, use for handover to other scripts in same directory, appended by nmis::config var @@ -367,16 +367,15 @@ my %allevents = loadAllEvents; print dumper_html(\%allevents); -my $OT = loadOutageTable(); print "

Outage Table

"; -print dumper_html($OT); +print dumper_html(NMIS::find_outages); my $ext = getExtension(dir=>'var'); foreach my $node (sort keys %{$NT}) { if ( $C->{server_name} eq $NT->{$node}{server} ) { my $nodeInfo = loadNodeInfoTable($node); - + print "

$node System File ( /var/$node.$ext)

last updated @{[ int ((-M \"$FindBin::Bin/../var/$node.$ext\") *24*60) ]} minutes ago
"; print dumper_html($nodeInfo); } @@ -385,8 +384,8 @@ my $NS = loadNodeSummary(); print "

Node Summary

"; print dumper_html($NS); - -my $dir = "$FindBin::Bin/../var"; + +my $dir = "$FindBin::Bin/../var"; foreach my $summary ( qw( nmis-summary8h nmis-summary16h ) ) { my %summaryHash = readFiletoHash(file=>"$dir/$summary"); @@ -421,7 +420,7 @@ print ''; for my $i ( 0 .. $C->{response_time_threshold}) { my $c = colorResponseTime($i,$C->{response_time_threshold}); - if ($i % 20 == 0) { + if ($i % 20 == 0) { print ""; } } @@ -453,7 +452,7 @@ sub dumper_html $unclean =~ s/>/>/g; # and that's all that data::dumper::html did :-( - $unclean =~ s{\n}{
\n}g; + $unclean =~ s{\n}{
\n}g; $unclean =~ s{\t}{ }g; $unclean=~ s{[ ]{5}}{     }g; $unclean =~ s{[ ]{3}}{   }g; @@ -540,4 +539,3 @@ sub dumper_html color:#666666; font-size:11px; } - diff --git a/cgi-bin/menu.pl b/cgi-bin/menu.pl index c64d32c..6e31b60 100755 --- a/cgi-bin/menu.pl +++ b/cgi-bin/menu.pl @@ -196,16 +196,6 @@ sub menu_bar_site { push @netperf, qq|Link List| if ($AU->CheckAccess("Table_Links_view","check")); - - ### 2012-11-26 keiths, Optional opFlow Widgets if opFlow Installed. - if ($M->moduleInstalled(module => "opFlow") ) { - push @netperf, qq|--------|; - push @netperf, qq|Application Flows|; - push @netperf, qq|TopN Applications|; - push @netperf, qq|TopN Application Sources|; - push @netperf, qq|TopN Talkers|; - push @netperf, qq|TopN Listeners|; - } push @menu_site,(qq|Network Performance|,[ @netperf ]); @@ -322,7 +312,7 @@ sub menu_bar_site { push @tableMenu, qq|Model Policy| if ($AU->CheckAccess("table_models_view","check")); - + push @tableMenu, qq|------| if (@tableMenu); # no separator if there's nothing to separate... foreach my $table (sort {$Tables->{$a}{DisplayName} cmp $Tables->{$b}{DisplayName} } keys %{$Tables}) { @@ -330,33 +320,6 @@ sub menu_bar_site { } my (@systemitems, @setupitems); - - - #if ($AU->CheckAccess("table_config_view","check")) - #{ - # push @systemitems, qq|Add/Edit Groups|; - #} - # - #push @systemitems, qq|Add/Edit Nodes and Devices| - # if ($AU->CheckAccess("Table_Nodes_view","check")); - # - #push @systemitems, qq|Node Customisation| - # if ($AU->CheckAccess("table_nodeconf_view","check")); - # - #push @systemitems, qq|System Configuration| - # if ($AU->CheckAccess("table_config_view","check")); - # - #push @systemitems, qq|Emails, Notifications and Escalations| - # if ($AU->CheckAccess("Table_Escalations_view","check")); - # - #push @systemitems, qq|Thresholding Alerts| - # if ($AU->CheckAccess("table_models_view","check")); - # - #push @systemitems, qq|Event Logging and Syslog| - # if ($AU->CheckAccess("table_models_view","check")); - # - #push @systemitems, qq|------| if (@tableMenu); # no separator if there's nothing to separate... - push @systemitems, qq|System Configuration|, \@tableMenu if (@tableMenu); if ($AU->CheckAccess("tls_event_flow","check") @@ -429,6 +392,9 @@ sub menu_bar_site { push @setupitems, qq|Model Policy| if ($AU->CheckAccess("table_models_view","check")); + push @setupitems, qq|Polling Policy| + if ($AU->CheckAccess("table_polling-policy_view","check")); + #push @setupitems, qq|Event Logging and Syslog| # if ($AU->CheckAccess("table_models_view","check")); diff --git a/cgi-bin/models.pl b/cgi-bin/models.pl index 8bbd91a..21ba034 100755 --- a/cgi-bin/models.pl +++ b/cgi-bin/models.pl @@ -27,40 +27,38 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** +use strict; +our $VERSION = "8.6.2G"; + use FindBin; use lib "$FindBin::Bin/../lib"; -use strict; +use CGI qw(:standard *table *Tr *td *form *Select *div); +use Fcntl qw(:DEFAULT :flock); +use Data::Dumper; + use NMIS; use func; use csv; -use Fcntl qw(:DEFAULT :flock); use Sys; use Mib; use Auth; -use Data::Dumper; - -use CGI qw(:standard *table *Tr *td *form *Select *div); my $q = new CGI; my $Q = $q->Vars; my $wantwidget = (!getbool($Q->{widget},"invert")); # default is thus 1=widgetted. $Q->{widget} = $wantwidget? "true":"false"; # and set it back to prime urls and inputs + my $C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}); +die "failed to load configuration!\n" if (!$C or ref($C) ne "HASH" or !keys %$C); -if (!$C) -{ - print header(-status => 500); - pageStart(title => "NMIS Modeling") if (!$wantwidget); - print "
Error: Failed to load config file!
"; - pageEnd if (!$wantwidget); - exit 1; -} +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth # variables used for the security mods my $headeropts = {type=>'text/html',expires=>'now'}; -my $AU = Auth->new(conf => $C); # Auth::new will reap init values from NMIS::config +my $AU = Auth->new(conf => $C); if ($AU->Require) { @@ -205,7 +203,10 @@ elsif ($Q->{act} eq 'config_model_delete') { deleteModel(); } # performing actual changes elsif ($Q->{act} eq 'config_model_doadd') { doAddModel(); displayModel(); } -elsif ($Q->{act} eq 'config_model_doedit') { doEditModel(); displayModel(); } +# if edit fails, remain on the editmodel page +elsif ($Q->{act} eq 'config_model_doedit') { + doEditModel()? displayModel() : editModel(); +} elsif ($Q->{act} eq 'config_model_dodelete') { doDeleteModel(); displayModel(); } else { @@ -258,6 +259,10 @@ sub displayModel . hidden(-override => 1, -name => "widget", -value => $Q->{widget}), # the menu-ish table part start_table(), + + # error indicator if there is one + ($Q->{error_message} ? Tr(td({class=>'Fatal',align=>'center'}, "Error: $Q->{error_message}")) : ""), + "", td({class=>'header'}, "Select Model
". popup_menu(-name=>'model', -override=>'1', @@ -478,6 +483,9 @@ sub editModel print "
$i
", Tr(td({class=>"header",colspan=>'8',align=>'center'}, "Editing Model $wantedmodel")); + print Tr(td({class=>'Fatal',align=>'center',colspan => 8}, "Error: $Q->{error_message}")) + if ($Q->{error_message}); + my $field = $modelstruct; my @locationsteps = split(/,/, $locsteps); # print header, and traverse the structure @@ -700,10 +708,10 @@ sub addModel # endpoint for post, for making in-place edits to leaf things # args: none but uses q's mode, section, hash and value, also cancel -# returns: nothing; +# returns: 1 if ok, undef if not - sets Q's error attribute in that case sub doEditModel { - return if (getbool($Q->{cancel})); + return 1 if (getbool($Q->{cancel})); $AU->CheckAccess("Table_Models_rw",'header'); my ($wantedmodel,$wantedsection,$locsteps,$value) = @@ -718,6 +726,18 @@ sub doEditModel if (ref($modelstruct) ne "HASH" or !keys %$modelstruct); my @locationsteps = split(/,/, $locsteps); + # validation: enfoce numeric value (floating point or integer) for + # 1. all things under section=threshold with second-to-last step being 'value' + # 2. all things under section=alerts, with second-to-last step being 'threshold' + if ($value !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/ + and ( ($wantedsection eq "threshold" and $locationsteps[-2] eq "value") + or ($wantedsection eq "alerts" and $locationsteps[-2] eq "threshold"))) + { + $Q->{error_message} = "Validation for $locationsteps[-1] failed: '$value' is not a number!"; + return undef; + } + + # validation ok? then traverse the structure and update the leaf my $target = $modelstruct; for my $nextstep (@locationsteps[0..$#locationsteps-1]) { @@ -733,6 +753,7 @@ sub doEditModel $target->{$locationsteps[-1]} = $value; } writeHashtoFile(file => $modelfn, data => $modelstruct); + return 1; } # endpoint for post, for deleting leaves or whole subtrees diff --git a/cgi-bin/network.pl b/cgi-bin/network.pl index 6fcdccf..6cdd41c 100755 --- a/cgi-bin/network.pl +++ b/cgi-bin/network.pl @@ -38,6 +38,7 @@ use URI::Escape; use URI; use URI::QueryParam; +use Net::SNMP qw(oid_lex_sort); use Data::Dumper; $Data::Dumper::Indent = 1; @@ -52,8 +53,8 @@ # load NMIS configuration table if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; -# if options, then called from command line -if ( $#ARGV > 0 ) { $C->{auth_require} = 0; } # bypass auth +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth # NMIS Authentication module use Auth; @@ -1100,7 +1101,7 @@ sub selectLarge { my $lastUpdate = ""; my $colorlast = $color; my $lastUpdateClass = "info Plain nowrap"; - my $time = $groupSummary->{$node}{lastUpdateSec}; + my $time = $groupSummary->{$node}{last_poll}; if ( $time ne "") { $lastUpdate = returnDateStamp($time); if ($time < (time - 60*15)) { @@ -1471,7 +1472,7 @@ sub viewNode { } my %status = PreciseNodeStatus(system => $S); - + $S->readNodeView(); my $V = $S->view; @@ -1487,7 +1488,7 @@ sub viewNode { print "$nodelink is managed by server $NT->{$node}{server}"; print < - viewwndw('$node','$url',$wd,$ht); + viewwndw('$node','$url',$wd,$ht,'server'); EO_HTML return; @@ -1495,8 +1496,9 @@ sub viewNode { # fallback/default order and set of propertiess for displaying all information my @order = ( - 'status' - ,'sysName' + 'status', + 'outage', + 'sysName', ,'host_addr' ,'group' ,'customer' @@ -1506,6 +1508,7 @@ sub viewNode { ,'notes' ,'nodeType' ,'nodeModel' + ,'polling_policy' ,'sysUpTime' ,'ifNumber' ,'sysLocation' @@ -1555,13 +1558,26 @@ sub viewNode { } #http://nmisdev64.dev.opmantek.com/cgi-nmis8/nodeconf.pl?conf=Config.xxxx&act= + # this will handle the Name and URL for additional node information + my $context; + if ( defined $NT->{$node}{node_context_name} and $NT->{$node}{node_context_name} ne "" ) { + my $url = $NT->{$node}{node_context_url} if $NT->{$node}{node_context_url}; + # substitute any known parameters + $url =~ s/\$host/$NT->{$node}{host}/g; + $url =~ s/\$name/$NT->{$node}{name}/g; + $url =~ s/\$node_name/$NT->{$node}{name}/g; + + $context = qq| $NT->{$node}{node_context_name}|; + } + + # this will handle the Name and URL for remote management connection my $remote; if ( defined $NT->{$node}{remote_connection_name} and $NT->{$node}{remote_connection_name} ne "" ) { my $url = $NT->{$node}{remote_connection_url} if $NT->{$node}{remote_connection_url}; # substitute any known parameters $url =~ s/\$host/$NT->{$node}{host}/g; $url =~ s/\$name/$NT->{$node}{name}/g; - $url =~ s/\$node/$NT->{$node}{name}/g; + $url =~ s/\$node_name/$NT->{$node}{name}/g; $remote = qq| $NT->{$node}{remote_connection_name}|; } @@ -1574,6 +1590,7 @@ sub viewNode { $nodeDetails .= " - $editnode" if $editnode; $nodeDetails .= " - $editconf" if $editconf; $nodeDetails .= " - $remote" if $remote; + $nodeDetails .= " - $context" if $context; print Tr(th({class=>'title', colspan=>'2'},$nodeDetails)); print start_Tr; @@ -1582,108 +1599,129 @@ sub viewNode { # list of values eval { my @out; - foreach my $k (@items){ + foreach my $k (@items) + { # the default title is the key name. # but can I get a better title? my $title = ( defined($V->{system}->{"${k}_title"}) ? $V->{system}{"${k}_title"} : $S->getTitle(attr=>$k,section=>'system')) || $k; - # print STDERR "DEBUG: k=$k, title=$title\n"; - - if ($title ne '') { - my $color = $V->{system}{"${k}_color"} || '#FFF'; - my $gurl = $V->{system}{"${k}_gurl"}; # create new window - - # existing window, possibly widgeted or not - # but that's unknown when nmis.pl creates the view entry! - my $url; - if ($V->{system}{"${k}_url"}) + next if ($title eq ''); + + my $color = $V->{system}{"${k}_color"} || '#FFF'; + my $gurl = $V->{system}{"${k}_gurl"}; # create new window + + # existing window, possibly widgeted or not + # but that's unknown when nmis.pl creates the view entry! + my $url; + if ($V->{system}{"${k}_url"}) + { + my $u = URI->new($V->{system}{"${k}_url"}); + $u->query_param("widget" => ($wantwidget? "true": "false")); + $url = $u->as_string; + } + + my $value; + # get the value from the view if it one of the special ones, or only present there + if ( $k =~ /^(host_addr|lastUpdate|configurationState|configLastChanged|configLastSaved|bootConfigLastChanged)$/ + or not exists($NI->{system}{$k}) ) + { + $value = $V->{system}{"${k}_value"}; + } + else + { + $value = $NI->{system}{$k}; + } + + # escape the input if there's anything in need of escaping; + # we don't want doubly-escaped uglies. + $value = escapeHTML($value) if ($value =~ /[<>]/); + + $color = colorPercentHi(100) if $V->{system}{"${k}_value"} eq "running"; + $color = colorPercentHi(0) if $color eq "red"; + + # a few special ones + if ($k eq 'status') + { + if ( !$status{overall} ) { - my $u = URI->new($V->{system}{"${k}_url"}); - $u->query_param("widget" => ($wantwidget? "true": "false")); - $url = $u->as_string; + $value = "unreachable"; + $color = "#F00"; } - - my $value; - # get the value from the view if it one of the special ones, or only present there - if ( - $k =~ /^(host_addr|lastUpdate|configurationState|configLastChanged|configLastSaved|bootConfigLastChanged)$/ - or not exists($NI->{system}{$k}) - ) { - $value = $V->{system}{"${k}_value"}; + elsif ( $status{overall} == -1 ) + { + $value = "degraded"; + $color = "#FF0"; } else { - $value = $NI->{system}{$k}; + $value = "reachable"; + $color = "#0F0"; } + } + # from outageCheck, neither nodeinfo nor view + elsif ($k eq 'outage') + { + my ($outagestatus, $nextoutage) = NMIS::outageCheck(node => $node, time => time()); + + # slightly special: don't show this row unless current or pending + next if (!$outagestatus); - # escape the input if there's anything in need of escaping; - # we don't want doubly-escaped uglies. - $value = escapeHTML($value) if ($value =~ /[<>]/); - - $color = colorPercentHi(100) if $V->{system}{"${k}_value"} eq "running"; - $color = colorPercentHi(0) if $color eq "red"; - - if ($k eq 'status') + $title = "Outage Status"; + if ($outagestatus eq "current") { - if ( !$status{overall} ) - { - $value = "unreachable"; - $color = "#F00"; - } - elsif ( $status{overall} == -1 ) - { - $value = "degraded"; - $color = "#FF0"; - } - else { - $value = "reachable"; - $color = "#0F0"; - } + $value = "Outage \"".($nextoutage->{change_id} || $nextoutage->{description})."\" is Current"; + $color = eventColor("Warning"); } - - if ($k eq 'lastUpdate') { - # check lastupdate - my $time = $NI->{system}{lastUpdateSec}; - if ( $time ne "" ) { - if ($time < (time - 60*15)) { - $color = "#ffcc00"; # to late - } - } + else + { + $value = "Planned Future Outage \"".($nextoutage->{change_id} || $nextoutage->{description}).'"'; + $color = eventColor("Normal"); # it's not active yet, let's show it as good/green/whatever } - - if ($k eq 'TimeSinceTopologyChange' and $NI->{system}{TimeSinceTopologyChange} =~ /\d+/ ) { - if ( $value ne "N/A" ) { - # convert to uptime format, time since change - $value = convUpTime($NI->{system}{TimeSinceTopologyChange}/100); - # did this reset in the last 1 h - if ( $NI->{system}{TimeSinceTopologyChange} / 100 < 360000 ) { - $color = "#ffcc00"; # to late - } + } + elsif ($k eq 'lastUpdate') + { + # check lastupdate + my $time = $NI->{system}{last_poll}; + if ( $time ne "" ) { + if ($time < (time - 60*15)) { + $color = "#ffcc00"; # to late } } + } + elsif ($k eq 'TimeSinceTopologyChange' and $NI->{system}{TimeSinceTopologyChange} =~ /\d+/ ) + { + if ( $value ne "N/A" ) { + # convert to uptime format, time since change + $value = convUpTime($NI->{system}{TimeSinceTopologyChange}/100); + # did this reset in the last 1 h + if ( $NI->{system}{TimeSinceTopologyChange} / 100 < 360000 ) { + $color = "#ffcc00"; # to late + } + } + } - ### 2012-02-21 keiths, fixed popup window not opening correctly. - my $content = $value; - if ($gurl) { - $content = a({target=>"Graph-$node", onClick=>"viewwndw(\'$node\',\'$gurl\',$C->{win_width},$C->{win_height})"},"$value"); - } - elsif ($url) { - $content = a({href=>$url},$value); - } - - my $printData = 1; - $printData = 0 if $k eq "customer" and not tableExists('Customers'); - $printData = 0 if $k eq "businessService" and not tableExists('BusinessServices'); - $printData = 0 if $k eq "serviceStatus" and not tableExists('ServiceStatus'); - $printData = 0 if $k eq "location" and not tableExists('Locations'); - - if ( $printData ) { - push @out,Tr(td({class=>'info Plain'}, escapeHTML($title)), - td({class=>'info Plain',style=>getBGColor($color)},$content)); - } + ### 2012-02-21 keiths, fixed popup window not opening correctly. + my $content = $value; + if ($gurl) { + $content = a({target=>"Graph-$node", onClick=>"viewwndw(\'$node\',\'$gurl\',$C->{win_width},$C->{win_height})"},"$value"); + } + elsif ($url) { + $content = a({href=>$url},$value); + } + + my $printData = 1; + $printData = 0 if $k eq "customer" and not tableExists('Customers'); + $printData = 0 if $k eq "businessService" and not tableExists('BusinessServices'); + $printData = 0 if $k eq "serviceStatus" and not tableExists('ServiceStatus'); + $printData = 0 if $k eq "location" and not tableExists('Locations'); + + if ( $printData ) { + push @out,Tr(td({class=>'info Plain'}, escapeHTML($title)), + td({class=>'info Plain',style=>getBGColor($color)},$content)); } } + # display events for this one node - also close one if asked to if (my %nodeevents = loadAllEvents(node => $node)) { @@ -2690,17 +2728,16 @@ sub viewCpuList { . ", status=$NI->{system}{status_summary}")); } - print Tr(th({class=>'title',colspan=>'7'},"List of CPU's on node $NI->{system}{name}")); + print Tr(th({class=>'title',colspan=>'7'},"List of CPUs on node $NI->{system}{name}")); my $url = url(-absolute=>1)."?conf=$Q->{conf}&act=network_service_list&refresh=$Q->{refresh}&widget=$widget&node=".uri_escape($node); - if (defined $NI->{services}) { + if (my @cpus = $S->getTypeInstances(graphtype => "hrsmpcpu") ) { print Tr( td({class=>'header'},"CPU ID and Description"), td({class=>'header'},"History"), ); - foreach my $index ( $S->getTypeInstances(graphtype => "hrsmpcpu")) { - + foreach my $index ( sort @cpus ) { print Tr( td({class=>'lft Plain'},"Server CPU $index ($NI->{device}{$index}{hrDeviceDescr})"), td({class=>'info Plain'},htmlGraph(graphtype=>"hrsmpcpu",node=>$node,intf=>$index, width=>$smallGraphWidth,height=>$smallGraphHeight) ) @@ -2708,7 +2745,7 @@ sub viewCpuList { } } else { - print Tr(th({class=>'title',colspan=>'6'},"No Services found for $NI->{system}{name}")); + print Tr(th({class=>'title',colspan=>'6'},"No CPUs found for $NI->{system}{name}")); } print end_table; pageEnd() if (!$wantwidget); @@ -2942,7 +2979,7 @@ sub viewSystemHealth $gotHeaders = 1; } - foreach my $index (sort {$a <=> $b} keys %{$NI->{$section}} ) { + foreach my $index (oid_lex_sort(keys %{$NI->{$section}}) ) { if( exists( $M->{systemHealth}{rrd}{$section}{control} ) && !$S->parseString(string=>"($M->{systemHealth}{rrd}{$section}{control}) ? 1:0", index=>$index, sect=>$section)) { next; @@ -3282,12 +3319,12 @@ sub viewTop10 { my $NI = $S->ndinfo; my $IF = $S->ifinfo; # reachable, available, health, response - %reportTable = (%reportTable,%{getSummaryStats(sys=>$S,type=>"health",start=>$start,end=>$end,index=>$reportnode)}); + %reportTable = (%reportTable,%{ getSummaryStats(sys=>$S,type=>"health",start=>$start,end=>$end,index=>$reportnode) // {}}); # cpu only for routers, switch cpu and memory in practice not an indicator of performance. # avgBusy1min, avgBusy5min, ProcMemUsed, ProcMemFree, IOMemUsed, IOMemFree if ($NI->{graphtype}{nodehealth} =~ /cpu/ and getbool($NI->{system}{collect})) { - %cpuTable = (%cpuTable,%{getSummaryStats(sys=>$S,type=>"nodehealth",start=>$start,end=>$end,index=>$reportnode)}); + %cpuTable = (%cpuTable,%{ getSummaryStats(sys=>$S,type=>"nodehealth",start=>$start,end=>$end,index=>$reportnode) // {} }); print STDERR "Result: ". Dumper \%cpuTable; } @@ -3510,218 +3547,216 @@ sub nodeAdminSummary foreach my $node (sort keys %{$LNT}) { - #if ( $LNT->{$node}{active} eq "true" ) { - if ( 1 ) { - if ( $AU->InGroup($LNT->{$node}{group}) and ($group eq "" or $group eq $LNT->{$node}{group}) ) { - my $intCollect = 0; - my $intCount = 0; - my $S = Sys::->new; # get system object + if ( $AU->InGroup($LNT->{$node}{group}) and ($group eq "" or $group eq $LNT->{$node}{group}) ) + { + my $intCollect = 0; + my $intCount = 0; + my $S = Sys::->new; # get system object $S->init(name=>$node,snmp=>'false'); # load node info and Model if name exists - my $NI = $S->ndinfo; - my $IF = $S->ifinfo; - my $exception = 0; - my @issueList; - - # Is the node active and are we doing stats on it. - if ( getbool($LNT->{$node}{active}) and getbool($LNT->{$node}{collect}) ) { - for my $ifIndex (keys %{$IF}) { - ++$intCount; - if ( $IF->{$ifIndex}{collect} eq "true") { - ++$intCollect; - #print "$IF->{$ifIndex}{ifIndex}\t$IF->{$ifIndex}{ifDescr}\t$IF->{$ifIndex}{collect}\t$IF->{$ifIndex}{Description}\n"; - } + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + my $exception = 0; + my @issueList; + + # Is the node active and are we doing stats on it. + if ( getbool($LNT->{$node}{active}) and getbool($LNT->{$node}{collect}) ) { + for my $ifIndex (keys %{$IF}) { + ++$intCount; + if ( $IF->{$ifIndex}{collect} eq "true") { + ++$intCollect; + #print "$IF->{$ifIndex}{ifIndex}\t$IF->{$ifIndex}{ifDescr}\t$IF->{$ifIndex}{collect}\t$IF->{$ifIndex}{Description}\n"; } } - my $sysDescr = $NI->{system}{sysDescr}; - $sysDescr =~ s/[\x0A\x0D]/\\n/g; - $sysDescr =~ s/,/;/g; - - my $community = "OK"; - my $commClass = "info Plain"; - - my $lastCollectPoll = defined $NI->{system}{lastCollectPoll} ? returnDateStamp($NI->{system}{lastCollectPoll}) : "N/A"; - my $lastCollectClass = "info Plain"; - - my $lastUpdatePoll = defined $NI->{system}{lastUpdatePoll} ? returnDateStamp($NI->{system}{lastUpdatePoll}) : "N/A"; - my $lastUpdateClass = "info Plain"; - - my $pingable = "unknown"; - my $pingClass = "info Plain"; - - my $snmpable = "unknown"; - my $snmpClass = "info Plain"; - - my $wmiworks = "unknown"; - my $wmiclass = "info Plain"; - - my $moduleClass = "info Plain"; - - my $actClass = "info Plain Minor"; + } + my $sysDescr = $NI->{system}{sysDescr}; + $sysDescr =~ s/[\x0A\x0D]/\\n/g; + $sysDescr =~ s/,/;/g; + + my $community = "OK"; + my $commClass = "info Plain"; + + my $lastCollectPoll = defined $NI->{system}{lastCollectPoll} ? returnDateStamp($NI->{system}{lastCollectPoll}) : "N/A"; + my $lastCollectClass = "info Plain"; + + my $lastUpdatePoll = defined $NI->{system}{last_update} ? returnDateStamp($NI->{system}{last_update}) : "N/A"; + my $lastUpdateClass = "info Plain"; + + my $pingable = "unknown"; + my $pingClass = "info Plain"; + + my $snmpable = "unknown"; + my $snmpClass = "info Plain"; + + my $wmiworks = "unknown"; + my $wmiclass = "info Plain"; + + my $moduleClass = "info Plain"; + + my $actClass = "info Plain Minor"; + if ( $LNT->{$node}{active} eq "false" ) { + push(@issueList,"Node is not active"); + } + else { + $actClass = "info Plain"; if ( $LNT->{$node}{active} eq "false" ) { - push(@issueList,"Node is not active"); + $lastCollectPoll = "N/A"; } - else { - $actClass = "info Plain"; - if ( $LNT->{$node}{active} eq "false" ) { - $lastCollectPoll = "N/A"; - } - elsif ( not defined $NI->{system}{lastCollectPoll} ) { - $lastCollectPoll = "unknown"; - $lastCollectClass = "info Plain Minor"; - $exception = 1; - push(@issueList,"Last collect poll is unknown"); - } - elsif ( $NI->{system}{lastCollectPoll} < (time - 60*15) ) { - $lastCollectClass = "info Plain Major"; + elsif ( not defined $NI->{system}{lastCollectPoll} ) { + $lastCollectPoll = "unknown"; + $lastCollectClass = "info Plain Minor"; + $exception = 1; + push(@issueList,"Last collect poll is unknown"); + } + elsif ( $NI->{system}{lastCollectPoll} < (time - 60*15) ) { + $lastCollectClass = "info Plain Major"; + $exception = 1; + push(@issueList,"Last collect poll was over 5 minutes ago"); + } + + if ( $LNT->{$node}{active} eq "false" ) { + $lastUpdatePoll = "N/A"; + } + elsif ( not defined $NI->{system}{last_update} ) { + $lastUpdatePoll = "unknown"; + $lastUpdateClass = "info Plain Minor"; + $exception = 1; + push(@issueList,"Last update poll is unknown"); + } + elsif ( $NI->{system}{last_update} < (time - 86400) ) { + $lastUpdateClass = "info Plain Major"; + $exception = 1; + push(@issueList,"Last update poll was over 1 day ago"); + } + + $pingable = "true"; + $pingClass = "info Plain"; + if ( not defined $NI->{system}{nodedown} ) { + $pingable = "unknown"; + $pingClass = "info Plain Minor"; + $exception = 1; + push(@issueList,"Node state is unknown"); + } + elsif ( $NI->{system}{nodedown} eq "true" ) { + $pingable = "false"; + $pingClass = "info Plain Major"; + $exception = 1; + push(@issueList,"Node is currently unreachable"); + } + + # figure out what sources are enabled and which of those work/are misconfig'd etc + my %status = PreciseNodeStatus(system => $S); + + if ( !getbool($LNT->{$node}{collect}) or !$status{wmi_enabled} ) + { + $wmiworks = "N/A"; + } + else + { + if (!$status{wmi_status}) + { + $wmiworks = "false"; + $wmiclass = "Info Plain Major"; $exception = 1; - push(@issueList,"Last collect poll was over 5 minutes ago"); + push @issueList, "WMI access is currently down"; } - - if ( $LNT->{$node}{active} eq "false" ) { - $lastUpdatePoll = "N/A"; + else + { + $wmiworks = "true"; } - elsif ( not defined $NI->{system}{lastUpdatePoll} ) { - $lastUpdatePoll = "unknown"; - $lastUpdateClass = "info Plain Minor"; + } + + if ( !getbool($LNT->{$node}{collect}) or !$status{snmp_enabled} ) + { + $community = $snmpable = "N/A"; + } + else + { + $snmpable = 'true'; + if ( !$status{snmp_status} ) + { + $snmpable = 'false'; + $snmpClass = "info Plain Major"; $exception = 1; - push(@issueList,"Last update poll is unknown"); + push(@issueList,"SNMP access is currently down"); } - elsif ( $NI->{system}{lastUpdatePoll} < (time - 86400) ) { - $lastUpdateClass = "info Plain Major"; + + if ( $LNT->{$node}{community} eq "" ) { + $community = "BLANK"; + $commClass = "info Plain Major"; $exception = 1; - push(@issueList,"Last update poll was over 1 day ago"); + push(@issueList,"SNMP Community is blank"); } - - $pingable = "true"; - $pingClass = "info Plain"; - if ( not defined $NI->{system}{nodedown} ) { - $pingable = "unknown"; - $pingClass = "info Plain Minor"; + + if ( $LNT->{$node}{community} eq "public" ) { + $community = "DEFAULT"; + $commClass = "info Plain Minor"; $exception = 1; - push(@issueList,"Node state is unknown"); + push(@issueList,"SNMP Community is default (public)"); } - elsif ( $NI->{system}{nodedown} eq "true" ) { - $pingable = "false"; - $pingClass = "info Plain Major"; + + if ( $LNT->{$node}{model} ne "automatic" ) { + $moduleClass = "info Plain Minor"; $exception = 1; - push(@issueList,"Node is currently unreachable"); - } - - # figure out what sources are enabled and which of those work/are misconfig'd etc - my %status = PreciseNodeStatus(system => $S); - - if ( !getbool($LNT->{$node}{collect}) or !$status{wmi_enabled} ) - { - $wmiworks = "N/A"; - } - else - { - if (!$status{wmi_status}) - { - $wmiworks = "false"; - $wmiclass = "Info Plain Major"; - $exception = 1; - push @issueList, "WMI access is currently down"; - } - else - { - $wmiworks = "true"; - } - } - - if ( !getbool($LNT->{$node}{collect}) or !$status{snmp_enabled} ) - { - $community = $snmpable = "N/A"; - } - else - { - $snmpable = 'true'; - if ( !$status{snmp_status} ) - { - $snmpable = 'false'; - $snmpClass = "info Plain Major"; - $exception = 1; - push(@issueList,"SNMP access is currently down"); - } - - if ( $LNT->{$node}{community} eq "" ) { - $community = "BLANK"; - $commClass = "info Plain Major"; - $exception = 1; - push(@issueList,"SNMP Community is blank"); - } - - if ( $LNT->{$node}{community} eq "public" ) { - $community = "DEFAULT"; - $commClass = "info Plain Minor"; - $exception = 1; - push(@issueList,"SNMP Community is default (public)"); - } - - if ( $LNT->{$node}{model} ne "automatic" ) { - $moduleClass = "info Plain Minor"; - $exception = 1; - push(@issueList,"Not using automatic model discovery"); - } + push(@issueList,"Not using automatic model discovery"); } } + } + + my $wd = 850; + my $ht = 700; + + my $idsafenode = $node; + $idsafenode = (split(/\./,$idsafenode))[0]; + $idsafenode =~ s/[^a-zA-Z0-9_:\.-]//g; + + my $nodelink = a({href=>url(-absolute=>1)."?conf=$Q->{conf}&act=network_node_view&refresh=$Q->{refresh}&widget=$widget&node=".uri_escape($node), id=>"node_view_$idsafenode"},$LNT->{$node}{name}); + #my $url = "network.pl?conf=$Q->{conf}&act=network_node_view&refresh=$C->{page_refresh_time}&widget=$widget&node=".uri_escape($node); + #a({target=>"NodeDetails-$node", onclick=>"viewwndw(\'$node\',\'$url\',$wd,$ht)"},$LNT->{$node}{name}); + my $issues = join("
",@issueList); + + my $sysObject = "$NI->{system}{sysObjectName} $NI->{system}{sysObjectID}"; + my $intNums = "$intCollect/$intCount"; + + if ( length($sysDescr) > 40 ) { + my $shorter = substr($sysDescr,0,40); + $sysDescr = "$shorter (more...)"; + } - my $wd = 850; - my $ht = 700; - - my $idsafenode = $node; - $idsafenode = (split(/\./,$idsafenode))[0]; - $idsafenode =~ s/[^a-zA-Z0-9_:\.-]//g; - - my $nodelink = a({href=>url(-absolute=>1)."?conf=$Q->{conf}&act=network_node_view&refresh=$Q->{refresh}&widget=$widget&node=".uri_escape($node), id=>"node_view_$idsafenode"},$LNT->{$node}{name}); - #my $url = "network.pl?conf=$Q->{conf}&act=network_node_view&refresh=$C->{page_refresh_time}&widget=$widget&node=".uri_escape($node); - #a({target=>"NodeDetails-$node", onclick=>"viewwndw(\'$node\',\'$url\',$wd,$ht)"},$LNT->{$node}{name}); - my $issues = join("
",@issueList); - - my $sysObject = "$NI->{system}{sysObjectName} $NI->{system}{sysObjectID}"; - my $intNums = "$intCollect/$intCount"; - - if ( length($sysDescr) > 40 ) { - my $shorter = substr($sysDescr,0,40); - $sysDescr = "$shorter (more...)"; - } - - if ( not $filter or ( $filter eq "exceptions" and $exception ) ) - { - $noExceptions = 0; + if ( not $filter or ( $filter eq "exceptions" and $exception ) ) + { + $noExceptions = 0; - my $urlsafegroup = uri_escape($LNT->{$node}->{group}); - print Tr( - td({class => "info Plain"},$nodelink), - td({class => 'info Plain'}, - a({href => url(-absolute=>1)."?conf=$Q->{conf}&act=node_admin_summary&group=$urlsafegroup&refresh=$C->{page_refresh_time}&widget=$widget&filter=$filter"},$LNT->{$node}{group}) - ), - td({class => 'infolft Plain'},$issues), - td({class => $actClass},$LNT->{$node}{active}), - td({class => $lastCollectClass},$lastCollectPoll), - td({class => $lastUpdateClass},$lastUpdatePoll), + my $urlsafegroup = uri_escape($LNT->{$node}->{group}); + print Tr( + td({class => "info Plain"},$nodelink), + td({class => 'info Plain'}, + a({href => url(-absolute=>1)."?conf=$Q->{conf}&act=node_admin_summary&group=$urlsafegroup&refresh=$C->{page_refresh_time}&widget=$widget&filter=$filter"},$LNT->{$node}{group}) + ), + td({class => 'infolft Plain'},$issues), + td({class => $actClass},$LNT->{$node}{active}), + td({class => $lastCollectClass},$lastCollectPoll), + td({class => $lastUpdateClass},$lastUpdatePoll), - td({class => 'info Plain'},$LNT->{$node}{ping}), - td({class => $pingClass},$pingable), + td({class => 'info Plain'},$LNT->{$node}{ping}), + td({class => $pingClass},$pingable), - td({class => 'info Plain'},$LNT->{$node}{collect}), + td({class => 'info Plain'},$LNT->{$node}{collect}), - td({class => $wmiclass},$wmiworks), + td({class => $wmiclass},$wmiworks), - td({class => $snmpClass},$snmpable), - td({class => $commClass},$community), - td({class => 'info Plain'},$LNT->{$node}{version}), + td({class => $snmpClass},$snmpable), + td({class => $commClass},$community), + td({class => 'info Plain'},$LNT->{$node}{version}), - td({class => 'info Plain'},$NI->{system}{nodeVendor}), - td({class => $moduleClass},"$NI->{system}{nodeModel} ($LNT->{$node}{model})"), - td({class => 'info Plain'},$NI->{system}{nodeType}), - td({class => 'info Plain'},$sysObject), - td({class => 'info Plain'},$sysDescr), - td({class => 'info Plain'},$intNums), - ); - } + td({class => 'info Plain'},$NI->{system}{nodeVendor}), + td({class => $moduleClass},"$NI->{system}{nodeModel} ($LNT->{$node}{model})"), + td({class => 'info Plain'},$NI->{system}{nodeType}), + td({class => 'info Plain'},$sysObject), + td({class => 'info Plain'},$sysDescr), + td({class => 'info Plain'},$intNums), + ); } } } diff --git a/cgi-bin/node.pl b/cgi-bin/node.pl index 38bee7a..07ca4ff 100755 --- a/cgi-bin/node.pl +++ b/cgi-bin/node.pl @@ -35,7 +35,7 @@ # use strict; -use List::Util; +use List::Util 1.33; # older versions don't have a usable any() use NMIS; use func; use Sys; @@ -57,6 +57,9 @@ # Before going any further, check to see if we must handle # an authentication login or logout request +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } + # NMIS Authentication module use Auth; my $user; @@ -66,6 +69,8 @@ my $headeropts = {type=>'text/html',expires=>'now'}; my $AU = Auth->new(conf => $C); # Auth::new will reap init values from NMIS::config + + if ($AU->Require) { exit 0 unless $AU->loginout(type=>$Q->{auth_type},username=>$Q->{auth_username}, password=>$Q->{auth_password},headeropts=>$headeropts) ; @@ -117,6 +122,8 @@ sub typeGraph { my $urlsafenode = uri_escape($node); my $urlsafegroup = uri_escape($group); + my $urlsafeindex= uri_escape($index); + my $urlsafeitem = uri_escape($item); my $length; @@ -354,19 +361,20 @@ sub typeGraph { Tr( # Start date field td({class=>'header',align=>'center',colspan=>'1'},"Start", - textfield(-name=>"date_start",-override=>1,-value=>"$date_start",size=>'23')), + textfield(-name=>"date_start",-override=>1,-value=>"$date_start",size=>'23',tabindex=>"1")), # Node select menu td({class=>'header',align=>'center',colspan=>'1'},eval { return hidden(-name=>'node', -default=>$Q->{node},-override=>'1') if $Q->{graphtype} eq 'metrics' or $Q->{graphtype} eq 'nmis'; return "Node ",popup_menu(-name=>'node', -override=>'1', - -values=>[@nodelist], - -default=>"$Q->{node}", - -onChange=>'JavaScript:this.form.submit()'); + tabindex=>"3", + -values=>[@nodelist], + -default=>"$Q->{node}", + -onChange=>'JavaScript:this.form.submit()'); }), # Graphtype select menu td({class=>'header',align=>'center',colspan=>'1'},"Type ", - popup_menu(-name=>'graphtype', -override=>'1', + popup_menu(-name=>'graphtype', -override=>'1', tabindex=>"4", -values=>[sort keys %{$GTT}], -default=>"$Q->{graphtype}", -onChange=>'JavaScript:this.form.submit()')), @@ -377,72 +385,72 @@ sub typeGraph { Tr( # End date field td({class=>'header',align=>'center',colspan=>'1'},"End ", - textfield(-name=>"date_end",-override=>1,-value=>"$date_end",size=>'23')), + textfield(-name=>"date_end",-override=>1,-value=>"$date_end",size=>'23',tabindex=>"2")), # Group or Interface select menu td({class=>'header',align=>'center',colspan=>'1'}, eval { return hidden(-name=>'intf', -default=>$Q->{intf},-override=>'1') if $Q->{graphtype} eq 'nmis'; if ( $Q->{graphtype} eq "metrics") { - return "Group ",popup_menu(-name=>'group', -override=>'1',-size=>'1', + return "Group ",popup_menu(-name=>'group', -override=>'1',-size=>'1', tabindex=>"5", -values=>[grep $AU->InGroup($_), 'network',sort keys %{$GT}], -default=>"$group", -onChange=>'JavaScript:this.form.submit()'), hidden(-name=>'intf', -default=>$Q->{intf},-override=>'1'); } elsif ($Q->{graphtype} eq "hrsmpcpu") { - return "CPU ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "CPU ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort $S->getTypeInstances(graphtype => "hrsmpcpu")], -default=>"$index", -onChange=>'JavaScript:this.form.submit()'); } elsif ($Q->{graphtype} =~ /service|service-cpumem|service-response/) { - return "Service ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Service ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort $S->getTypeInstances(section => "service")], -default=>"$index", -onChange=>'JavaScript:this.form.submit()'); } elsif ($Q->{graphtype} eq "hrdisk") { my @disks = $S->getTypeInstances(graphtype => "hrdisk"); - return "Disk ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Disk ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @disks], -default=>"$index", -labels=>{ map{($_ => $NI->{storage}{$_}{hrStorageDescr})} sort @disks }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($GTT->{$graphtype} eq "env_temp") { my @sensors = $S->getTypeInstances(graphtype => "env_temp"); - return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @sensors], -default=>"$index", -labels=>{ map{($_ => $NI->{env_temp}{$_}{tempDescr})} sort @sensors }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($GTT->{$graphtype} eq "akcp_temp") { my @sensors = $S->getTypeInstances(graphtype => "akcp_temp"); - return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @sensors], -default=>"$index", -labels=>{ map{($_ => $NI->{akcp_temp}{$_}{hhmsSensorTempDescr})} sort @sensors }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($GTT->{$graphtype} eq "akcp_hum") { my @sensors = $S->getTypeInstances(graphtype => "akcp_hum"); - return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @sensors], -default=>"$index", -labels=>{ map{($_ => $NI->{akcp_hum}{$_}{hhmsSensorHumDescr})} sort @sensors }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($GTT->{$graphtype} eq "cssgroup") { my @cssgroup = $S->getTypeInstances(graphtype => "cssgroup"); - return "Group ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Group ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @cssgroup], -default=>"$index", -labels=>{ map{($_ => $NI->{cssgroup}{$_}{CSSGroupDesc})} sort @cssgroup }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($GTT->{$graphtype} eq "csscontent") { my @csscont = $S->getTypeInstances(graphtype => "csscontent"); - return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Sensor ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort @csscont], -default=>"$index", -labels=>{ map{($_ => $NI->{csscontent}{$_}{CSSContentDesc})} sort @csscont }, -onChange=>'JavaScript:this.form.submit()'); } elsif ($systemHealth) { - return "$systemHealthTitle ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "$systemHealthTitle ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['',sort keys %{$NI->{$systemHealthSection}}], -default=>"$index", -labels=>{ @systemHealthLabels }, @@ -458,7 +466,7 @@ sub typeGraph { if (not grep { $_ eq $index } @wantedifs ) { push(@wantedifs, $index); } - return "Interface ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', + return "Interface ",popup_menu(-name=>'intf', -override=>'1',-size=>'1', tabindex=>"5", -values=>['', @wantedifs], -default=>"$index", -labels=>{ map{($_ => $IF->{$_}{ifDescr})} @wantedifs }, @@ -472,15 +480,15 @@ sub typeGraph { foreach my $gtp (keys %graph_button_table) { foreach my $gt (keys %{$GTT}) { if ($gtp eq $gt) { - push @out,a({class=>'button',href=>url(-absolute=>1)."?$cg&act=network_graph_view&graphtype=$gtp"},$graph_button_table{$gtp}); + push @out,a({class=>'button', tabindex=>"-1", href=>url(-absolute=>1)."?$cg&act=network_graph_view&graphtype=$gtp"},$graph_button_table{$gtp}); } } } if (not($graphtype =~ /cbqos|calls/ and $Q->{item} eq '')) { - push @out,a({class=>'button',href=>url(-absolute=>1)."?$cg&act=network_export&graphtype=$Q->{graphtype}"},"Export"); - push @out,a({class=>'button',href=>url(-absolute=>1)."?$cg&act=network_stats&graphtype=$Q->{graphtype}"},"Stats"); + push @out,a({class=>'button', tabindex=>"-1", href=>url(-absolute=>1)."?$cg&act=network_export&graphtype=$Q->{graphtype}"},"Export"); + push @out,a({class=>'button', tabindex=>"-1", href=>url(-absolute=>1)."?$cg&act=network_stats&graphtype=$Q->{graphtype}"},"Stats"); } - push @out,a({class=>'button',href=>url(-absolute=>1)."?$cg&act=network_graph_view&graphtype=nmis"},"NMIS"); + push @out,a({class=>'button', tabindex=>"-1", href=>url(-absolute=>1)."?$cg&act=network_graph_view&graphtype=nmis"},"NMIS"); return @out; })) )))); @@ -575,7 +583,7 @@ sub typeGraph { } my $graphLink="$C->{'rrddraw'}?conf=$Q->{conf}&act=draw_graph_view". - "&node=$urlsafenode&group=$urlsafegroup&graphtype=$graphtype&start=$start&end=$end&width=$width&height=$height&intf=$index&item=$item"; + "&node=$urlsafenode&group=$urlsafegroup&graphtype=$graphtype&start=$start&end=$end&width=$width&height=$height&intf=$urlsafeindex&item=$urlsafeitem"; my $chartDiv = ""; if( getbool($C->{display_opcharts}) ) { $chartDiv = qq |
|; @@ -585,7 +593,7 @@ sub typeGraph { if( getbool($C->{display_opcharts}) ) { push @output, Tr(td({class=>'info Plain',align=>'center',colspan=>'4'}, $chartDiv)); } else { - push @output, Tr(td({class=>'info Plain',align=>'center',colspan=>'4'},image_button(-name=>'graphimg',-src=>"$graphLink",-align=>'MIDDLE'))); + push @output, Tr(td({class=>'info Plain',align=>'center',colspan=>'4'},image_button(-name=>'graphimg',-src=>"$graphLink",-align=>'MIDDLE', -tabindex=>"-1"))); push @output, Tr(td({class=>'info Plain',align=>'center',colspan=>'4'},"Clickable graphs: Left -> Back; Right -> Forward; Top Middle -> Zoom In; Bottom Middle-> Zoom Out, in time")); } } diff --git a/cgi-bin/nodeconf.pl b/cgi-bin/nodeconf.pl index c0527cc..f11749d 100755 --- a/cgi-bin/nodeconf.pl +++ b/cgi-bin/nodeconf.pl @@ -27,36 +27,50 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -# Auto configure to the /lib +use strict; + use FindBin; use lib "$FindBin::Bin/../lib"; -use strict; +use CGI qw(:standard *table *Tr *td *form *Select *div); + use NMIS; use func; - -use Data::Dumper; -$Data::Dumper::Indent = 1; - -use CGI qw(:standard *table *Tr *td *form *Select *div); +use Auth; my $q = new CGI; # This processes all parameters passed via GET and POST my $Q = $q->Vars; # values in hash -my $C; -if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; +my $C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}); +die "failed to load configuration!\n" if (!$C or ref($C) ne "HASH" or !keys %$C); + +#====================================================================== + +# if somehow someone defines refresh disable it. +if ( defined $Q->{refresh} ) { + delete $Q->{refresh}; +} -# widget default on, only off if explicitely set to off -my $wantwidget = !getbool($Q->{widget},"invert"); -my$widget = $wantwidget ? "true" : "false"; +my $widget = getbool($Q->{widget},"invert") ? 'false' : 'true'; +$Q->{expand} = "true" if ($widget eq "true"); + +### unless told otherwise, and this is not JQuery call, widget is false! +if ( not defined $Q->{widget} and not defined $ENV{HTTP_X_REQUESTED_WITH} ) { + $widget = "false"; +} + +if ( not defined $ENV{HTTP_X_REQUESTED_WITH} ) { + $widget = "false"; +} + +my $wantwidget = ($widget eq "true"); my $formid = 'nodeconf'; # Before going any further, check to see if we must handle # an authentication login or logout request - -# NMIS Authentication module -use Auth; +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth # variables used for the security mods my $headeropts = {type => 'text/html', expires => 'now'}; @@ -150,8 +164,6 @@ sub displayNodemenu print end_td,end_Tr,end_table; - htmlElementValues(); - pageEnd if (!$wantwidget); } @@ -465,12 +477,27 @@ sub updateNodeConf { # event, collect and threshold are special: # value "unchanged" means remove the override if (($source =~ /^(collect|event|threshold)_/ and $Q->{$source} eq "unchanged") - or !$Q->{$source}) # others: no value given means remove + # others: no value given means remove + or !defined($Q->{$source}) + or $Q->{$source} eq "") { delete $thisintfover->{$target}; } else { + # speed in and out are a bit more equal than others, too: + # if present they must be positive integers + if ($source =~ /^speed(In|Out)?_/) + { + my $theval = $Q->{$source}; + + if (int($theval) ne $theval or $theval <= 0) + { + print header(-status => 400, %$headeropts), + "Validation error for $source: '$theval' is not a positive integer!"; + return undef; + } + } $thisintfover->{$target} = $Q->{$source}; } } diff --git a/cgi-bin/outages.pl b/cgi-bin/outages.pl index 1b357d7..081d8bf 100755 --- a/cgi-bin/outages.pl +++ b/cgi-bin/outages.pl @@ -3,39 +3,42 @@ ## $Id: outages.pl,v 8.5 2012/04/28 00:59:36 keiths Exp $ # # Copyright (C) Opmantek Limited (www.opmantek.com) -# +# # ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM -# +# # This file is part of Network Management Information System (“NMIS”). -# +# # NMIS is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. -# +# # NMIS 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 General Public License for more details. -# +# # You should have received a copy of the GNU General Public License -# along with NMIS (most likely in a file named LICENSE). +# along with NMIS (most likely in a file named LICENSE). # If not, see -# +# # For further information on NMIS or for a license other than GPL please see -# www.opmantek.com or email contact@opmantek.com -# +# www.opmantek.com or email contact@opmantek.com +# # User group details: # http://support.opmantek.com/users/ -# +# # ***************************************************************************** # Auto configure to the /lib use FindBin; use lib "$FindBin::Bin/../lib"; -# +# use strict; use Time::ParseDate; +use JSON::XS; +use POSIX; + use NMIS; use Sys; use func; @@ -90,20 +93,27 @@ sub notfound { #=================== -sub viewOutage { - +sub viewOutage +{ my @out; my $node = $Q->{node}; - + my $title = $node? "Outages for $node" : "List of Outages"; my $time = time(); print header($headeropts); pageStartJscript(title => $title, refresh => 86400) if (!$wantwidget); - - my $OT = loadOutageTable(); + my $NT = loadNodeTable(); + my $res = NMIS::find_outages(); # attention: cannot filter by affected node + if (!$res->{success}) + { + $Q->{error} = "Cannot find outages: $res->{error}"; + return; + } + my @outages = @{$res->{outages}}; + my $S = Sys::->new; $S->init(name=>$node,snmp=>'false'); @@ -114,22 +124,14 @@ sub viewOutage { . hidden(-override => 1, -name => "act", -value => "outage_table_doadd") . hidden(-override => 1, -name => "widget", -value => $widget); - print createHrButtons(node=>$node, system=>$S, refresh=>$Q->{refresh},widget=>$widget, conf => $Q->{conf}, AU => $AU); + # doesn't make sense to run the bar creator if it can't create any output anyway... + print createHrButtons(node=>$node, system=>$S, refresh=>$Q->{refresh},widget=>$widget, conf => $Q->{conf}, AU => $AU) + if ($node); print start_table; if ($AU->CheckAccess("Table_Outages_rw",'check')) { - print Tr(td({class=>'header',colspan=>'6'},'Add Outage')); - - print Tr( - td({class=>'header',align=>'center'},'Node'), - td({class=>'header',align=>'center'},'Start'), - td({class=>'header',align=>'center'},'End'), - td({class=>'header',align=>'center'},'Change'), - td({class=>'header',align=>'center',colspan=>'2'},'Action') - ); - my $start = $time+300; my $end = $time+3600; my $change = 'ticket #'; @@ -138,68 +140,113 @@ sub viewOutage { $end = $Q->{end}; $change = $Q->{change}; } + my @nodes = grep { $AU->InGroup($NT->{$_}{group}) } sort {lc $a cmp lc $b} keys %{$NT}; my @nd = split(/,/,$node); + + print Tr(td({class=>'header',colspan=>'3'},'Add Planned Outage')); print Tr( - td({class=>'info'}, - scrolling_list(-name=>'node',-multiple=>'true',-size=>'12',override=>'1',-values=>\@nodes,-default=>\@nd) ), - td({class=>'info'}, + td({class=>'header',align=>'left'},'Planned Outage Start'), + td({class=>'info',colspan=>'2'}, textfield(-name=>'start',-id=>'id_start',-style=>'background-color:yellow;width:100%;',override=>'1', - -value=>returnDateStamp($start)),div({-id=>'calendar-start'}) ), - td({class=>'info'}, + -value=>returnDateStamp($start)),div({-id=>'calendar-start'}) ) + ); + + print Tr( + td({class=>'header',align=>'left'},'Planned Outage End'), + td({class=>'info',colspan=>'2'}, textfield(-name=>'end',-id=>'id_end',-style=>'background-color:yellow;width:100%;',override=>'1', - -value=>returnDateStamp($end)),div({-id=>'calendar-end'}) ), - td({class=>'info'}, - textfield(-name=>'change',-style=>'background-color:yellow;width:200px;',override=>'1',-value=>$change)), - td({class=>'info',colspan=>'2',align=>'center'}, + -value=>returnDateStamp($end)),div({-id=>'calendar-end'}) ) + ); + + print Tr( + td({class=>'header',align=>'left'},'Related Change Details'), + td({class=>'info',colspan=>'2'}, + textfield(-name=>'change',-style=>'background-color:yellow;width:200px;',override=>'1',-value=>$change)) + ); + + print Tr( + td({class=>'header',align=>'left'},'Select Node or Nodes'), + td({class=>'info',colspan=>'2'}, + scrolling_list(-name=>'node',-multiple=>'true',-size=>'12',override=>'1',-values=>\@nodes,-default=>\@nd) ) + ); + + print Tr( + td({class=>'header',align=>'left'},'Action'), + td({class=>'info',align=>'center',colspan=>'2'}, button(-name=>'button',-onclick=> ($wantwidget? "get('nmisOutages');" : "submit()"), -value=>"Add")) ); + if ($Q->{error} ne '') { - print Tr(td({class=>'error',colspan=>'6'},$Q->{error})); + print Tr(td({class=>'error',colspan=>'3'},$Q->{error})); } } + print Tr(td({class=>'info',colspan=>'2'},' ')); + #==== my $hd = ($node ne "") ? "Outage Table of Node $node" : "Outage Table"; print Tr(td({class=>'header',colspan=>'6'},$hd)); push @out, Tr( - td({class=>'header',align=>'center'},'Node'), + td({class=>'header',align=>'center'},'Node Selector'), td({class=>'header',align=>'center'},'Start'), td({class=>'header',align=>'center'},'End'), td({class=>'header',align=>'center'},'Change'), td({class=>'header',align=>'center'},'Status'), td({class=>'header',align=>'center'},'Action') - ); - foreach my $ot (sortall($OT,'start','rev')) { - next unless $AU->InGroup($NT->{$OT->{$ot}{node}}{group}); - next if $Q->{node} ne '' and $node !~ /$OT->{$ot}{node}/; - - my $outage = 'closed'; - my $color = "#FFFFFF"; - if ($OT->{$ot}{start} <= $time and $OT->{$ot}{end} >= $time) { - $outage = 'current'; - $color = "#00FF00"; - } elsif ($OT->{$ot}{start} >= $time) { - $outage = 'pending'; - $color = "#FFFF00"; + ); + + for my $outage (@outages) + { + + # no coloring/status for anything but non-recurring+current ones + my ($status,$color) = ($outage->{frequency},"white"); + + if ($outage->{frequency} eq "once") + { + if ($time >= $outage->{end}) + { + $status = 'closed'; + $color = "#FFFFFF"; + } + elsif ($time < $outage->{start}) + { + $status = "pending"; + } + else + { + $status = 'current'; + $color = "#00FF00"; + } } + # very rough stringification of the of the selector + my $visual = JSON::XS->new->encode($outage->{selector}); + push @out, Tr( - td({class=>'info',style=>getBGColor($color)},$NT->{$OT->{$ot}{node}}{name}), - td({class=>'info',style=>getBGColor($color)},returnDateStamp($OT->{$ot}{start})), - td({class=>'info',style=>getBGColor($color)},returnDateStamp($OT->{$ot}{end})), - td({class=>'info',style=>getBGColor($color)},$OT->{$ot}{change}), - td({class=>'info',style=>getBGColor($color)},$outage), - td({class=>'info'},a({href=>url(-absolute=>1)."?conf=$Q->{conf}&act=outage_table_dodelete&hash=$ot&widget=$widget"},'delete')) - ); + td({class=>'info',style=>getBGColor($color)}, $visual), + + # one-offs have start/end in unix seconds, make them friendlier for viewing + td({class=>'info',style=>getBGColor($color)}, $outage->{start} =~ /^\d+(\.\d+)?$/? + POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime($outage->{start})) : $outage->{start}), + + td({class=>'info',style=>getBGColor($color)}, $outage->{end} =~ /^\d+(\.\d+)?$/? + POSIX::strftime("%Y-%m-%dT%H:%M:%S", localtime($outage->{end})) : $outage->{end}), + + td({class=>'info',style=>getBGColor($color)}, $outage->{change_id}), + td({class=>'info',style=>getBGColor($color)}, $status), + td({class=>'info'},a({href=>url(-absolute=>1)."?conf=$Q->{conf}&act=outage_table_dodelete&id=$outage->{id}&widget=$widget"},'delete')) + ); } - if ($#out > 0) { + + if (@out) + { print @out; } else { - print Tr(td({class=>'info',colspan=>'5'},'No outage current',eval { return " of Node $node" if $node ne '';})); + print Tr(td({class=>'info',colspan=>'6'}, 'No outage current' . ($node ne ''? " of Node $node": ""))); } print end_table; @@ -245,6 +292,7 @@ sub doaddOutage { $AU->CheckAccess("Table_Outages_rw",'header'); my $node = $Q->{node}; + my $start = parsedate($Q->{start}); # convert to number of seconds my $end = parsedate($Q->{end}); my $change = $Q->{change}; @@ -268,32 +316,40 @@ sub doaddOutage { return; } - $change =~ s/,//g; # remove comma + $Q->{node} = ''; # fixme: what is that for?? - my ($OT,$handle) = loadTable(dir=>'conf',name=>'Outage',lock=>'true'); + $change =~ s/,//g; # remove comma to appease brittle event log system - # process multiple node select - foreach my $nd ( split(/,/,$node) ) { - my $outageHash = "$nd-$start-$end"; # key - $OT->{$outageHash}{node} = $nd; - $OT->{$outageHash}{start} = $start; - $OT->{$outageHash}{end} = $end; - $OT->{$outageHash}{change} = $change; - $OT->{$outageHash}{user} = $AU->User(); - } + # process multiple node selection - which arrives \0-packed if POSTed, ie. nonwidget, + # or comma separated in widget mode + my $sep = $wantwidget? qr/\s*,\s*/ : qr/\0/; + my @nodes = split( $sep, $node); - writeTable(dir=>'conf',name=>'Outage',data=>$OT,handle=>$handle); + my $res = NMIS::update_outage(frequency => "once", + change_id => $change, + start => $start, + end => $end, + meta => { user => $AU->User }, + selector => { node => + { name => + (@nodes > 1? \@nodes : $nodes[0]) } }); # array only if more than one - $Q->{node} = ''; + if (!$res->{success}) + { + $Q->{error} = "Failed to create outage: $res->{error}"; + return; + } } +# requires the outage id sub dodeleteOutage { - $AU->CheckAccess("Table_Outages_rw",'header'); - outageRemove(key=>$Q->{hash}); + $Q->{node} = ''; # fixme what is that for? - - $Q->{node} = ''; + my $res = NMIS::remove_outage(id => $Q->{id}, meta => { user => $AU->User } ); + if (!$res->{success}) + { + $Q->{error} = "Failed to delete outage $Q->{id}: $res->{error}"; + } } - diff --git a/cgi-bin/reports.pl b/cgi-bin/reports.pl index 71c3c84..a98d26a 100755 --- a/cgi-bin/reports.pl +++ b/cgi-bin/reports.pl @@ -71,7 +71,8 @@ # if no options, assume called from web interface .... my $outputfile; -if ( $#ARGV > 0 ) { +if ( @ARGV ) +{ my %nvp = getArguements(@ARGV); $Q->{act} = $nvp{report} ? "report_dynamic_$nvp{report}" : "report_dynamic_health"; @@ -1382,7 +1383,6 @@ sub outageReport my $datestamp_end = returnDateStamp($end); my $NT = loadNodeTable(); - my $OT = loadOutageTable(); my $index; my %logreport; diff --git a/cgi-bin/rrddraw.pl b/cgi-bin/rrddraw.pl index ef62949..51e4b38 100755 --- a/cgi-bin/rrddraw.pl +++ b/cgi-bin/rrddraw.pl @@ -50,7 +50,9 @@ my $C; if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; -$C->{auth_require} = 0; # bypass auth + +# bypass auth iff called from command line +$C->{auth_require} = 0 if (@ARGV); # NMIS Authentication module use Auth; @@ -93,7 +95,7 @@ sub error { # produce one graph # args: pretty much all coming from a global $Q object # returns: rrds::graph result array -sub rrdDraw +sub rrdDraw { my %args = @_; @@ -120,12 +122,12 @@ sub rrdDraw ### 2012-02-06 keiths, handling default graph length # default is hours! my $graphlength = $C->{graph_amount}; - if ( $C->{graph_unit} eq "days" ) + if ( $C->{graph_unit} eq "days" ) { $graphlength = $C->{graph_amount} * 24; } - if ( $start eq "" or $start == 0) + if ( $start eq "" or $start == 0) { $start = time() - ($graphlength*3600); } @@ -141,26 +143,26 @@ sub rrdDraw my @opt; my $db; - if ($graphtype eq 'metrics') + if ($graphtype eq 'metrics') { $item = $Q->{group}; $intf = ""; } # special graphtypes: cbqos is dynamic (multiple inputs create one graph), ditto calls - if ($graphtype =~ /cbqos/) + if ($graphtype =~ /cbqos/) { @opt = graphCBQoS(sys=>$S, graphtype=>$graphtype, intf=>$intf, item=>$item, start=>$start,end=>$end,width=>$width,height=>$height); - } - elsif ($graphtype eq "calls") + } + elsif ($graphtype eq "calls") { @opt = graphCalls(sys=>$S,graphtype=>$graphtype,intf=>$intf,item=>$item,start=>$start,end=>$end,width=>$width,height=>$height); - } - else + } + else { if (!($db = $S->getDBName(graphtype=>$graphtype,index=>$intf,item=>$item)) ) { # get database name from node info error(); @@ -169,7 +171,7 @@ sub rrdDraw my $graph; if (!($graph = loadTable(dir=>'models',name=>"Graph-$graphtype")) - or !keys %$graph ) + or !keys %$graph ) { logMsg("ERROR failed to read Graph-$graphtype!"); error(); @@ -188,11 +190,11 @@ sub rrdDraw $size = 'small' if $width <= 400 and $graph->{option}{small} ne ""; - if (($ttl = $graph->{title}{$title}) eq "") + if (($ttl = $graph->{title}{$title}) eq "") { logMsg("no title->$title found in Graph-$graphtype"); } - if (($lbl = $graph->{vlabel}{$vlabel}) eq "") + if (($lbl = $graph->{vlabel}{$vlabel}) eq "") { logMsg("no vlabel->$vlabel found in Graph-$graphtype"); } @@ -243,10 +245,11 @@ sub rrdDraw { # scalars must be global - no strict; # *shudder* - if ($intf ne "") + no strict; # *shudder* this is so utterly wrong and broken + if ($intf ne "") { $indx = $intf; + $ifDescr = $IF->{$intf}{ifDescr}; $ifSpeed = $IF->{$intf}{ifSpeed}; $ifSpeedIn = $IF->{$intf}{ifSpeed}; @@ -269,10 +272,7 @@ sub rrdDraw $datestamp_end = returnDateStamp($end); $datestamp = returnDateStamp(time); $database = $db; - - # escape any : chars which might be in the database name e.g handling C: in the RPN - $database =~ s/:/\\:/g; - + $group = $grp; $itm = $item; $length = $l; @@ -280,9 +280,26 @@ sub rrdDraw $GLINE = getbool($C->{graph_split}) ? "AREA" : "LINE1" ; $weight = 0.983; - foreach my $str (@opt) { - $str =~ s{\$(\w+)}{if(defined${$1}){${$1};}else{"ERROR, no variable \'\$$1\' ";}}egx; - if ($str =~ /ERROR/) { + for my $idx (0..$#opt) + { + my $str = $opt[$idx]; + + # escape any ':' chars which might be in the database name (e.g C:\\) or the other + # inputs (e.g. indx == service name). this must be done for ALL substitutables, + # but no thanks to no strict we don't exactly know who they are, nor can we safely change + # their values without side-effects...so we do it on the go, and only where not already pre-escaped. + + # EXCEPT in --title, where we can't have colon escaping. grrrrrr! + if ($idx > 0 && $opt[$idx-1] eq "--title") + { + $str =~ s{\$(\w+)}{if(defined${$1}){${$1};}else{"ERROR, no variable \'\$$1\' ";}}egx; + } + else + { + $str =~ s{\$(\w+)}{if(defined${$1}){NMIS::postcolonial(${$1});}else{"ERROR, no variable \'\$$1\' ";}}egx; + } + + if ($str =~ /ERROR/) { logMsg("ERROR in expanding variables, $str"); return; } @@ -303,13 +320,14 @@ sub rrdDraw ($graphret,$xs,$ys) = RRDs::graph($filename, @options); } - if ($ERROR = RRDs::error) + if ($ERROR = RRDs::error) { logMsg("$db Graphing Error for $graphtype: $ERROR"); } return $graphret; } + # CBQoS Support # this handles both cisco and huawei flavour cbqos sub graphCBQoS @@ -326,14 +344,14 @@ sub graphCBQoS my $width = $args{width}; my $height = $args{height}; my $debug = $Q->{debug}; - + my $database; my @opt; my $title; - + # order the names, find colors and bandwidth limits, index and section names my ($CBQosNames,$CBQosValues) = NMIS::loadCBQoS(sys=>$S, graphtype=>$graphtype, index=>$intf); - + if ( $item eq "" ) { # display all class-maps in one graph my $i; @@ -352,7 +370,7 @@ sub graphCBQoS } else { $title = "$NI->{name} $ifDescr $direction - CBQoS from ".'$datestamp_start to $datestamp_end'; } - + @opt = ( "--title", $title, "--vertical-label",$vlabel, @@ -373,21 +391,21 @@ sub graphCBQoS "--color", 'ARROW#924040', # Arrow Color for X/Y Axis "--color", 'FRAME#808080' # Canvas Frame Color ); - + if ($width > 400) { push(@opt,"--font", $C->{graph_default_font_standard}) if $C->{graph_default_font_standard}; } else { push(@opt,"--font", $C->{graph_default_font_small}) if $C->{graph_default_font_small}; } - + # calculate the sum (avg and max) of all Classmaps for PrePolicy and Drop # note that these CANNOT be graphed by themselves, as 0 isn't a valid RPN expression in rrdtool $avgppr = "CDEF:avgPrePolicyBitrate=0"; $maxppr = "CDEF:maxPrePolicyBitrate=0"; $avgdbr = "CDEF:avgDropBitrate=0"; $maxdbr = "CDEF:maxDropBitrate=0"; - + # is this hierarchical or flat? my $HQOS = 0; foreach my $i (1..$#$CBQosNames) { @@ -395,14 +413,14 @@ sub graphCBQoS $HQOS = 1; } } - + my $gtype = "AREA"; my $gcount = 0; my $parent_name = ""; foreach my $i (1..$#$CBQosNames) { my $thisinfo = $CBQosValues->{$intf.$CBQosNames->[$i]}; - + $database = $S->getDBName(graphtype => $thisinfo->{CfgSection}, index => $thisinfo->{CfgIndex}, item => $CBQosNames->[$i] ); @@ -411,12 +429,12 @@ sub graphCBQoS $parent = 1; $gtype = "LINE1"; } - + if ( $CBQosNames->[$i] =~ /^([\w\-]+)\-\-\w+\-\-/ ) { $parent_name = $1; print STDERR "DEBUG parent_name=$parent_name\n" if ($debug); } - + if ( not $parent and not $gcount) { $gtype = "AREA"; ++$gcount; @@ -428,7 +446,7 @@ sub graphCBQoS my $alias = $CBQosNames->[$i]; $alias =~ s/$parent_name\-\-//g; $alias =~ s/\-\-/\//g; - + # rough alignment for the columns, necessarily imperfect # as X-char strings aren't equally wide... my $tab = "\\t"; @@ -441,19 +459,19 @@ sub graphCBQoS elsif ( length($alias) <= 19 ) { $tab = $tab x 2; } - + my $color = $CBQosValues->{$intf.$CBQosNames->[$i]}{'Color'}; - + push(@opt,"DEF:avgPPB$i=$database:".$thisinfo->{CfgDSNames}->[0].":AVERAGE"); push(@opt,"DEF:maxPPB$i=$database:".$thisinfo->{CfgDSNames}->[0].":MAX"); push(@opt,"DEF:avgDB$i=$database:".$thisinfo->{CfgDSNames}->[2].":AVERAGE"); push(@opt,"DEF:maxDB$i=$database:".$thisinfo->{CfgDSNames}->[2].":MAX"); - + push(@opt,"CDEF:avgPPR$i=avgPPB$i,8,*"); push(@opt,"CDEF:maxPPR$i=maxPPB$i,8,*"); push(@opt,"CDEF:avgDBR$i=avgDB$i,8,*"); push(@opt,"CDEF:maxDBR$i=maxDB$i,8,*"); - + if ($width > 400) { push @opt,"$gtype:avgPPR$i#$color:$alias$tab"; push(@opt,"GPRINT:avgPPR$i:AVERAGE:Avg %8.2lf%s\\t"); @@ -464,7 +482,7 @@ sub graphCBQoS else { push(@opt,"$gtype:avgPPR$i#$color:$alias"); } - + #push(@opt,"LINE1:avgPPR$i#$color:$CBQosNames->[$i]"); $avgppr = $avgppr.",avgPPR$i,+"; $maxppr = $maxppr.",maxPPR$i,+"; @@ -475,7 +493,7 @@ sub graphCBQoS push(@opt,$maxppr); push(@opt,$avgdbr); push(@opt,$maxdbr); - + if ($width > 400) { push(@opt,"COMMENT:\\l"); push(@opt,"GPRINT:avgPrePolicyBitrate:AVERAGE:PrePolicyBitrate\\t\\t\\tAvg %8.2lf%s\\t"); @@ -483,24 +501,24 @@ sub graphCBQoS push(@opt,"GPRINT:avgDropBitrate:AVERAGE:DropBitrate\\t\\t\\tAvg %8.2lf%s\\t"); push(@opt,"GPRINT:maxDropBitrate:MAX:Max\\t%8.2lf%s\\l"); } - + } else { # display ONLY the selected class-map my $thisinfo = $CBQosValues->{$intf.$item}; - + my $speed = defined $thisinfo->{CfgRate}? &convertIfSpeed($thisinfo->{'CfgRate'}) : undef; my $direction = ($graphtype eq "cbqos-in") ? "input" : "output" ; - + $database = $S->getDBName(graphtype => $thisinfo->{CfgSection}, index => $thisinfo->{CfgIndex}, item => $item ); - + # in this case we always use the FIRST color, not the one for this item my $color = $CBQosValues->{$intf.$CBQosNames->[1]}->{'Color'}; - + my $ifDescr = shortInterface($IF->{$intf}{ifDescr}); $title = "$ifDescr $direction - $item from ".'$datestamp_start to $datestamp_end'; - + @opt = ( "--title", "$title", "--vertical-label", 'Avg Bits per Second', @@ -521,14 +539,14 @@ sub graphCBQoS "--color", 'ARROW#924040', # Arrow Color for X/Y Axis "--color", 'FRAME#808080', # Canvas Frame Color ); - + if ($width > 400) { push(@opt,"--font", $C->{graph_default_font_standard}) if $C->{graph_default_font_standard}; } else { push(@opt,"--font", $C->{graph_default_font_small}) if $C->{graph_default_font_small}; } - + # needs to work for both types of qos, hence uses the CfgDSNames push @opt, ( "DEF:PrePolicyByte=$database:".$thisinfo->{CfgDSNames}->[0].":AVERAGE", @@ -537,11 +555,11 @@ sub graphCBQoS "DEF:maxDropByte=$database:".$thisinfo->{CfgDSNames}->[2].":MAX", "DEF:PrePolicyPkt=$database:".$thisinfo->{CfgDSNames}->[3].":AVERAGE", "DEF:DropPkt=$database:".$thisinfo->{CfgDSNames}->[5].":AVERAGE"); - + # huawei doesn't have NoBufDropPkt push @opt, "DEF:NoBufDropPkt=$database:".$thisinfo->{CfgDSNames}->[6].":AVERAGE" if (defined $thisinfo->{CfgDSNames}->[6]); - + push @opt, ( "CDEF:PrePolicyBitrate=PrePolicyByte,8,*", "CDEF:maxPrePolicyBitrate=maxPrePolicyByte,8,*", @@ -549,7 +567,7 @@ sub graphCBQoS "TEXTALIGN:left", "AREA:PrePolicyBitrate#$color:PrePolicyBitrate", ); - + # detailed legends are only shown on the 'big' graphs if ($width > 400) { push(@opt,"GPRINT:PrePolicyBitrate:AVERAGE:\\tAvg %8.2lf %sbps\\t"); @@ -557,30 +575,30 @@ sub graphCBQoS } # move back to previous line, then right-align push @opt, "COMMENT:\\u", "AREA:DropBitrate#ff0000:DropBitrate\\r:STACK"; - + if ($width > 400) { push(@opt,"GPRINT:PrePolicyByte:AVERAGE:Bytes transferred\\t\\tAvg %8.2lf %sB/s\\n"); - + push(@opt,"GPRINT:DropByte:AVERAGE:Bytes dropped\\t\\t\\tAvg %8.2lf %sB/s\\t"); push(@opt,"GPRINT:maxDropByte:MAX:Max %8.2lf %sB/s\\n"); - + push(@opt,"GPRINT:PrePolicyPkt:AVERAGE:Packets transferred\\t\\tAvg %8.2lf\\l"); push(@opt,"GPRINT:DropPkt:AVERAGE:Packets dropped\\t\\t\\tAvg %8.2lf"); - + # huawei doesn't have that push(@opt,"COMMENT:\\l","GPRINT:NoBufDropPkt:AVERAGE:Packets No buffer dropped\\tAvg %8.2lf\\l") if (defined $thisinfo->{CfgDSNames}->[6]); - + # not all qos setups have a graphable bandwidth limit push @opt, "COMMENT:\\u", "COMMENT:".$thisinfo->{CfgType}." $speed\\r" if (defined $speed); } } - + return @opt; } -sub graphCalls +sub graphCalls { my %args = @_; my $S = $args{sys}; @@ -677,4 +695,3 @@ sub graphCalls return @opt; } - diff --git a/cgi-bin/tables.pl b/cgi-bin/tables.pl index 690df42..2d3a4a3 100755 --- a/cgi-bin/tables.pl +++ b/cgi-bin/tables.pl @@ -27,47 +27,49 @@ # http://support.opmantek.com/users/ # # ***************************************************************************** -# Auto configure to the /lib +use strict; +our $VERSION="8.6.2G"; + use FindBin; use lib "$FindBin::Bin/../lib"; -# -use strict; +use CGI qw(:standard *table *Tr *td *form *Select *div); +use Data::Dumper; +use URI::Escape; +use Net::IP; + use NMIS; use NMIS::UUID; use Sys; use func; use csv; -use Net::hostent; -use Socket; -use Data::Dumper; -use URI::Escape; +use Auth; use DBfunc; -use CGI qw(:standard *table *Tr *td *form *Select *div); - my $q = new CGI; # This processes all parameters passed via GET and POST my $Q = $q->Vars; # values in hash -my $C; +my $C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}); -if (!($C = loadConfTable(conf=>$Q->{conf},debug=>$Q->{debug}))) { exit 1; }; +die "failed to load configuration!\n" if (!$C or ref($C) ne "HASH" or !keys %$C); -# Before going any further, check to see if we must handle -# an authentication login or logout request +# if arguments present, then called from command line +if ( @ARGV ) { $C->{auth_require} = 0; } # bypass auth -# NMIS Authentication module -use Auth; # variables used for the security mods my $headeropts = {type=>'text/html',expires=>'now'}; -my $AU = Auth->new(conf => $C); # Auth::new will reap init values from NMIS::config +my $AU = Auth->new(conf => $C); if ($AU->Require) { exit 0 unless $AU->loginout(type=>$Q->{auth_type},username=>$Q->{auth_username}, password=>$Q->{auth_password},headeropts=>$headeropts) ; } - +else +{ + # that's the command line/debugger scenario, where we assume a full admin + $AU->SetUser("nmis"); +} # check for remote request if ($Q->{server} ne "") { exit if requestServer(headeropts=>$headeropts); } @@ -77,7 +79,6 @@ my $widget = getbool($Q->{widget},"invert")? "false" : "true"; my $wantwidget = $widget eq "true"; - #====================================================================== # select function @@ -104,7 +105,9 @@ sub notfound { #================================================================== # -sub loadReqTable { +# loads the file with the actual values +sub loadReqTable +{ my %args = @_; my $table = $args{table}; my $msg = $args{msg}; @@ -125,25 +128,6 @@ sub loadReqTable { return $T; } -sub loadCfgTable { - my %args = @_; - my $table = $args{table}; - - # Set the Environment VAR to tell the EVAL'd program who the user is. - $ENV{'NMIS_USER'} = $AU->{user}; - - my $tabCfg = loadGenericTable("Table-$table"); - my %Cfg = %{$tabCfg}; - - if (!($Cfg{$table})) { - print Tr(td({class=>'error'},"Configuration of table $table does not exists")); - return; - } - - return $Cfg{$table}; -} - -# sub menuTable{ my $table = $Q->{table}; @@ -169,7 +153,8 @@ sub menuTable{ $T = loadReqTable(table=>$table); # load requested table my $CT; - return if (!($CT = loadCfgTable(table=>$table))); # load configuration of table + # load configuration of table + return if (!($CT = loadCfgTable(table=>$table, user => $AU->{user}))); print start_table; @@ -214,9 +199,10 @@ sub menuTable{ if ($AU->CheckAccess("Table_${table}_rw","check")) { $bt = ' ' - . a({href=>"$url&act=config_table_edit&key=$safekey&widget=$widget"}, - 'edit') - . ' ' + # polling-policy: not editable, only add and delete + . ($table eq "Polling-Policy"? '' : + (a({href=>"$url&act=config_table_edit&key=$safekey&widget=$widget"}, + 'edit') . ' ')) . a({href=>"$url&act=config_table_delete&key=$safekey&widget=$widget"}, 'delete'); # if looking at the users table AND lockout feature is enabled, offer a failure count reset @@ -257,7 +243,7 @@ sub viewTable { my $T; return if (!($T = loadReqTable(table=>$table))); # load requested table - my $CT = loadCfgTable(table=>$table); # load table configuration + my $CT = loadCfgTable(table=>$table, user => $AU->{user}); # load table configuration # not delete -> we assume view my $action= $Q->{act} =~ /delete/? "config_table_dodelete": "config_table_menu"; @@ -285,6 +271,25 @@ sub viewTable { if ($Q->{act} =~ /delete/) { + # Polling Policy: should not be deletable if there are nodes with this policy + my $forbidden; + if ($table eq "Polling-Policy") + { + my $LNT = loadLocalNodeTable(); + my $problematic = scalar grep($_->{polling_policy} eq $key, values %$LNT); + $forbidden = "Policy \"$key\" is used by $problematic nodes, cannot be deleted!" if ($problematic); + } + + if ($forbidden) + { + print Tr(td({class=>"Error", colspan => 2 }, $forbidden, + ' ', button(-name=>'button', + onclick=> '$("#cancelinput").val("true");' + . ($wantwidget? "get('$formid');" : 'submit();'), + -value=>"Ok"))); + } + else + { print Tr(td(' '), td(button(-name=>"button",onclick => ($wantwidget? "get('$formid');" : 'submit()'), -value=>"Delete"), @@ -294,6 +299,7 @@ sub viewTable { onclick=> '$("#cancelinput").val("true");' . ($wantwidget? "get('$formid');" : 'submit();'), -value=>"Cancel"))); + } } else { @@ -326,7 +332,7 @@ sub showTable { my $T; return if (!($T = loadReqTable(table=>$table))); # load requested table - my $CT = loadCfgTable(table=>$table); # load table configuration + my $CT = loadCfgTable(table=>$table, user => $AU->{user}); # load table configuration my $S = Sys::->new; $S->init(name=>$node,snmp=>'false'); @@ -372,6 +378,9 @@ sub editTable my $table = $Q->{table}; my $key = $Q->{key}; + # polling policy: add and delete, no edit + return menuTable() if ($table eq "Polling-Policy" and $Q->{act} eq 'config_table_edit'); + my @hash; # items of key #start of page @@ -383,7 +392,7 @@ sub editTable my $T; return if (!($T = loadReqTable(table=>$table,msg=>'false')) and $Q->{act} =~ /edit/); # load requested table - my $CT = loadCfgTable(table=>$table); + my $CT = loadCfgTable(table=>$table, user => $AU->{user}); my $func = ($Q->{act} eq 'config_table_add') ? 'doadd' : 'doedit'; my $button = ($Q->{act} eq 'config_table_add') ? 'Add' : 'Edit'; @@ -517,20 +526,26 @@ sub editTable pageEnd() if (getbool($widget,"invert")); } +# this function performs the actual modification of the files with values +# called for both adding and editing sub doeditTable { my $table = $Q->{table}; my $hash = $Q->{hash}; - my $new_name; # only needed for nodes table - return 1 if (getbool($Q->{cancel})); + # no editing for the Polling Policy, only add and delete + return 1 if ($table eq "Polling-Policy" + and $Q->{act} eq "config_table_doedit"); + + my $new_name; # only needed for nodes table + $AU->CheckAccess("Table_${table}_rw",'header'); - my $T = loadReqTable(table=>$table,msg=>'false'); + my $T = loadReqTable(table=>$table, msg=>'false'); - my $CT = loadCfgTable(table=>$table); + my $CT = loadCfgTable(table=>$table, user => $AU->{user}); my $TAB = loadGenericTable('Tables'); # combine key from values, values separated by underscrore @@ -543,7 +558,7 @@ sub doeditTable $key = stripSpaces($key); } - # test on existing key + # test for invalid or existing key if ($Q->{act} =~ /doadd/) { if (exists $T->{$key}) { @@ -561,7 +576,8 @@ sub doeditTable # make room, make room! accessing a nonexistent $T->{$key} does NOT attach it to $T... my $thisentry = $T->{$key} ||= {}; - my $V; + my $V; # fixme: deprecated, in sql db mode only + # store new values in table structure for my $ref ( @{$CT}) { @@ -581,90 +597,203 @@ sub doeditTable # any such into comma-sep data - but for a standalone submission that does not happen. my $value = join(",", unpack("(Z*)*", stripSpaces($Q->{$item}))); $thisentry->{$item} = $V->{$item} = $value; - } - } - # some sanity checks BEFORE writing the data out - if ($table eq 'Nodes') - { - # check host address - if (!$thisentry->{host}) - { - print header($headeropts); - print Tr(td({class=>'error'} , "Field \'host\' must be filled in table $table")); - return 0; - } + # and validate if told to + next if (ref($thisitem->{validate}) ne "HASH"); + + # supported validation mechanisms: + # "int" => [ min, max ], undef can be used for no min/max - rejects X < min or > max. + # "float" => [ min, max, above, below ] - rejects X < min or X <= above, X > max or X >= below + # that's required to express 'positive float' === strictly above zero: [0 or undef,dontcare,0,dontcare] + # "regex" => qr//, + # "ip" => [ 4 or 6 or 4, 6], + # "resolvable" => [ 4 or 6 or 4, 6] - accepts ip of that type or hostname that resolves to that ip type + # "onefromlist" => [ list of accepted values ] or undef - if undef, 'value' list is used + # accepts exactly one value + # "multifromlist" => [ list of accepted values ] or undef, like fromlist but more than one + # accepts any number of values from the list, including none whatsoever! + # more than one rule possible but likely not very useful + for my $valtype (sort keys %{$thisitem->{validate}}) + { + my $valprops = $thisitem->{validate}->{$valtype}; + + if ($valtype eq "int" or $valtype eq "float") + { + return validation_abort($item, "'$value' is not an integer!") + if ($valtype eq "int" and int($value) ne $value); + return validation_abort($item, "'$value' is not a floating point number!") + # integer or full ieee floating point with optional exponent notation + if ($valtype eq "float" and $value !~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/); + + my ($min,$max,$above,$below) = ref($valprops) eq "ARRAY"? @{$valprops} : (undef,undef,undef,undef); + return validation_abort($item, "$value below minimum $min!") + if (defined($min) and $value < $min); + return validation_abort($item,"$value above maximum $max!") + if (defined($max) and $value > $max); + + # integers don't subdivide infinitely precisely so above and below not needed + if ($valtype eq "float") + { + return validation_abort($item, "$value is not above $above!") + if (defined($above) and $value <= $above); + + return validation_abort($item, "$value is not below $below!") + if (defined($below) and $value >= $below); + } + } + elsif ($valtype eq "regex") + { + my $expected = ref($valprops) eq "Regexp"? $valprops : qr//; # fallback will match anything + return validation_abort($item, "'$value' didn't match regular expression!") + if ($value !~ $expected); + } + elsif ($valtype eq "ip") + { + my @ipversions = ref($valprops) eq "ARRAY"? @$valprops : (4,6); - ### test the DNS for DNS names, if no IP returned, error exit - # fixme: ipv6 not supported yet - if ( $thisentry->{host} !~ /\d+\.\d+\.\d+\.\d+/ ) - { - my $address = resolveDNStoAddr($thisentry->{host}); - if ( $address !~ /\d+\.\d+\.\d+\.\d+/ or !$address ) { - print header($headeropts); - print Tr(td({class=>'error'} , escapeHTML("ERROR, cannot resolve IP address \'$thisentry->{host}\'") - ."
". "Please correct this item in table $table")); - return 0; + my $ipobj = Net::IP->new($value); + return validation_abort($item, "'$value' is not a valid IP address!") + if (!$ipobj); + + return validation_abort($item, "'$value' is IP address of the wrong type!") + if (($ipobj->version == 6 and !grep($_ == 6, @ipversions)) + or $ipobj->version == 4 and !grep($_ == 4, @ipversions)); + } + elsif ($valtype eq "resolvable") + { + return validation_abort($item, "'$value' is not a resolvable name or IP address!") + if (!$value); + + my @ipversions = ref($valprops) eq "ARRAY"? @$valprops : (4,6); + + my $alreadyip = Net::IP->new($value); + if ($alreadyip) + { + return validation_abort($item, "'$value' is IP address of the wrong type!") + if (!grep($_ == $alreadyip->version, @ipversions)); + # otherwise, we're happy... + } + else + { + my @addresses = NMIS::resolve_dns_name($value); + return validation_abort($item, "DNS failed to resolve '$value'!") + if (!@addresses); + + my @addr_objs = map { Net::IP->new($_) } (@addresses); + my $goodones; + for my $type (4,6) + { + $goodones += grep($_->version == $type, @addr_objs) if (grep($_ == $type, @ipversions)); + } + return validation_abort($item, + "'$value' does not resolve to an IP address of the right type!") + if (!$goodones); + } + } + elsif ($valtype eq "onefromlist" or $valtype eq "multifromlist") + { + # either explicit list of acceptables, or the 'value' config item + my @acceptable = ref($valprops) eq "ARRAY"? @$valprops : + ref($thisitem->{value}) eq "ARRAY"? @{$thisitem->{value}}: (); + return validation_abort($item, "no validation choices configured!") + if (!@acceptable); + + # for multifromlist assume that value is now comma-separated. *sigh* + # for onefromlist values with colon are utterly unspecial *double sigh* + my @mustcheckthese = ($valtype eq "multifromlist")? split(/,/, $value) : $value; + for my $oneofmany (@mustcheckthese) + { + return validation_abort($item, "'$oneofmany' is not in list of acceptable values!") + if (!List::Util::any { $oneofmany eq $_ } (@acceptable)); + } + } + else + { + return validation_abort($item, "unknown validation type \"$valtype\""); + } } } + } + # nodes requires special handling, extra sanity checks, and dealing with rename + if ($table eq 'Nodes') + { # ensure a uuid is present - $thisentry->{uuid} ||= getUUID($key); + $thisentry->{uuid} ||= NMIS::UUID::getUUID($key); $V->{uuid} ||= $thisentry->{uuid}; # keep the new_name from being written to the config file $new_name = $thisentry->{new_name}; delete $thisentry->{new_name}; - } - - my $db = "db_".lc($table)."_sql"; - if ( getbool($C->{$db}) ) { - my $stat; - $V->{index} = $key; # add this column - if ($Q->{act} =~ /doadd/) { - $stat = DBfunc::->insert(table=>$table,data=>$V); - } else { - $stat = DBfunc::->update(table=>$table,data=>$V,index=>$key); - } - if (!$stat) { - print header({-type=>"text/html",-expires=>'now'}); - print Tr(td({class=>'error'} , escapeHTML(DBfunc::->error()))); - return 0; - } - } else { - writeTable(dir=>'conf',name=>$table, data=>$T); - } - - # further special handling for nodes: rename and general update - if ($table eq 'Nodes') - { - # handle the renaming case + # renaming? if ($new_name && $new_name ne $thisentry->{name}) { + # this rewrites nodes.nmis twice, by necessity; backs out nodes.nmis if unsuccessful my ($error,$message) = NMIS::rename_node(old => $thisentry->{name}, new => $new_name, originator => "tables.pl.editNodeTable"); if ($error) { print header($headeropts), - Tr(td({class=>'error'}, escapeHTML("ERROR, renaming node \'$thisentry->{name}\' to \'$new_name\' failed: $message"))); + Tr(td({class=>'error'}, + escapeHTML("ERROR, renaming node \'$thisentry->{name}\' to \'$new_name\' failed: $message"))); return 0; } $key = $new_name; } - + # nope, just a general modification so write out the data... + else + { + writeTable(dir=>'conf',name=>$table, data=>$T); + } cleanEvent($key, "tables.pl.editNodeTable"); + # ...before possibly running an update on that node if (getbool($Q->{update})) { doNodeUpdate(node=>$key); return 0; } + + # don't let the generic code (re|over)write the nodes table again... + return 1; } + # the non-nodes.nmis case + my $db = "db_".lc($table)."_sql"; + if ( getbool($C->{$db}) ) + { + my $stat; + $V->{index} = $key; # add this column + if ($Q->{act} =~ /doadd/) { + $stat = DBfunc::->insert(table=>$table,data=>$V); + } else { + $stat = DBfunc::->update(table=>$table,data=>$V,index=>$key); + } + if (!$stat) + { + print header({-type=>"text/html",-expires=>'now'}); + print Tr(td({class=>'error'} , escapeHTML(DBfunc::->error()))); + return 0; + } + } + else + { + writeTable(dir=>'conf',name=>$table, data=>$T); + } return 1; } +# print (negative) html response +sub validation_abort +{ + my ($item, $message) = @_; + + print header($headeropts), + Tr(td({class=>'error'} , escapeHTML("'$item' failed to validate: $message"))); + return undef; +} + sub dodeleteTable { my $table = $Q->{table}; my $key = $Q->{key}; diff --git a/install.pl b/install.pl index 0961d6c..ef9cb7d 100755 --- a/install.pl +++ b/install.pl @@ -69,7 +69,7 @@ my $nmisModules; # local modules used in our scripts -die $usage if ( $ARGV[0] =~ /^-{\?|h|-help$/i ); +die $usage if ( $ARGV[0] =~ /^-(\?|h|-help)$/i ); # let's prefer std -X flags, fall back to word=value style my (%options, %oldstyle); die $usage if (!getopts("yldt:", \%options)); @@ -122,6 +122,23 @@ close G; logInstall("Installation of NMIS $nmisversion on host '$hostname' started at ".scalar localtime(time)); +# safeguard against local::lib breaking system-wide module installation for cpan'ables + +# if PERL5LIB was set, remove all its members from @INC or the module availabilty test will +# look in the wrong places +if (defined $ENV{PERL_LOCAL_LIB_ROOT}) +{ + logInstall("clearing local::lib config items"); + for my $dontwantpath (split(/:/,$ENV{PERL5LIB})) + { + @INC = grep($_ ne $dontwantpath, @INC); # bit inefficient but good enough + } + for my $dontwant (qw(PERL_LOCAL_LIB_ROOT PERL5LIB PERL_MM_OPT PERL_MB_OPT)) + { + delete $ENV{$dontwant}; + } +} + # there are some slight but annoying differences my ($osflavour,$osmajor,$osminor,$ospatch,$osiscentos); if (-f "/etc/redhat-release") @@ -231,8 +248,11 @@ libcrypt-unixcrypt-perl libcrypt-rijndael-perl libuuid-tiny-perl libproc-processtable-perl libdigest-sha-perl libnet-ldap-perl libnet-snpp-perl libdbi-perl libtime-modules-perl libsoap-lite-perl libauthen-simple-radius-perl libauthen-tacacsplus-perl -libauthen-sasl-perl rrdtool librrds-perl libsys-syslog-perl libtest-deep-perl dialog libui-dialog-perl libcrypt-des-perl libdigest-hmac-perl libclone-perl -libexcel-writer-xlsx-perl libmojolicious-perl)); +libauthen-sasl-perl rrdtool librrds-perl libsys-syslog-perl libtest-deep-perl dialog libcrypt-des-perl libdigest-hmac-perl libclone-perl +libexcel-writer-xlsx-perl libmojolicious-perl libdatetime-perl +libnet-ip-perl libscalar-list-utils-perl libtest-requires-perl libtest-fatal-perl libtest-number-delta-perl + +)); my @rhpackages = (qw(perl-core autoconf automake gcc cvs cairo cairo-devel pango pango-devel glib glib-devel libxml2 libxml2-devel gd gd-devel @@ -243,9 +263,10 @@ perl-DBI perl-Net-SMTPS perl-Net-SMTP-SSL perl-CGI net-snmp-perl perl-Proc-ProcessTable perl-Authen-SASL perl-Crypt-PasswdMD5 perl-Crypt-Rijndael perl-Net-SNPP perl-Net-SNMP perl-GD rrdtool rrdtool-perl perl-Test-Deep dialog -perl-Excel-Writer-XLSX - perl-Digest-HMAC perl-Crypt-DES perl-Clone -)); +perl-Excel-Writer-XLSX perl-Net-IP perl-DateTime +perl-Digest-HMAC perl-Crypt-DES perl-Clone perl-ExtUtils-CBuilder +perl-ExtUtils-ParseXS perl-ExtUtils-MakeMaker perl-Test-Fatal perl-Test-Number-Delta +perl-Test-Requires )); # perl-Time-modules no longer a/v in rh/centos7 push @rhpackages, ($osflavour eq "redhat" && $osmajor < 7)? @@ -258,6 +279,13 @@ push @rhpackages, "perl-CGI"; } + # stretch ships with these packages + push @debpackages, (qw(libproc-queue-perl libstatistics-lite-perl libtime-moment-perl )) + if ($osflavour eq "debian" and $osmajor >= 9); + # stretch no longer ships with this package... + push @debpackages, "libui-dialog-perl" + if ($osflavour ne "debian" or $osmajor <= 8); + my $pkgmgr = $osflavour eq "redhat"? "YUM": ($osflavour eq "debian" or $osflavour eq "ubuntu")? "APT": undef; my $pkglist = $osflavour eq "redhat"? \@rhpackages : ($osflavour eq "debian" or $osflavour eq "ubuntu")? \@debpackages: undef; @@ -266,8 +294,9 @@ # curl is present in most basic redhat install # wget is present on debian/ubuntu via priority:important - my $testres = system("curl -s -m 10 -o /dev/null https://opmantek.com/robots.txt 2>/dev/null") >> 8; - $testres = system("wget -q -T 10 -O /dev/null https://opmantek.com/robots.txt 2>/dev/null") >> 8 + # however, ca-certificates may be out of date/incomplete at this time + my $testres = system("curl --insecure -s -m 10 -o /dev/null https://opmantek.com/robots.txt 2>/dev/null") >> 8; + $testres = system("wget --no-check-certificate -q -T 10 -O /dev/null https://opmantek.com/robots.txt 2>/dev/null") >> 8 if ($testres); $can_use_web = !$testres; @@ -792,8 +821,9 @@ package installation without Internet access in that case: for my $cff ("License.nmis", "Access.nmis", "Config.nmis", "BusinessServices.nmis", "ServiceStatus.nmis", "Contacts.nmis", "Enterprise.nmis", "Escalations.nmis", "ifTypes.nmis", "Links.nmis", "Locations.nmis", "Logs.nmis", - "Customers.nmis", "Events.nmis", - "Model-Policy.nmis", "Modules.nmis", "Nodes.nmis", "Outage.nmis", "Portal.nmis", + "Customers.nmis", "Events.nmis", "Polling-Policy.nmis", + "Model-Policy.nmis", "Modules.nmis", "Nodes.nmis", + "Outage.nmis", "Portal.nmis", "PrivMap.nmis", "Services.nmis", "Users.nmis", "users.dat") { if (-f "$site/install/$cff" && !-e "$site/conf/$cff") @@ -824,8 +854,8 @@ package installation without Internet access in that case: # patch config changes that affect existing entries, which update_config_defaults # doesn't handle - # which includes enabling uuid - execPrint("$site/admin/patch_config.pl -b $site/conf/Config.nmis /system/non_stateful_events='Node Configuration Change, Node Reset, NMIS runtime exceeded' /globals/uuid_add_with_node=true /system/node_summary_field_list,=uuid /system/json_node_fields,=uuid"); + # which includes enabling uuid and showing the polling_policy + execPrint("$site/admin/patch_config.pl -b $site/conf/Config.nmis /system/non_stateful_events='Node Configuration Change, Node Reset, NMIS runtime exceeded' /globals/uuid_add_with_node=true /system/node_summary_field_list,=uuid /system/json_node_fields,=uuid /system/network_viewNode_field_list,=polling_policy"); echolog("\n"); if (input_yn("OK to remove syslog and JSON logging from default event escalation?")) @@ -1059,8 +1089,8 @@ package installation without Internet access in that case: { printBanner("Checking Common-database file for updates"); - # two cases: something not found -> migration tool to update, missing stuff, FIRST. - # then if there are any issues with actual actual differences, full migration tool run + # two cases: something new -> updateconfig.pl to fill that in + # then if there are an issues with actual actual differences, full migration tool run my $diffs = `$site/admin/diffconfigs.pl $site/models/Common-database.nmis $site/models-install/Common-database.nmis 2>/dev/null`; my $res = $? >> 8; @@ -1071,9 +1101,8 @@ package installation without Internet access in that case: } elsif ($diffs =~ m!^-\s+{"Crypt::DES"} = { file => "MODULE NOT FOUND", type => "use", by => "lib/snmp.pm" }; $nmisModules->{"Digest::HMAC"} = { file => "MODULE NOT FOUND", type => "use", by => "lib/snmp.pm" }; - # these are critical for getting mojolicious installed, as centos 6 perl has a much too old perl - # these modules are in core since 5.19 or thereabouts + # most of these are critical for getting mojolicious installed, as centos 6 + # has a much too old perl. many of these modules are in core since 5.19 or thereabouts $nmisModules->{"IO::Socket::IP"} = { file => "MODULE NOT FOUND", type => "use", by => "lib/Auth.pm", priority => 99 }; @@ -1507,6 +1536,10 @@ sub check_installed_modules $nmisModules->{"Test::More"} = { file => "MODULE NOT FOUND", type => "use", by => "lib/Auth.pm", minversion => "0.96", priority => 100 }; + # and time::moment doesn't install cleanly if extutils::parsexs isn't fully installed first + $nmisModules->{"ExtUtils::ParseXS"} = { file => "MODULE NOT FOUND", type => "use", + by => "lib/Auth.pm", minversion => "3.18", + priority => 100 }; # now determine if installed or not. # sort by the required cpan sequencing (no priority is last) @@ -1553,7 +1586,7 @@ sub moduleVersion open FH,"<$mFile" or return 'FileNotFound'; while () { - if (/^\s*((our|my)\s+\$|\$(\w+::)*)VERSION\s*=\s*['"]?\s*[vV]?([0-9\.]+)\s*['"]?s*;/) + if (/^\s*((our|my)\s+\$|\$(\w+::)*)VERSION\s*=\s*['"]?\s*[vV]?([0-9\._]+)\s*['"]?s*;/) { close FH; return $4; @@ -1634,33 +1667,42 @@ sub listModules } +# prints prompt, waits for confirmation +sub input_ok +{ + print "\nHit to continue:\n"; + my $x = if (!$noninteractive); +} + # print question, return true if y (or in unattended mode). default is yes. sub input_yn { my ($query) = @_; - print $query; - if ($noninteractive) + while (1) { - print " (auto-default YES)\n\n"; - return 1; - } - else - { - print "\nType 'y' or hit to accept, any other key for 'no': "; - my $input = ; - chomp $input; - logInstall("User input for \"$query\": \"$input\""); + print $query; + if ($noninteractive) + { + print " (auto-default YES)\n\n"; + return 1; + } + else + { + print "\nType 'y' or to accept, or 'n' to decline: "; + my $input = ; + chomp $input; + logInstall("User input for \"$query\": \"$input\""); - return ($input =~ /^\s*(y|yes)?\s*$/i)? 1:0; - } -} + if ($input !~ /^\s*[yn]?\s*$/i) + { + print "Invalid input \"$input\"\n\n"; + next; + } -# prints prompt, waits for confirmation -sub input_ok -{ - print "\nHit to continue:\n"; - my $x = if (!$noninteractive); + return ($input =~ /^\s*y?\s*$/i)? 1:0; + } + } } # question, default answer, whether we want confirmation or not @@ -1821,7 +1863,7 @@ sub enable_custom_repo elsif ($reponame eq "gf") { echolog("\nEnabling Ghettoforge repository\n"); - execPrint("yum -y install 'http://mirror.symnds.com/distributions/gf/el/$majorlevel/gf/x86_64/gf-release-$majorlevel-10.gf.el$majorlevel.noarch.rpm'"); + execPrint("yum -y install 'http://mirror.ghettoforge.org/distributions/gf/gf-release-latest.gf.el$majorlevel.noarch.rpm'"); } else { diff --git a/install/Access.nmis b/install/Access.nmis index 8033877..a48486c 100644 --- a/install/Access.nmis +++ b/install/Access.nmis @@ -1224,6 +1224,30 @@ 'level5' => '0', 'name' => 'Table_Services_view' }, + + 'table_polling-policy_rw' => { + 'descr' => 'Write access to table Polling-Policy', + 'group' => 'access', + 'level0' => '1', + 'level1' => '1', + 'level2' => '0', + 'level3' => '0', + 'level4' => '0', + 'level5' => '0', + 'name' => 'Table_Polling-Policy_rw' + }, + 'table_polling-policy_view' => { + 'descr' => 'View access to table Polling-Policy', + 'group' => 'access', + 'level0' => '1', + 'level1' => '1', + 'level2' => '0', + 'level3' => '0', + 'level4' => '0', + 'level5' => '0', + 'name' => 'Table_Polling-Policy_view' + }, + 'table_tables_rw' => { 'descr' => 'View access to table $table', 'group' => 'access', diff --git a/install/Config.nmis b/install/Config.nmis index 33e7845..cebe0df 100644 --- a/install/Config.nmis +++ b/install/Config.nmis @@ -71,13 +71,16 @@ my %hash = ( "roletype_list" => "core,distribution,access,default", "nettype_list" => "wan,lan,vpn,man,san,voice,default", "nodetype_list" => "generic,switch,router,firewall,server", + + # new configuration option to have a configurable field to use for location, e.g. sysLocation or location. + 'location_field_name' => 'sysLocation', # for coloring the group status panel . 'default' applies to unlisted role types 'severity_by_roletype' => { core => [ 'Critical', 'Major' ], distribution => [ 'Major', 'Minor' ], access => [ 'Minor', 'Warning' ], default => [ 'Major', 'Minor' ] }, # if this option is present, then *only* properties listed here will be shown in the node view, # and in precisely this order. custom properties are supported. - 'network_viewNode_field_list' => 'status,sysName,host_addr,group,customer,location,businessService,serviceStatus,nodeType,nodeModel,sysUpTime,sysLocation,sysContact,sysDescr,ifNumber,lastUpdate,nodeVendor,sysObjectName,roleType,netType', + 'network_viewNode_field_list' => 'status,outage,sysName,host_addr,group,customer,location,businessService,serviceStatus,nodeType,nodeModel,polling_policy,sysUpTime,sysLocation,sysContact,sysDescr,ifNumber,lastUpdate,nodeVendor,sysObjectName,roleType,netType', # if this option is present use binary logic for node down and set the level to the overall_node_status_level 'overall_node_status_coarse' => 'false', @@ -182,6 +185,8 @@ my %hash = ( 'max_child_runtime' => undef, # to disable the generation of "nmis runtime exceeded" events, set this to 1 or true 'disable_nmis_process_events' => undef, + # produce STDERR output (and thus cron emails) if killing nmis processes + 'verbose_nmis_process_events' => 'true', # to enable the logging of polling time to the NMIS log, will log every node every 5 minutes, set this to 1 or true 'log_polling_time' => undef, # various selftest limits @@ -296,6 +301,7 @@ my %hash = ( 'daemon_ipsla_filename' => 'ipslad.pl', 'daemon_fping_active' => 'true', 'daemon_fping_dns_cache' => 'true', + 'daemon_fping_run_escalation' => 'true', 'daemon_fping_filename' => 'fpingd.pl' }, diff --git a/install/Enterprise.nmis b/install/Enterprise.nmis index e93fe4f..3d98586 100644 --- a/install/Enterprise.nmis +++ b/install/Enterprise.nmis @@ -73720,4 +73720,8 @@ 'Enterprise' => 'Ubiquiti Networks, Inc.', 'OID' => '41112' }, + '19547' => { + 'Enterprise' => 'Oplink Communications, Inc.', + 'OID' => '19547' + }, ); diff --git a/install/Events.nmis b/install/Events.nmis index 3403c54..8a6caa1 100644 --- a/install/Events.nmis +++ b/install/Events.nmis @@ -517,5 +517,24 @@ 'Stateful' => 'true', 'Status' => 'true' }, + 'NMIS runtime exceeded' => { + 'Event' => 'NMIS runtime exceeded', + 'Notify' => 'true', + 'Status' => 'true', + 'Log' => 'true', + 'Stateful' => 'true', + 'CancelingEvent' => 'N/A', + 'Description' => 'The collect process for a node has been killed to ensure that polling continues for other nodes.', + }, + + "Planned Outage Open" => { + "Event" => "Planned Outage Open", + "Notify" => "true", + "Status" => "false", # does not contribute to status summary calculations + "Log" => "true", # needs to be logged for opEvents and others + "Stateful" => "true", + "CancelingEvent" => "Planned Outage Closed", + "Description" => "A scheduled outage window for a node has become active.", + }, ); diff --git a/install/Model-Policy.nmis b/install/Model-Policy.nmis index 6ff8917..a779f98 100644 --- a/install/Model-Policy.nmis +++ b/install/Model-Policy.nmis @@ -22,13 +22,42 @@ # 'tempStatus' => 'false', # remove if present # }, # }, - - # 20 => { - # IF => { 'node.name' => 'nodeC' }, - # systemHealth => { - # diskIOTable => 'false' # this node runs off r/o flash disk - # }, - # }, + + # more examples: +# 20 => { +# IF => { 'node.name' => ['nodeA','nodeB'] }, +# systemHealth => { +# Juniper_CoS => 'true', # Turn on the Juniper CoS section + +#as these system health items are in this model but we don't want them on we need to explicitly turn them off as we would in the default section. +# jnxDestinationClassUsage => 'false', +# jnxSourceClassUsage => 'false', +# ifTable => 'false', +# mplsL3VpnVrf => 'false', +# mplsL3VpnIfConf => 'false', +# mplsVpnInterface => 'false', +# mplsL3VpnVrfRT => 'false', +# }, +# }, +# + + +# 600 => { +# IF => { 'node.group' => '/Core/', +# 'node.nodeModel' => '/JuniperRouter/', }, # note needs to be regex for nodeModel +# +# systemHealth => { +# jnxDestinationClassUsage => 'true', #enable SCU and DCU +# jnxSourceClassUsage => 'true', + +# Juniper_CoS => 'false', # we just want DCU and SCU so we need to explicitly turn this off. +# ifTable => 'false', +# mplsL3VpnVrf => 'false', +# mplsL3VpnIfConf => 'false', +# mplsVpnInterface => 'false', +# mplsL3VpnVrfRT => 'false', +# }, +# }, 999 => { # the fallback/defaults, without filter systemHealth => { @@ -43,6 +72,7 @@ fanStatus => 'true', tempStatus => 'true', 'env-temp' => 'true', + #Do not remove the '' due to interpretation of temp fanStatus => 'true', psuStatus => 'true', tempStatus => 'true', @@ -60,6 +90,9 @@ mplsL3VpnVrfRT => 'false', mplsVpnVrfRouteTarget => 'false', mplsLdpEntity => 'false', + Juniper_CoS => 'false', + jnxDestinationClassUsage => 'false', + jnxSourceClassUsage => 'false', }, # should include all possible values @@ -83,6 +116,7 @@ ciscoAsset => [ 6, "Device Inventory using the Cisco Asset Entity MIB" ], 'env-temp' => [ 8, "Cisco Environment Temperature" ], + #Do not remove the '' due to interpretation of temp fanStatus => [ 8, "Cisco Fan Status" ], psuStatus => [ 8, "Cisco Power Supply Status" ], tempStatus => [ 8, "Cisco Temperature Status" ], @@ -98,7 +132,10 @@ mplsL3VpnVrfRT => [ 10, "Logical Network Inventory - MPLS Route Targets" ], mplsVpnVrfRouteTarget => [ 10, "Logical Network Inventory - MPLS Route Targets" ], mplsLdpEntity => [ 10, "Logical Network Inventory - MPLS Label Distribution Protocol Information" ], - - }, - }, + + Juniper_CoS => [ 12, "Juniper Routers jnX - CBQoS statistics" ], + jnxDestinationClassUsage => [ 12, "Juniper Routers jnX - Juniper Destination Class Filter Usage" ], + jnxSourceClassUsage => [ 12, "Juniper Routers jnX - Juniper Source Class Filter Usage" ], + }, + }, ); diff --git a/install/Nodes.nmis b/install/Nodes.nmis index 4e545b5..774f768 100644 --- a/install/Nodes.nmis +++ b/install/Nodes.nmis @@ -14,6 +14,8 @@ 'name' => 'localhost', 'active' => 'true', 'port' => '161', + 'max_msg_size' => 1472, + 'max_repetitions' => 0, 'host' => '127.0.0.1', 'netType' => 'lan', 'ping' => 'true' diff --git a/install/Outage.nmis b/install/Outage.nmis deleted file mode 100644 index 966c543..0000000 --- a/install/Outage.nmis +++ /dev/null @@ -1,14 +0,0 @@ -%hash = ( - 'switch-1199975083-1200373200' => { - 'change' => 'ticket # holiday', - 'node' => 'switch', - 'end' => 1200373200, - 'start' => 1199975083 - }, - 'adsl-1195240525-1195330525-must be filled' => { - 'change' => 'must be filled', - 'node' => 'adsl', - 'start' => 1195240525, - 'end' => 1195330525 - } -); diff --git a/install/Polling-Policy.nmis b/install/Polling-Policy.nmis new file mode 100644 index 0000000..5643687 --- /dev/null +++ b/install/Polling-Policy.nmis @@ -0,0 +1,16 @@ +%hash = ( + 'half_hourly' => { + 'description' => 'a polling policy for infrequent polling', + 'name' => 'half_hourly', + 'ping' => '5m', + 'snmp' => '30m', + 'wmi' => '30m' + }, + 'very_infrequently' => { + 'description' => 'poll very infrequently', + 'name' => 'very_infrequently', + 'ping' => '30m', + 'snmp' => '1h', + 'wmi' => '1h' + } +); diff --git a/install/Table-Config.nmis b/install/Table-Config.nmis new file mode 100644 index 0000000..5c3cdb1 --- /dev/null +++ b/install/Table-Config.nmis @@ -0,0 +1,317 @@ +my %hash = ( + "Config" => { + + 'online' => [ { 'nmis_docs_online' => { display => 'text', value => ['https://community.opmantek.com/'] } }, + ], + + 'modules' => [ { 'display_opmaps_widget' => { display => 'popup', value => ["true", "false"]} } ], + + 'directories' => [ + { '' => { display => 'text', value => ['/usr/local/nmis']}}, + { '' => { display => 'text', value => ['/bin']}}, + { '' => { display => 'text', value => ['/cgi-bin']}}, + { '' => { display => 'text', value => ['/conf']}}, + { '' => { display => 'text', value => ['']}}, + { '' => { display => 'text', value => ['/logs']}}, + { '' => { display => 'text', value => ['/menu']}}, + { '' => { display => 'text', value => ['/models']}}, + { '' => { display => 'text', value => ['/var']}}, + { '' => { display => 'text', value => ['/menu']}}, + { 'database_root' => { display => 'text', value => ['/database']}}, + { 'log_root' => { display => 'text', value => ['']}}, + { 'mib_root' => { display => 'text', value => ['/mibs']}}, + { 'report_root' => { display => 'text', value => ['/htdocs/reports']}}, + { 'script_root' => { display => 'text', value => ['/scripts']}}, + { 'web_root' => { display => 'text', value => ['/htdocs']}}, + ], + + 'system' => [ + { 'group_list' => { display => 'text', value => ['']}}, + { 'roletype_list' => { display => 'text', value => ['']}}, + { 'nettype_list' => { display => 'text', value => ['']}}, + { 'nodetype_list' => { display => 'text', value => ['']}}, + { 'nmis_host' => { display => 'text', value => ['localhost']}}, + { 'domain_name' => { display => 'text', value => ['']}}, + { 'cache_summary_tables' => { display => 'popup', value => ["true", "false"]}}, + { 'cache_var_tables' => { display => 'popup', value => ["true", "false"]}}, + { 'page_refresh_time' => { display => 'text', value => ['60']}}, + { 'os_posix' => { display => 'popup', value => ["true", "false"]}}, + { 'os_cmd_read_file_reverse' => { display => 'text', value => ['tac']}}, + { 'os_cmd_file_decompress' => { display => 'text', value => ['gzip -d -c']}}, + { 'os_kernelname' => { display => 'text', value => ['']}}, + { 'os_fileperm' => { display => 'text', value => ['0660'], + validate => { 'regex' => qr/^[0-7]{4,5}$/ }}}, + { 'report_files_max' => { display => 'text', value => ['60'], + validate => { 'int' => [ 1, undef ] }}}, + { 'loc_sysLoc_format' => { display => 'text', value => ['']}}, + { 'loc_from_DNSloc' => { display => 'popup', value => ["true", "false"]}}, + { 'loc_from_sysLoc' => { display => 'popup', value => ["true", "false"]}}, + { 'cbqos_cm_collect_all' => { display => 'popup', value => ["true", "false"]}}, + { 'buttons_in_logs' => { display => 'popup', value => ["true", "false"]}}, + { 'node_button_in_logs' => { display => 'popup', value => ["true", "false"]}}, + { 'page_bg_color_full' => { display => 'popup', value => ["true", "false"]}}, + { 'http_req_timeout' => { display => 'text', value => ['30'], + validate => { 'int' => [ 1, undef ] }}}, + { 'ping_timeout' => { display => 'text', value => ['500'], + validate => { 'int' => [ 1, undef ] }}}, + { 'server_name' => { display => 'text', value => ['localhost']}}, + { 'response_time_threshold' => { display => 'text', value => ['3'], + validate => { 'int' => [ 1, undef ] }}}, + { 'nmis_user' => { display => 'text', value => ['nmis']}}, + { 'nmis_group' => { display => 'text', value => ['nmis']}}, + { 'fastping_timeout' => { display => 'text', value => ['300']}}, + { 'fastping_packet' => { display => 'text', value => ['56'], + validate => { 'int' => [ 40, 65535 ] }}}, + { 'fastping_retries' => { display => 'text', value => ['3'], + validate => { 'int' => [ 0, 100 ] }}}, + { 'fastping_count' => { display => 'text', value => ['3'], + validate => { 'int' => [ 1, 100 ] }}}, + + { 'fastping_node_poll' => { display => 'text', value => ['200'], + validate => { 'int' => [ 1, undef ] }}}, + { 'ipsla_collect_time' => { display => 'text', value => ['60']}}, + { 'ipsla_bucket_interval' => { display => 'text', value => ['180']}}, + { 'ipsla_extra_buckets' => { display => 'text', value => ['5']}}, + { 'ipsla_mthread' => { display => 'popup', value => ["true", "false"]}}, + { 'ipsla_maxthreads' => { display => 'text', value => ['10']}}, + { 'ipsla_mthreaddebug' => { display => 'popup', value => ["false", "true"]}}, + { 'ipsla_dnscachetime' => { display => 'text', value => ['3600']}}, + { 'ipsla_control_enable_other' => { display => 'popup', value => ["true", "false"]}}, + { 'fastping_timeout' => { display => 'text', value => ['300']}}, + { 'fastping_packet' => { display => 'text', value => ['56']}}, + { 'fastping_retries' => { display => 'text', value => ['3']}}, + { 'fastping_count' => { display => 'text', value => ['3']}}, + { 'fastping_sleep' => { display => 'text', value => ['60']}}, + { 'fastping_node_poll' => { display => 'text', value => ['300']}}, + { 'default_graphtype' => { display => 'text', value => ['abits']}}, + { 'ping_timeout' => { display => 'text', value => ['300']}}, + { 'ping_packet' => { display => 'text', value => ['56'], + validate => { 'int' => [ 40, 65535 ] }}}, + { 'ping_retries' => { display => 'text', value => ['3'], + validate => { 'int' => [ 1, 100 ] }}}, + { 'ping_count' => { display => 'text', value => ['3'], + validate => { 'int' => [ 1, 100 ] }}}, + { 'global_collect' => { display => 'popup', value => ["true", "false"]}}, + { 'wrap_node_names' => { display => 'popup', value => ["false", "true"]}}, + { 'nmis_summary_poll_cycle' => { display => 'popup', value => ["true", "false"]}}, + { 'snpp_server' => { display => 'text', value => ['']}}, + { 'snmp_timeout' => { display => 'text', value => ['5'], + validate => { 'int' => [ 0, undef ] } } }, + { 'snmp_retries' => { display => 'text', value => ['1'], + validate => { 'int' => [ 0, undef ] } } }, + { 'snmp_stop_polling_on_error' => { display => 'popup', + value => ["false", "true"]}}, + ], + + 'url' => [ + { '' => { display => 'text', value => ['/nmis8']}}, + { '' => { display => 'text', value => ['/cgi-nmis8']}}, + { '' => { display => 'text', value => ['/menu8']}}, + { 'web_report_root' => { display => 'text', value => ['/reports']}} + ], + + 'tools' => [ + { 'view_ping' => { display => 'popup', value => ["true", "false"]}}, + { 'view_trace' => { display => 'popup', value => ["true", "false"]}}, + { 'view_telnet' => { display => 'popup', value => ["true", "false"]}}, + { 'view_mtr' => { display => 'popup', value => ["true", "false"]}}, + { 'view_lft' => { display => 'popup', value => ["true", "false"]}} + ], + + 'files' => [ + { 'styles' => { display => 'text', value => ['/nmis.css']}}, + { 'syslog_log' => { display => 'text', value => ['/cisco.log']}}, + { 'event_log' => { display => 'text', value => ['/event.log']}}, + { 'outage_log' => { display => 'text', value => ['/outage.log']}}, + { 'help_file' => { display => 'text', value => ['/help.pod.html']}}, + { 'nmis' => { display => 'text', value => ['/nmiscgi.pl']}}, + { 'nmis_log' => { display => 'text', value => ['/nmis.log']}} + ], + + 'email' => [ + { 'mail_server' => { display => 'text', value => ['mail.domain.com']}}, + { 'mail_domain' => { display => 'text', value => ['domain.com']}}, + { 'mail_from' => { display => 'text', value => ['nmis@domain.com']}}, + { 'mail_combine' => { display => 'popup', value => ['true','false']}}, + { 'mail_from' => { display => "text", value => ['nmis@yourdomain.com']}}, + { 'mail_use_tls' => { display => 'popup', value => ['true','false']}}, + { 'mail_server_port' => { display => "text", value => ['25'], + validate => { 'int' => [ 1, 65535] }}}, + { 'mail_server_ipproto' => { display => "popup", value => ['','ipv4','ipv6']}}, + { 'mail_user' => { display => "text", value => ['your mail username']}}, + { 'mail_password' => { display => "text", value => ['']}} + ], + + 'menu' => [ + { 'menu_title' => { display => 'text', value => ['NMIS']}}, + { 'menu_types_active' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_types_full' => { display => 'popup', value => ["true", "false", "defer"]}}, + { 'menu_types_foldout' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_groups_active' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_groups_full' => { display => 'popup', value => ["true", "false", "defer"]}}, + { 'menu_groups_foldout' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_vendors_active' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_vendors_full' => { display => 'popup', value => ["true", "false", "defer"]}}, + { 'menu_vendors_foldout' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_maxitems' => { display => 'text', value => ['30']}}, + { 'menu_suspend_link' => { display => 'popup', value => ["true", "false"]}}, + { 'menu_start_page_id' => { display => 'text', value => ['']}} + ], + + 'icons' => [ + { 'normal_net_icon' => { display => 'text', value => ['/img/network-green.gif']}}, + { 'arrow_down_green' => { display => 'text', value => ['/img/arrow_down_green.gif']}}, + { 'arrow_up_big' => { display => 'text', value => ['/img/bigup.gif']}}, + { 'logs_icon' => { display => 'text', value => ['/img/logs.jpg']}}, + { 'mtr_icon' => { display => 'text', value => ['/img/mtr.jpg']}}, + { 'arrow_up' => { display => 'text', value => ['/img/arrow_up.gif']}}, + { 'help_icon' => { display => 'text', value => ['/img/help.jpg']}}, + { 'telnet_icon' => { display => 'text', value => ['/img/telnet.jpg']}}, + { 'back_icon' => { display => 'text', value => ['/img/back.jpg']}}, + { 'lft_icon' => { display => 'text', value => ['/img/lft.jpg']}}, + { 'fatal_net_icon' => { display => 'text', value => ['/img/network-red.gif']}}, + { 'trace_icon' => { display => 'text', value => ['/img/trace.jpg']}}, + { 'nmis_icon' => { display => 'text', value => ['/img/nmis.jpg']}}, + { 'summary_icon' => { display => 'text', value => ['/img/summary.jpg']}}, + { 'banner_image' => { display => 'text', value => ['/img/NMIS_Logo.gif']}}, + { 'map_icon' => { display => 'text', value => ['/img/australia-line.gif']}}, + { 'minor_net_icon' => { display => 'text', value => ['/img/network-yellow.gif']}}, + { 'arrow_down_big' => { display => 'text', value => ['/img/bigdown.gif']}}, + { 'ping_icon' => { display => 'text', value => ['/img/ping.jpg']}}, + { 'unknown_net_icon' => { display => 'text', value => ['/img/network-white.gif']}}, + { 'doc_icon' => { display => 'text', value => ['/img/doc.jpg']}}, + { 'arrow_down' => { display => 'text', value => ['/img/arrow_down.gif']}}, + { 'arrow_up_red' => { display => 'text', value => ['/img/arrow_up_red.gif']}}, + { 'major_net_icon' => { display => 'text', value => ['/img/network-amber.gif']}}, + { 'critical_net_icon' => { display => 'text', value => ['/img/network-red.gif']}} + ], + + 'authentication' => [ + { 'auth_method_1' => { display => 'popup', value => ['apache','htpasswd','radius','tacacs','ldap','ldaps','ms-ldap']}}, + { 'auth_method_2' => { display => 'popup', value => ['apache','htpasswd','radius','tacacs','ldap','ldaps','ms-ldap']}}, + { 'auth_expire' => { display => 'text', value => ['+20min']}}, + { 'auth_htpasswd_encrypt' => { display => 'popup', value => ['crypt','md5','plaintext']}}, + { 'auth_htpasswd_file' => { display => 'text', value => ['/users.dat']}}, + { 'auth_ldap_server' => { display => 'text', value => ['']}}, + { 'auth_ldaps_server' => { display => 'text', value => ['']}}, + { 'auth_ldap_attr' => { display => 'text', value => ['']}}, + { 'auth_ldap_context' => { display => 'text', value => ['']}}, + { 'auth_ms_ldap_server' => { display => 'text', value => ['']}}, + { 'auth_ms_ldap_dn_acc' => { display => 'text', value => ['']}}, + { 'auth_ms_ldap_dn_psw' => { display => 'text', value => ['']}}, + { 'auth_ms_ldap_base' => { display => 'text', value => ['']}}, + { 'auth_ms_ldap_attr' => { display => 'text', value => ['']}}, + { 'auth_radius_server' => { display => 'text', value => ['']}}, + { 'auth_radius_secret' => { display => 'text', value => ['secret']}}, + { 'auth_tacacs_server' => { display => 'text', value => ['']}}, + { 'auth_tacacs_secret' => { display => 'text', value => ['secret']}}, + { 'auth_web_key' => { display => 'text', value => ['thisismysecretkey']}} + ], + + 'escalation' => [ + { 'escalate0' => { display => 'text', value => ['0'], + validate => { 'int' => [ 0, undef ] }}}, + { 'escalate1' => { display => 'text', value => ['300'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate2' => { display => 'text', value => ['900'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate3' => { display => 'text', value => ['1800'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate4' => { display => 'text', value => ['2400'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate5' => { display => 'text', value => ['3600'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate6' => { display => 'text', value => ['7200'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate7' => { display => 'text', value => ['10800'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate8' => { display => 'text', value => ['21600'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate9' => { display => 'text', value => ['43200'], + validate => { 'int' => [ 60, undef ] }}}, + { 'escalate10' => { display => 'text', value => ['86400'], + validate => { 'int' => [ 60, undef ] }}} + ], + + 'daemons' => [ + { 'daemon_ipsla_active' => { display => 'popup', value => ['true','false']}}, + { 'daemon_ipsla_filename' => { display => 'text', value => ['ipslad.pl']}}, + { 'daemon_fping_active' => { display => 'popup', value => ['true','false']}}, + { 'daemon_fping_filename' => { display => 'text', value => ['fpingd.pl']}} + ], + + 'metrics' => [ + { 'weight_availability' => { display => 'text', value => ['0.1'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'weight_int' => { display => 'text', value => ['0.2'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'weight_mem' => { display => 'text', value => ['0.1'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'weight_cpu' => { display => 'text', value => ['0.1'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'weight_reachability' => { display => 'text', value => ['0.3'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'weight_response' => { display => 'text', value => ['0.2'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'metric_health' => { display => 'text', value => ['0.4'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'metric_availability' => { display => 'text', value => ['0.2'], + validate => { 'float' => [ 0, 1 ] }}}, + { 'metric_reachability' => { display => 'text', value => ['0.4'], + validate => { 'float' => [ 0, 1 ] }}} + ], + + 'graph' => [ + { 'graph_amount' => { display => 'text', value => ['48'], + # nonzero positive float + validate => { "float" => [ undef, undef, 0, undef ] }}}, + { 'graph_unit' => { display => 'text', value => ['hours'], + validate => { 'onefromlist' => [qw(minutes hours days months years)] }}}, + { 'graph_factor' => { display => 'text', value => ['2']}}, + { 'graph_width' => { display => 'text', value => ['700'], + validate => { 'int' => [ 32, undef ] }}}, + { 'graph_height' => { display => 'text', value => ['250'], + validate => { 'int' => [ 32, undef ] }}}, + { 'graph_split' => { display => 'popup', value => ['true','false']}}, + { 'win_width' => { display => 'text', value => ['835'], + validate => { 'int' => [ 200, undef ] }}}, + { 'win_height' => { display => 'text', value => ['570'], + validate => { 'int' => [ 200, undef ] }}} + ], + + 'tables NMIS4' => [ + { 'Interface_Table' => { display => 'text', value => ['']}}, + { 'Interface_Key' => { display => 'text', value => ['']}}, + { 'Escalation_Table' => { display => 'text', value => ['']}}, + { 'Escalation_Key' => { display => 'text', value => ['']}}, + { 'Locations_Table' => { display => 'text', value => ['']}}, + { 'Locations_Key' => { display => 'text', value => ['']}}, + { 'Nodes_Table' => { display => 'text', value => ['']}}, + { 'Nodes_Key' => { display => 'text', value => ['']}}, + { 'Users_Table' => { display => 'text', value => ['']}}, + { 'Users_Key' => { display => 'text', value => ['']}}, + { 'Contacts_Table' => { display => 'text', value => ['']}}, + { 'Contacts_Key' => { display => 'text', value => ['']}} + ], + + 'mibs' => [ + { 'full_mib' => { display => 'text', value => ['nmis_mibs.oid']}} + ], + + 'database' => [ + { 'db_events_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_nodes_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_users_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_locations_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_contacts_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_privmap_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_escalations_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_services_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_iftypes_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_access_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_logs_sql' => { display => 'popup', value => ['true','false']}}, + { 'db_links_sql' => { display => 'popup', value => ['true','false']}}, + ], + } + ); + diff --git a/install/Table-Nodes.nmis b/install/Table-Nodes.nmis index 7d8afca..991f556 100644 --- a/install/Table-Nodes.nmis +++ b/install/Table-Nodes.nmis @@ -32,8 +32,7 @@ use Auth; use NMIS::UUID; my $C = loadConfTable(); -# variables used for the security mods -my $AU = Auth->new(conf => $C); # Auth::new will reap init values from NMIS::config +my $AU = Auth->new(conf => $C); # Calling program needs to do auth, then set the ENVIRONMENT before this is called. $AU->SetUser($ENV{'NMIS_USER'}); @@ -42,6 +41,8 @@ my @groups = (); my $GT = loadGroupTable(); foreach (sort split(',',$C->{group_list})) { push @groups, $_ if $AU->InGroup($_); } +my @pollingpolicies = ("default", sort keys %{ loadGenericTable("Polling-Policy") || {}}); + my @nodes = (); my $LNT = loadLocalNodeTable(); # load from file or db foreach (sort {lc($a) cmp lc($b)} keys %{$LNT}) { push @nodes, $_ if $AU->InGroup($LNT->{$_}{group}); } @@ -54,18 +55,23 @@ if ( opendir(MDL,$C->{''}) ) { } closedir(MDL); -my $uuid = getUUID(); +my $uuid = NMIS::UUID::getUUID(); %hash = ( Nodes => [ - { name => { mandatory => 'true', header => 'Name',display => 'key,header,text',value => [""] }}, + { name => { mandatory => 'true', header => 'Name',display => 'key,header,text',value => [""], + validate => { 'regex' => qr!^[^/]+$! } }}, { new_name => { header => 'New Name', display => 'text,editonly', value => [""] }}, { uuid => { header => 'UUID',display => 'header,readonly',value => ["$uuid"] }}, - { host => { mandatory => 'true', header => 'Host Name/IP Address',display => 'header,text',value => [""] }}, - { group => { mandatory => 'true', header => 'Group',display => 'header,popup',value => [ @groups] }}, + { host => { mandatory => 'true', header => 'Host Name/IP Address',display => 'header,text',value => [""], + # nmis doesn't support ipv6 yet + validate => { "resolvable" => [ 4 ] } } + }, + { group => { mandatory => 'true', header => 'Group',display => 'header,popup',value => [ @groups], + validate => { "onefromlist" => undef } } }, { community => { mandatory => 'true', header => 'SNMP Community',display => 'text',value => ["$C->{default_communityRO}"] }}, { wmi => { special=>'separator', header => "WMI Options", } }, @@ -76,39 +82,67 @@ my $uuid = getUUID(); { customer => { header => 'Customer',display => 'header,popup',value => [ sort keys %{loadGenericTable('Customers')}] }}, { businessService => { header => 'Business Service',display => 'header,scrolling',value => [ sort keys %{loadGenericTable('BusinessServices')} ] }}, { serviceStatus => { header => 'Service Status',display => 'popup',value => [ sort keys %{loadGenericTable('ServiceStatus')} ] }}, - { extra_options => { special => 'separator', header => 'Remote Connection'}}, + + { extra_options => { special => 'separator', header => 'Name and URL for additional node information'}}, + { node_context_name => { header => 'Node Context Name',display => 'text',value => ["Node Context"] }}, + { node_context_url => { header => 'Node Context URL',display => 'text',value => ["https://somelink.com/map/thing/"] }}, + + { extra_options => { special => 'separator', header => 'Name and URL for remote management connection'}}, { remote_connection_name => { header => 'Remote Connection Name',display => 'text',value => ["SSH to Node"] }}, { remote_connection_url => { header => 'Remote Connection URL',display => 'text',value => ["ssh://\$host"] }}, + { extra_options => { special => 'separator', header => 'Extra Options'}}, - { display_name => { header => "Display Name", display => "header,text", value => "" }}, + { display_name => { header => "Display Name", display => "header,text", value => [""] }}, { notes => { header => 'Notes',display => 'header,textbox',value => [""] }}, - { roleType => { header => 'Role Type', display => 'popup',value => [ split(/\s*,\s*/, $C->{roletype_list}) ] }}, - { netType => { header => 'Net Type', display => 'popup',value => [ split(/\s*,\s*/, $C->{nettype_list}) ] }}, - { location => { header => 'Location',display => 'header,popup',value => [ sort keys %{loadGenericTable('Locations')}] }}, + { roleType => { header => 'Role Type', display => 'popup', value => [ split(/\s*,\s*/, $C->{roletype_list}) ], + validate => { "onefromlist" => undef } }}, + { netType => { header => 'Net Type', display => 'popup',value => [ split(/\s*,\s*/, $C->{nettype_list}) ], + validate => { "onefromlist" => undef } }}, + { location => { header => 'Location',display => 'header,popup',value => [ sort keys %{loadGenericTable('Locations')}], + validate => { "onefromlist" => undef } }}, { advanced_options => { special => 'separator', header => 'Advanced Options'}}, - { model => { header => 'Model',display => 'popup',value => [@models] }}, - { active => { header => 'Active',display => 'header,popup',value => ["true", "false"] }}, - { ping => { header => 'Ping', display => 'header,popup',value => ["true", "false"] }}, - { collect => { header => 'Collect',display => 'header,popup',value => ["true", "false"] }}, - { cbqos => { header => 'CBQoS',display => 'popup',value => ["none", "input", "output", "both"] }}, - { calls=> { header => 'Modem Calls', display => 'popup',value => ["false", "true"] }}, - { threshold => { header => 'Threshold', display => 'popup',value => ["true", "false"] }}, - { webserver => { header => 'Web Server', display => 'popup',value => ["false", "true"] }}, - { depend =>{ header => 'Depend', display => 'header,scrolling',value => [ "N/A", @nodes ] }}, - { services => { header => 'Services', display => 'header,scrolling',value => ["", sort keys %{loadServicesTable()}] }}, - { timezone => { header => 'Time Zone',display => 'text',value => ["0"] }}, + { polling_policy => { header => "Polling Policy", display => 'header,popup', value => [@pollingpolicies]}}, + { model => { header => 'Model',display => 'popup',value => [@models], + validate => { "onefromlist" => undef } }}, + { active => { header => 'Active',display => 'header,popup',value => ["true", "false"], + validate => { "onefromlist" => undef } }}, + { ping => { header => 'Ping', display => 'header,popup',value => ["true", "false"], + validate => { "onefromlist" => undef } }}, + { collect => { header => 'Collect',display => 'header,popup',value => ["true", "false"], + validate => { "onefromlist" => undef } }}, + { cbqos => { header => 'CBQoS',display => 'popup',value => ["none", "input", "output", "both"], + validate => { "onefromlist" => undef } }}, + { calls=> { header => 'Modem Calls', display => 'popup',value => ["false", "true"], + validate => { "onefromlist" => undef } }}, + { threshold => { header => 'Threshold', display => 'popup',value => ["true", "false"], + validate => { "onefromlist" => undef } }}, + { webserver => { header => 'Web Server', display => 'popup',value => ["false", "true"], + validate => { "onefromlist" => undef } }}, + { depend =>{ header => 'Depend', display => 'header,scrolling',value => [ "N/A", @nodes ], + validate => { "multifromlist" => undef } }}, + { services => { header => 'Services', display => 'header,scrolling', + value => ["", sort keys %{loadServicesTable()}], + validate => { "multifromlist" => undef } }}, + { timezone => { header => 'Time Zone',display => 'text',value => ["0"], + validate => { regex => qr/^([+-]?\d{1,2}(:\d{1,2})?)?$/ } } }, { extra_options => { special => 'separator', header => 'SNMP Settings'}}, - { version => { header => 'SNMP Version',display => 'popup',value => ["snmpv2c","snmpv1","snmpv3"] }}, - { max_msg_size => { header => "SNMP Max Message Size", display => 'text', value => ["$C->{snmp_max_msg_size}"] }}, - { max_repetitions => { header => "SNMP Max Repetitions", display => "text", value => ["0"] }}, - { port => { header => 'SNMP Port', display => 'text',value => ["161"] }}, + { version => { header => 'SNMP Version',display => 'popup',value => ["snmpv2c","snmpv1","snmpv3"], + validate => { "onefromlist" => undef } }}, + { max_msg_size => { header => "SNMP Max Message Size", display => 'text', value => ["$C->{snmp_max_msg_size}"], + validate => { 'int' => [ 484, 65535 ] } } }, + { max_repetitions => { header => "SNMP Max Repetitions", display => "text", value => ["0"], + validate => { 'int' => [ 0, 1000 ] } } }, + { port => { header => 'SNMP Port', display => 'text',value => ["161"], + validate => { 'int' => [ 1, 65535 ] } }}, { username => { header => 'SNMP Username',display => 'text',value => ["$C->{default_username}"] }}, { context => { header => 'SNMP Context',display => 'text',value => [""] }}, { authpassword => { header => 'SNMP Auth Password',display => 'text',value => ["$C->{default_authpassword}"] }}, { authkey => { header => 'SNMP Auth Key',display => 'text',value => ["$C->{default_authkey}"] }}, - { authprotocol => { header => 'SNMP Auth Proto',display => 'popup',value => ['md5','sha'] }}, + { authprotocol => { header => 'SNMP Auth Proto',display => 'popup',value => ['md5','sha'], + validate => { "onefromlist" => undef } }}, { privpassword => { header => 'SNMP Priv Password',display => 'text',value => ["$C->{default_privpassword}"] }}, { privkey => { header => 'SNMP Priv Key',display => 'text',value => ["$C->{default_privkey}"] }}, - { privprotocol => { header => 'SNMP Priv Proto',display => 'popup',value => ['des','aes','3des'] }}, + { privprotocol => { header => 'SNMP Priv Proto',display => 'popup',value => ['des','aes','3des'], + validate => { "onefromlist" => undef } }}, ] ); diff --git a/install/Table-Polling-Policy.nmis b/install/Table-Polling-Policy.nmis new file mode 100644 index 0000000..902541f --- /dev/null +++ b/install/Table-Polling-Policy.nmis @@ -0,0 +1,26 @@ +%hash = ( + "Polling-Policy" => [ + { 'name' => { mandatory => 'true', + header => 'Policy Name', + display => 'key,header,text', + value => [ "" ] }}, + { 'ping' => { mandatory => 'true', + header => 'ICMP/Ping Frequency', + display => 'header,popup', + value => [ "1m", "2m", "5m", "15m", "30m", "1h", "6h", "1d" ] }}, + + { 'snmp' => { mandatory => 'true', + header => 'SNMP Polling Frequency', + display => 'header,popup', + value => [ "5m", "1m", "2m", "15m", "30m", "1h", "6h", "1d" ] }}, + + { 'wmi' => { mandatory => 'true', + header => 'WMI Polling Frequency', + display => 'header,popup', + value => [ "5m", "1m", "2m", "15m", "30m", "1h", "6h", "1d" ] }}, + + { 'description' => { header => "Description", + display => "header,textbox", + value => [ '' ] }}, + + ]); diff --git a/install/Tables.nmis b/install/Tables.nmis index a312b91..21f436d 100644 --- a/install/Tables.nmis +++ b/install/Tables.nmis @@ -99,6 +99,13 @@ 'DisplayName' => 'Portal', 'Table' => 'Portal' }, + 'Polling-Policy' => + { + 'CaseSensitiveKey' => 'true', + 'Description' => "Definition of custom Polling Policies", + 'DisplayName' => 'Polling Policy', + 'Table' => "Polling-Policy", + }, 'PrivMap' => { 'CaseSensitiveKey' => 'false', 'Description' => 'Privilege Map for assigning roles to users', diff --git a/install/plugins/AlcatelASAM.pm b/install/plugins/AlcatelASAM.pm index aab4bb2..2d57156 100644 --- a/install/plugins/AlcatelASAM.pm +++ b/install/plugins/AlcatelASAM.pm @@ -30,12 +30,14 @@ # a small update plugin for converting the cdp index into interface name. package AlcatelASAM; -our $VERSION = "1.1.0"; +our $VERSION = "1.2.0"; use strict; use NMIS; # lnt use func; # for the conf table extras use snmp 1.1.0; # for snmp-related access +use Net::SNMP qw(oid_lex_sort); +use Data::Dumper; sub update_plugin { @@ -46,7 +48,9 @@ sub update_plugin my $NC = $S->ndcfg; my $NI = $S->ndinfo; my $IF = $S->ifinfo; - + my $ifTable = $NI->{ifTable}; + my $V = $S->view; + # anything to do? return (0,undef) if ( $NI->{system}{nodeModel} !~ "AlcatelASAM" or !getbool($NI->{system}->{collect})); @@ -55,6 +59,27 @@ sub update_plugin my $asamVersion42 = qr/OSWPAA42|L6GPAA42|OSWPAA46/; my $asamVersion43 = qr/OSWPRA43|OSWPAN43/; + # we have been told index 17 of the eqptHolder is the ASAM Model + my $asamModel = $NI->{eqptHolder}{17}{eqptHolderPlannedType}; + + + if ( $asamModel eq "NFXS-A" ) { + $asamModel = "7302 ($asamModel)"; + } + elsif ( $asamModel eq "NFXS-B" ) { + $asamModel = "7330-FD ($asamModel)"; + } + elsif ( $asamModel eq "ARAM-D" ) { + $asamModel = "ARAM-D ($asamModel)"; + } + elsif ( $asamModel eq "ARAM-E" ) { + $asamModel = "ARAM-E ($asamModel)"; + } + + $NI->{system}{asamModel} = $asamModel; + $V->{system}{"asamModel_value"} = $asamModel; + $V->{system}{"asamModel_title"} = "ASAM Model"; + my $asamSoftwareVersion = $NI->{system}{asamSoftwareVersion1}; if ( $NI->{system}{asamActiveSoftware2} eq "active" ) { @@ -80,82 +105,290 @@ sub update_plugin logMsg("ERROR: Unknown ASAM Version $node asamSoftwareVersion=$asamSoftwareVersion"); } - # Get the SNMP Session going. - my $snmp = snmp->new(name => $node); - return (2,"Could not open SNMP session to node $node: ".$snmp->error) - if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})); - return (2, "Could not retrieve SNMP vars from node $node: ".$snmp->error) - if (!$snmp->testsession); + $NI->{system}{asamVersion} = $version; + $V->{system}{"asamVersion_value"} = $version; + $V->{system}{"asamVersion_title"} = "ASAM Version"; + my $changesweremade = 0; + + my ($session, $error) = Net::SNMP->session( + #-hostname => $NC->{node}{host}, + #-port => $NC->{node}{port}, + #-version => $NC->{node}{version}, + #-community => $NC->{node}{community}, # v1/v2c + + -hostname => $LNT->{$node}{host}, + -port => $LNT->{$node}{port}, + -version => $LNT->{$node}{version}, + -community => $LNT->{$node}{community}, # v1/v2c + ); + + if ( $error ) { + dbg("ERROR with SNMP on $node: ". $error); + return ($changesweremade,undef); + } + + # Get the SNMP Session going. + #my $snmp = snmp->new(name => $node); + #return (2,"Could not open SNMP session to node $node: ".$snmp->error) + # if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})); + #return (2, "Could not retrieve SNMP vars from node $node: ".$snmp->error) + # if (!$snmp->testsession); - info("Working on $node atmVcl"); + info("Working on $node Customer_ID"); + foreach my $ifIndex (oid_lex_sort(keys %{$NI->{Customer_ID}})) { + my $entry = $NI->{Customer_ID}{$ifIndex}; - my $offset = 12288; - if ( $version eq "4.2" ) { - $offset = 6291456; + if ( defined $NI->{ifTable}{$ifIndex} ) { + $entry->{ifDescr} = $NI->{ifTable}{$ifIndex}{ifDescr}; + $entry->{ifAdminStatus} = $NI->{ifTable}{$ifIndex}{ifAdminStatus}; + $entry->{ifOperStatus} = $NI->{ifTable}{$ifIndex}{ifOperStatus}; + $entry->{ifType} = $NI->{ifTable}{$ifIndex}{ifType}; + $changesweremade = 1; + } } + + if ( $session ) { + # Using the data we collect from the atmVcl we will fill in the details of the DSLAM Port. + info("Working on $node atmVcl"); - for my $key (keys %{$NI->{atmVcl}}) - { - my $entry = $NI->{atmVcl}->{$key}; - - if ( my @parts = split(/\./,$entry->{index}) ) + my $offset = 12288; + if ( $version eq "4.2" ) { + $offset = 6291456; + } + + my @atmVclVars = qw( + asamIfExtCustomerId + xdslLineServiceProfileNbr + xdslLineSpectrumProfileNbr + ); + + # the ordered list of SNMP variables I want. + my @dslamVarList = qw( + asamIfExtCustomerId + xdslLineServiceProfileNbr + xdslLineSpectrumProfileNbr + xdslLineOutputPowerDownstream + xdslLineLoopAttenuationUpstream + xdslFarEndLineOutputPowerUpstream + xdslFarEndLineLoopAttenuationDownstream + xdslXturInvSystemSerialNumber + xdslLinkUpActualBitrateUpstream + xdslLinkUpActualBitrateDownstream + xdslLinkUpActualNoiseMarginUpstream + xdslLinkUpActualNoiseMarginDownstream + xdslLinkUpAttenuationUpstream + xdslLinkUpAttenuationDownstream + xdslLinkUpAttainableBitrateUpstream + xdslLinkUpAttainableBitrateDownstream + xdslLinkUpMaxBitrateUpstream + xdslLinkUpMaxBitrateDownstream + ); + + foreach my $key (oid_lex_sort(keys %{$NI->{atmVcl}})) { - my $ifIndex = shift(@parts); - my $atmVclVpi = shift(@parts); - my $atmVclVci = shift(@parts); - - my $offsetIndex = $ifIndex - $offset; + my $entry = $NI->{atmVcl}->{$key}; + + if ( my @parts = split(/\./,$entry->{index}) ) + { + my $ifIndex = shift(@parts); + my $atmVclVpi = shift(@parts); + my $atmVclVci = shift(@parts); + + # the crazy magic of ASAM + my $offsetIndex = $ifIndex - $offset; + + # the set of oids with dynamic index I want. + my %atmOidSet = ( + asamIfExtCustomerId => "1.3.6.1.4.1.637.61.1.6.5.1.1.$offsetIndex", + xdslLineServiceProfileNbr => "1.3.6.1.4.1.637.61.1.39.3.7.1.1.$offsetIndex", + xdslLineSpectrumProfileNbr => "1.3.6.1.4.1.637.61.1.39.3.7.1.2.$offsetIndex", + ); - my $asamIfExtCustomerId = "1.3.6.1.4.1.637.61.1.6.5.1.1.$offsetIndex"; - my $xdslLineServiceProfileNbr = "1.3.6.1.4.1.637.61.1.39.3.7.1.1.$offsetIndex"; - my $xdslLineSpectrumProfileNbr = "1.3.6.1.4.1.637.61.1.39.3.7.1.2.$offsetIndex"; + # build an array combining the atmVclVars and atmOidSet into a single array + my @oids = map {$atmOidSet{$_}} @atmVclVars; + #print Dumper \@oids; - my @oids = [ - "$asamIfExtCustomerId", - "$xdslLineServiceProfileNbr", - "$xdslLineSpectrumProfileNbr", - ]; + # get the snmp data from the thing + my $snmpdata = $session->get_request( + -varbindlist => \@oids + ); + + if ( $session->error() ) { + dbg("ERROR with SNMP on $node: ". $session->error()); + } + + # save the data for the atmVcl + $entry->{ifIndex} = $ifIndex; + $entry->{atmVclVpi} = $atmVclVpi; + $entry->{atmVclVci} = $atmVclVci; + $entry->{asamIfExtCustomerId} = "N/A"; + $entry->{xdslLineServiceProfileNbr} = "N/A"; + $entry->{xdslLineSpectrumProfileNbr} = "N/A"; + + if ( $snmpdata ) { + + foreach my $var (@atmVclVars) { + my $dataKey = $atmOidSet{$var}; + if ( $snmpdata->{$dataKey} ne "" and $snmpdata->{$dataKey} !~ /SNMP ERROR/ ) { + $entry->{$var} = $snmpdata->{$dataKey}; + } + else { + dbg("ERROR with SNMP on $node var=$var: ".$snmpdata->{$dataKey}) if ($snmpdata->{$dataKey} =~ /SNMP ERROR/); + $entry->{$var} = "N/A"; + } + } + + dbg("atmVcl SNMP Results: ifIndex=$ifIndex atmVclVpi=$atmVclVpi atmVclVci=$atmVclVci asamIfExtCustomerId=$entry->{asamIfExtCustomerId}"); - my $snmpdata = $snmp->get(@oids); - - $entry->{ifIndex} = $ifIndex; - $entry->{atmVclVpi} = $atmVclVpi; - $entry->{atmVclVci} = $atmVclVci; - $entry->{asamIfExtCustomerId} = "N/A"; - $entry->{xdslLineServiceProfileNbr} = "N/A"; - $entry->{xdslLineSpectrumProfileNbr} = "N/A"; - - if ( $snmpdata->{$asamIfExtCustomerId} ne "" and $snmpdata->{$asamIfExtCustomerId} !~ /SNMP ERROR/ ) { - $entry->{asamIfExtCustomerId} = $snmpdata->{$asamIfExtCustomerId}; - } + if ( defined $IF->{$ifIndex}{ifDescr} ) { + $entry->{ifDescr} = $IF->{$ifIndex}{ifDescr}; + $entry->{ifDescr_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$ifIndex&node=$node"; + $entry->{ifDescr_id} = "node_view_$node"; + } + else { + $entry->{ifDescr} = getIfDescr(prefix => "ATM", version => $version, ifIndex => $ifIndex, asamModel => $asamModel); + } - if ( $snmpdata->{$xdslLineServiceProfileNbr} ne "" and $snmpdata->{$xdslLineServiceProfileNbr} !~ /SNMP ERROR/ ) { - $entry->{xdslLineServiceProfileNbr} = $snmpdata->{$xdslLineServiceProfileNbr}; + $changesweremade = 1; + } } + } + + #"xdslLinkUp" "1.3.6.1.4.1.637.61.1.39.12" + #"xdslLinkUpTable" "1.3.6.1.4.1.637.61.1.39.12.1" + #"xdslLinkUpEntry" "1.3.6.1.4.1.637.61.1.39.12.1.1" + #"xdslLinkUpTimestampDown" "1.3.6.1.4.1.637.61.1.39.12.1.1.1" + #"xdslLinkUpTimestampUp" "1.3.6.1.4.1.637.61.1.39.12.1.1.2" + #"xdslLinkUpThresholdBitrateUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.13" + #"xdslLinkUpThresholdBitrateDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.14" + #"xdslLinkUpMaxDelayUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.15" + #"xdslLinkUpMaxDelayDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.16" + #"xdslLinkUpTargetNoiseMarginUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.17" + #"xdslLinkUpTargetNoiseMarginDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.18" + #"xdslLinkUpTimestamp" "1.3.6.1.4.1.637.61.1.39.12.2" + #"xdslLinkUpLineBitmapTable" "1.3.6.1.4.1.637.61.1.39.12.3" + #"xdslLinkUpLineBitmapEntry" "1.3.6.1.4.1.637.61.1.39.12.3.1" + #"xdslLinkUpLineBitmap" "1.3.6.1.4.1.637.61.1.39.12.3.1.1" + + #"asamIfExtCustomerId" "1.3.6.1.4.1.637.61.1.6.5.1.1" + #"xdslLineServiceProfileNbr" "1.3.6.1.4.1.637.61.1.39.3.7.1.1" + + #"xdslLineOutputPowerDownstream" "1.3.6.1.4.1.637.61.1.39.3.8.1.1.3" + #"xdslLineLoopAttenuationUpstream" "1.3.6.1.4.1.637.61.1.39.3.8.1.1.5" + #"xdslFarEndLineOutputPowerUpstream" "1.3.6.1.4.1.637.61.1.39.4.1.1.1.3" + #"xdslFarEndLineLoopAttenuationDownstream" "1.3.6.1.4.1.637.61.1.39.4.1.1.1.5" + + #"xdslXturInvSystemSerialNumber" "1.3.6.1.4.1.637.61.1.39.8.1.1.2" + + #"xdslLinkUpActualBitrateUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.3" + #"xdslLinkUpActualBitrateDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.4" + #"xdslLinkUpActualNoiseMarginUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.5" + #"xdslLinkUpActualNoiseMarginDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.6" + #"xdslLinkUpAttenuationUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.7" + #"xdslLinkUpAttenuationDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.8" + #"xdslLinkUpAttainableBitrateUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.9" + #"xdslLinkUpAttainableBitrateDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.10" + #"xdslLinkUpMaxBitrateUpstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.11" + #"xdslLinkUpMaxBitrateDownstream" "1.3.6.1.4.1.637.61.1.39.12.1.1.12" + + info("Working on $node ifTable for DSLAM Port Data"); + + for my $ifIndex (oid_lex_sort(keys %{$NI->{ifTable}})) { + my $entry = $NI->{ifTable}->{$ifIndex}; + my $dslamPort = $NI->{DSLAM_Ports}->{$ifIndex}; + if ( $entry->{ifDescr} eq "XDSL Line" ) { + # the crazy magic of ASAM + my $atmOffsetIndex = $ifIndex + $offset; + + # the set of oids with dynamic index I want. + my %dslamOidSet = ( + asamIfExtCustomerId => "1.3.6.1.4.1.637.61.1.6.5.1.1.$ifIndex", + xdslLineServiceProfileNbr => "1.3.6.1.4.1.637.61.1.39.3.7.1.1.$ifIndex", + xdslLineSpectrumProfileNbr => "1.3.6.1.4.1.637.61.1.39.3.7.1.2.$ifIndex", + xdslLineOutputPowerDownstream => "1.3.6.1.4.1.637.61.1.39.3.8.1.1.3.$ifIndex", + xdslLineLoopAttenuationUpstream => "1.3.6.1.4.1.637.61.1.39.3.8.1.1.5.$ifIndex", + xdslFarEndLineOutputPowerUpstream => "1.3.6.1.4.1.637.61.1.39.4.1.1.1.3.$ifIndex", + xdslFarEndLineLoopAttenuationDownstream => "1.3.6.1.4.1.637.61.1.39.4.1.1.1.5.$ifIndex", + xdslXturInvSystemSerialNumber => "1.3.6.1.4.1.637.61.1.39.8.1.1.2.$ifIndex", + xdslLinkUpActualBitrateUpstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.3.$ifIndex", + xdslLinkUpActualBitrateDownstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.4.$ifIndex", + xdslLinkUpActualNoiseMarginUpstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.5.$ifIndex", + xdslLinkUpActualNoiseMarginDownstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.6.$ifIndex", + xdslLinkUpAttenuationUpstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.7.$ifIndex", + xdslLinkUpAttenuationDownstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.8.$ifIndex", + xdslLinkUpAttainableBitrateUpstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.9.$ifIndex", + xdslLinkUpAttainableBitrateDownstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.10.$ifIndex", + xdslLinkUpMaxBitrateUpstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.11.$ifIndex", + xdslLinkUpMaxBitrateDownstream => "1.3.6.1.4.1.637.61.1.39.12.1.1.12.$ifIndex", + ); + + # build an array combining the dslamVarList and dslamOidSet into a single array + my @oids = map {$dslamOidSet{$_}} @dslamVarList; + #print Dumper \@oids; + + # get the snmp data from the thing + my $snmpdata = $session->get_request( + -varbindlist => \@oids + ); + + if ( $session->error() ) { + dbg("ERROR with SNMP on $node: ". $session->error()); + } + + # save the data for the dslamPort + $dslamPort->{ifIndex} = $ifIndex; + $dslamPort->{atmIfIndex} = $atmOffsetIndex; + + if ( $snmpdata ) { + + # now get each of the required vars snmp data into the entry for saving. + foreach my $var (@dslamVarList) { + my $dataKey = $dslamOidSet{$var}; + if ( $snmpdata->{$dataKey} ne "" and $snmpdata->{$dataKey} !~ /SNMP ERROR/ ) { + $dslamPort->{$var} = $snmpdata->{$dataKey}; + } + else { + dbg("ERROR with SNMP on $node var=$var: ".$snmpdata->{$dataKey}) if ($snmpdata->{$dataKey} =~ /SNMP ERROR/); + $dslamPort->{$var} = "N/A"; + } + } + + $dslamPort->{ifDescr} = getIfDescr(prefix => "ATM", version => $version, ifIndex => $atmOffsetIndex, asamModel => $asamModel); + + dbg("DSLAM SNMP Results: ifIndex=$ifIndex ifDescr=$dslamPort->{ifDescr} asamIfExtCustomerId=$dslamPort->{asamIfExtCustomerId}"); + + if ( $entry->{ifLastChange} ) { + $dslamPort->{ifLastChange} = convUpTime(int($entry->{ifLastChange}/100)); + } + else { + $dslamPort->{ifLastChange} = '0:00:00', + } + $dslamPort->{ifOperStatus} = $entry->{ifOperStatus} ? $entry->{ifOperStatus} : "N/A"; + $dslamPort->{ifAdminStatus} = $entry->{ifAdminStatus} ? $entry->{ifAdminStatus} : "N/A"; - if ( $snmpdata->{$xdslLineSpectrumProfileNbr} ne "" and $snmpdata->{$xdslLineSpectrumProfileNbr} !~ /SNMP ERROR/ ) { - $entry->{xdslLineSpectrumProfileNbr} = $snmpdata->{$xdslLineSpectrumProfileNbr}; - } - dbg("ASAM SNMP Results: ifIndex=$ifIndex atmVclVpi=$atmVclVpi atmVclVci=$atmVclVci asamIfExtCustomerId=$entry->{asamIfExtCustomerId}"); + # get the Service Profile Name based on the xdslLineServiceProfileNbr + if ( defined $NI->{xdslLineServiceProfile} and defined $dslamPort->{xdslLineServiceProfileNbr} ) { + my $profileNumber = $dslamPort->{xdslLineServiceProfileNbr}; + $dslamPort->{xdslLineServiceProfileName} = $NI->{xdslLineServiceProfile}{$profileNumber}{xdslLineServiceProfileName} ? $NI->{xdslLineServiceProfile}{$profileNumber}{xdslLineServiceProfileName} : "N/A"; + } - if ( defined $IF->{$ifIndex}{ifDescr} ) { - $entry->{ifDescr} = $IF->{$ifIndex}{ifDescr}; - $entry->{ifDescr_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$ifIndex&node=$node"; - $entry->{ifDescr_id} = "node_view_$node"; + $changesweremade = 1; + } } else { - $entry->{ifDescr} = getIfDescr(prefix => "ATM", version => $version, ifIndex => $ifIndex); + delete $NI->{DSLAM_Ports}->{$ifIndex}; } - - $changesweremade = 1; } } + else { + dbg("ERROR some session problem with SNMP on $node"); + } info("Working on $node ifStack"); - for my $key (keys %{$NI->{ifStack}}) + for my $key (oid_lex_sort(keys %{$NI->{ifStack}})) { my $entry = $NI->{ifStack}->{$key}; @@ -192,6 +425,7 @@ sub getIfDescr { my $oid_value = $args{ifIndex}; my $prefix = $args{prefix}; + my $asamModel = $args{asamModel}; if ( $args{version} eq "4.1" or $args{version} eq "4.3" ) { my $rack_mask = 0x70000000; @@ -210,7 +444,11 @@ sub getIfDescr { $slot = $slot - 2; ++$circuit; - return "$prefix-$rack-$shelf-$slot-$circuit"; + my $slotCor = asamSlotCorrection($slot,$asamModel); + + dbg("ASAM getIfDescr: ifIndex=$args{ifIndex} slot=$slot $slotCor=$slotCor asamVersion=$args{version} asamModel=$asamModel"); + + return "$prefix-$rack-$shelf-$slotCor-$circuit"; } else { my $slot_mask = 0x7E000000; @@ -228,11 +466,35 @@ sub getIfDescr { ++$circuit; $prefix = "XDSL" if $level == 16; + + my $slotCor = asamSlotCorrection($slot,$asamModel); + + dbg("ASAM getIfDescr: ifIndex=$args{ifIndex} slot=$slot slotCor=$slotCor asamVersion=$args{version} asamModel=$asamModel"); - return "$prefix-1-1-$slot-$circuit"; + return "$prefix-1-1-$slotCor-$circuit"; } } - +sub asamSlotCorrection { + my $slot = shift; + my $asamModel = shift; + + if ( $asamModel =~ /7302/ and $slot >= 9 ) { + $slot = $slot + 1; + } + elsif ( $asamModel =~ /ARAM-D/ ) { + $slot = $slot + 3 + } + elsif ( $asamModel =~ /ARAM-E/ and $slot < 9 ) { + $slot = $slot + 1 + } + elsif ( $asamModel =~ /ARAM-E/ and $slot >= 9 ) { + $slot = $slot + 3 + } + elsif ( $asamModel =~ /7330-FD/ ) { + $slot = $slot + 3 + } + return $slot; +} 1; diff --git a/install/plugins/AsamInterface.pm b/install/plugins/AsamInterface.pm index 96209a0..c22654e 100644 --- a/install/plugins/AsamInterface.pm +++ b/install/plugins/AsamInterface.pm @@ -8,6 +8,8 @@ use strict; use NMIS; # lnt use func; # for loading extra tables use snmp 1.1.0; # for snmp-related access +use Net::SNMP qw(oid_lex_sort); +use Data::Dumper; sub update_plugin { @@ -30,13 +32,32 @@ sub update_plugin logMsg("ERROR $errmsg") if $errmsg; $override ||= {}; + my $changesweremade = 0; + + my ($session, $error) = Net::SNMP->session( + #-hostname => $NC->{node}{host}, + #-port => $NC->{node}{port}, + #-version => $NC->{node}{version}, + #-community => $NC->{node}{community}, # v1/v2c + + -hostname => $LNT->{$node}{host}, + -port => $LNT->{$node}{port}, + -version => $LNT->{$node}{version}, + -community => $LNT->{$node}{community}, # v1/v2c + ); + + if ( $error ) { + dbg("ERROR with SNMP on $node: ". $error); + return ($changesweremade,undef); + } + # Get the SNMP Session going. - my $snmp = snmp->new(name => $node); - return (2,"Could not open SNMP session to node $node: ".$snmp->error) - if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})); - - return (2, "Could not retrieve SNMP vars from node $node: ".$snmp->error) - if (!$snmp->testsession); + #my $snmp = snmp->new(name => $node); + #return (2,"Could not open SNMP session to node $node: ".$snmp->error) + # if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})); + # + #return (2, "Could not retrieve SNMP vars from node $node: ".$snmp->error) + # if (!$snmp->testsession); # remove any old redundant useless and otherwise annoying entries. delete $S->{info}{interface}; @@ -67,6 +88,26 @@ sub update_plugin my $asamVersion41 = qr/OSWPAA41|L6GPAA41|OSWPAA37|L6GPAA37|OSWPRA41/; my $asamVersion42 = qr/OSWPAA42|L6GPAA42|OSWPAA46/; my $asamVersion43 = qr/OSWPRA43|OSWPAN43/; + + # we have been told index 17 of the eqptHolder is the ASAM Model + my $asamModel = $NI->{eqptHolder}{17}{eqptHolderPlannedType}; + + if ( $asamModel eq "NFXS-A" ) { + $asamModel = "7302 ($asamModel)"; + } + elsif ( $asamModel eq "NFXS-B" ) { + $asamModel = "7330-FD ($asamModel)"; + } + elsif ( $asamModel eq "ARAM-D" ) { + $asamModel = "ARAM-D ($asamModel)"; + } + elsif ( $asamModel eq "ARAM-E" ) { + $asamModel = "ARAM-E ($asamModel)"; + } + + $NI->{system}{asamModel} = $asamModel; + $V->{system}{"asamModel_value"} = $asamModel; + $V->{system}{"asamModel_title"} = "ASAM Model"; my $rack_count = 1; my $shelf_count = 1; @@ -93,7 +134,7 @@ sub update_plugin # How to identify it is an ARAM-D? #"For ARAM-D with extensions " $version = 4.1; - my ($indexes,$rack_count,$shelf_count) = build_41_interface_indexes(NI => $NI); + my ($indexes,$rack_count,$shelf_count) = build_interface_indexes(NI => $NI); @ifIndexNum = @{$indexes}; } @@ -101,13 +142,13 @@ sub update_plugin elsif( $asamSoftwareVersion =~ /$asamVersion42/ ) { $version = 4.2; - my $indexes = build_42_interface_indexes(NI => $NI); + my $indexes = build_interface_indexes(NI => $NI); @ifIndexNum = @{$indexes}; } elsif( $asamSoftwareVersion =~ /$asamVersion43/ ) { $version = 4.3; - my ($indexes,$rack_count,$shelf_count) = build_41_interface_indexes(NI => $NI); + my ($indexes,$rack_count,$shelf_count) = build_interface_indexes(NI => $NI); @ifIndexNum = @{$indexes}; } else { @@ -118,28 +159,52 @@ sub update_plugin my $intfTotal = 0; my $intfCollect = 0; # reset counters - + foreach my $index (@ifIndexNum) { $intfTotal++; - my $ifDescr = getIfDescr(prefix => "ATM", version => $version, ifIndex => $index); + my $ifDescr = getIfDescr(prefix => "ATM", version => $version, ifIndex => $index, asamModel => $asamModel); my $Description = getDescription(version => $version, ifIndex => $index); my $offset = 12288; if ( $version eq "4.2" ) { $offset = 6291456; } + + my $offsetIndex = $index - $offset; #asamIfExtCustomerId my $prefix = "1.3.6.1.4.1.637.61.1.6.5.1.1"; - my $offsetIndex = $index - $offset; my $oid = "$prefix.$offsetIndex"; - my $customerid = $snmp->get($oid); - - dbg("SNMP $node $ifDescr $Description, index=$index, offset=$offset, offsetIndex=$offsetIndex, customerid=$customerid->{$oid}"); - if ( $customerid->{$oid} ne "" and $customerid->{$oid} !~ /SNMP ERROR/ ) { - $Description = $customerid->{$oid}; + + if ( defined $NI->{Customer_ID} and defined $NI->{Customer_ID}{$offsetIndex} ) { + $Description = $NI->{Customer_ID}{$offsetIndex}{asamIfExtCustomerId}; + dbg("Customer_ID $node $ifDescr $Description"); + } + else { + #my $customerid = $snmp->get($oid); + my $customerid; + if ( $session ) { + #print "DEBUG: running the SNMP NOW\n"; + my @oids = ( $oid ); + $customerid = $session->get_request( + -varbindlist => \@oids + ); + + if ( $session->error() ) { + dbg("ERROR with SNMP on $node: ". $session->error()); + } + } + else { + dbg("ERROR some session problem with SNMP on $node"); + } + + if ( $customerid->{$oid} ne "" and $customerid->{$oid} !~ /SNMP ERROR/ ) { + $Description = $customerid->{$oid}; + } + dbg("SNMP $node $ifDescr $Description, index=$index, offset=$offset, offsetIndex=$offsetIndex, customerid=$Description"); } + $S->{info}{interface}{$index} = { 'Description' => $Description, 'ifAdminStatus' => 'unknown', @@ -338,22 +403,21 @@ sub getRackShelfMatrix { elsif ( $version eq "4.2" ) { my $slot = 0; my @indexes; - foreach my $eqpt (sort {$a <=> $b} keys %{$eqptHolder} ) { - dbg("$eqpt, eqptHolderPlannedType=$eqptHolder->{$eqpt}{eqptHolderPlannedType}"); - if ( $eqptHolder->{$eqpt}{eqptHolderPlannedType} =~ /$rackMatch/ ) { - ++$slot; - } - } #foreach my $eqpt (sort {$a <=> $b} keys %{$eqptHolder} ) { - # dbg("$eqpt = eqptPortMapping=$eqptHolder->{$eqpt}{eqptPortMappingLSMSlot}"); - # if ( $eqptHolder->{$eqpt}{eqptPortMappingLSMSlot} != 65535 ) { + # dbg("$eqpt, eqptHolderPlannedType=$eqptHolder->{$eqpt}{eqptHolderPlannedType}"); + # if ( $eqptHolder->{$eqpt}{eqptHolderPlannedType} =~ /$rackMatch/ ) { # ++$slot; - # push(@indexes,$eqptHolder->{$eqpt}{eqptPortMappingLSMSlot}); # } #} + foreach my $eqpt (sort {$a <=> $b} keys %{$eqptHolder} ) { + dbg("$eqpt = eqptPortMapping=$eqptHolder->{$eqpt}{eqptPortMappingLSMSlot}"); + if ( $eqptHolder->{$eqpt}{eqptPortMappingLSMSlot} != 65535 ) { + ++$slot; + push(@indexes,$eqptHolder->{$eqpt}{eqptPortMappingLSMSlot}); + } + } $config{slot}{slots} = $slot; - # not used - #$config{slot}{indexes} = \@indexes; + $config{slot}{indexes} = \@indexes; } # print Dumper(\%config) if $debug; @@ -366,6 +430,7 @@ sub getIfDescr { my $oid_value = $args{ifIndex}; my $prefix = $args{prefix}; + my $asamModel = $args{asamModel}; if ( $args{version} eq "4.1" or $args{version} eq "4.3" ) { my $rack_mask = 0x70000000; @@ -384,6 +449,8 @@ sub getIfDescr { $slot = $slot - 2; ++$circuit; + $slot = asamSlotCorrection($slot,$asamModel); + return "$prefix-$rack-$shelf-$slot-$circuit"; } else { @@ -402,11 +469,35 @@ sub getIfDescr { ++$circuit; $prefix = "XDSL" if $level == 16; + + $slot = asamSlotCorrection($slot,$asamModel); return "$prefix-1-1-$slot-$circuit"; } } +sub asamSlotCorrection { + my $slot = shift; + my $asamModel = shift; + + if ( $asamModel =~ /7302/ and $slot >= 9 ) { + $slot = $slot + 1; + } + elsif ( $asamModel =~ /ARAM-D/ ) { + $slot = $slot + 3 + } + elsif ( $asamModel =~ /ARAM-E/ and $slot < 9 ) { + $slot = $slot + 1 + } + elsif ( $asamModel =~ /ARAM-E/ and $slot >= 9 ) { + $slot = $slot + 3 + } + elsif ( $asamModel =~ /7330-FD/ ) { + $slot = $slot + 3 + } + return $slot; +} + sub getDescription { my %args = @_; @@ -523,18 +614,22 @@ sub build_42_interface_indexes { my $level = 3; #Look at the eqptHolderPlannedType data to see what is planned for this device. + #if ( exists $NI->{eqptHolder} ) { + # $systemConfig = getRackShelfMatrix("4.2",$NI->{eqptHolder}); + #} if ( exists $NI->{eqptHolder} ) { - $systemConfig = getRackShelfMatrix("4.2",$NI->{eqptHolder}); + $systemConfig = getRackShelfMatrix("4.2",$NI->{eqptPortMapping}); } my $slot_count = $systemConfig->{slot}{slots}; # correct the slot_count #my $slot_limit = ( $slot_count * 2 ) + 2; - #my $slot_limit = $slot_count + 1; - my $slot_limit = $slot_count; - - dbg("DEBUG slot_count=$slot_count slot_limit=$slot_limit"); + my $slot_limit = $slot_count + 1; + #my $slot_limit = $slot_count; + #dbg("DEBUG slot_count=$slot_count slot_limit=$slot_limit"); + dbg("DEBUG slot_count=$slot_count slot_limit=$slot_limit indexes=@{$systemConfig->{slot}{indexes}}"); + #Slot count x 2 + 3? Or + 2 my @slots = (2..$slot_limit); @@ -551,6 +646,28 @@ sub build_42_interface_indexes { return \@interfaces; } +sub build_interface_indexes { + my %args = @_; + my $NI = $args{NI}; + my $systemConfig; + + my $level = 3; + + my @interfaces = (); + + if ( exists $NI->{ifTable} ) { + foreach my $ifIndex (oid_lex_sort(keys(%{$NI->{ifTable}}))) { + if ( $NI->{ifTable}{$ifIndex}{ifDescr} eq "atm Interface" ) { + push( @interfaces, $ifIndex ); + } + } + } + + dbg("DEBUG indexes=@interfaces"); + + return \@interfaces; +} + sub generate_interface_index_41 { my %args = @_; my $rack = $args{rack}; diff --git a/install/plugins/BTI_Devices.pm b/install/plugins/BTI_Devices.pm new file mode 100644 index 0000000..5330ca7 --- /dev/null +++ b/install/plugins/BTI_Devices.pm @@ -0,0 +1,96 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for handling various things the BTI devices need + +package BTI_Devices; +our $VERSION = "1.0.0"; + +use strict; + +use func; # for the conf table extras +use NMIS; + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + my $NI = $S->ndinfo; + + # anything to do? + return (0,undef) if ( $NI->{system}{nodeModel} !~ "BTI-7000" or !getbool($NI->{system}->{collect})); + + # anything to do? + my $changesweremade = 0; + my @sections = qw(BTI_7000_GE_Ports BTI_7000_STM_Optical BTI_7000_GE_Optical BTI_7000_FC_Optical BTI_7000_GE_Bytes); + foreach my $section (@sections) { + if (ref($NI->{$section}) eq "HASH") { + info("Working on $node $section"); + $changesweremade = 1; + + for my $index (keys %{$NI->{$section}}) + { + my $entry = $NI->{$section}{$index}; + my @parts = split(/\./,$index); + if ( @parts == 5 ) { + # dispose of the first field + shift @parts; + } + my $interface = "GE"; + if ( $section =~ /BTI_7000_(\w+)_/ ) { + $interface = $1; + } + my $shelf = $parts[0]; + my $slot = $parts[1]; + my $port = $parts[2]; + my $name = "$interface/Shelf-$shelf/Slot-$slot/Port-$port"; + $entry->{name} = $name; + + } + } + } + if (ref($NI->{BTI_7000_Slot_Inventory}) eq "HASH") { + info("Working on $node BTI_7000_Slot_Inventory"); + $changesweremade = 1; + + for my $index (keys %{$NI->{BTI_7000_Slot_Inventory}}) + { + my $entry = $NI->{BTI_7000_Slot_Inventory}{$index}; + my @parts = split(/\./,$index); + my $shelf = $parts[0]; + my $slot = $parts[2]; + my $name = "Shelf-$shelf/Slot-$slot"; + $entry->{name} = $name; + } + } + return ($changesweremade,undef); # report if we changed anything +} + +1; diff --git a/install/plugins/CiscoDSL.pm b/install/plugins/CiscoDSL.pm new file mode 100644 index 0000000..8198703 --- /dev/null +++ b/install/plugins/CiscoDSL.pm @@ -0,0 +1,133 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for converting the cdp index into interface name. + +package CiscoDSL; +our $VERSION = "1.0.1"; + +use strict; +use NMIS; # lnt +use func; # for the conf table extras +use Data::Dumper; +use Net::SNMP qw(oid_lex_sort); + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + my $LNT = loadLocalNodeTable(); # fixme required? are rack_count and shelf_count kept in the node's ndinfo section? + my $NC = $S->ndcfg; + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + my $ifTable = $NI->{ifTable}; + + my $changesweremade = 0; + + # anything to do? if the data we need isn't there, return. + return (0,undef) if ( not defined $NI->{ifStack} ); + + info("Working on $node ifStack"); + + # sample ifStack information + #"ifStack" : { + # "0.11" : { + # "index" : "0.11", + # "ifStackStatus" : "active" + # }, + # "11.12" : { + # "index" : "11.12", + # "ifStackStatus" : "active" + # }, + # "12.14" : { + # "index" : "12.14", + # "ifStackStatus" : "active" + # }, + # "13.14" : { + # "index" : "13.14", + # "ifStackStatus" : "active" + # }, + # "14.0" : { + # "index" : "14.0", + # "ifStackStatus" : "active" + # }, + + + for my $key (oid_lex_sort(keys %{$NI->{ifStack}})) + { + my $entry = $NI->{ifStack}->{$key}; + + if ( my @parts = split(/\./,$entry->{index}) ) { + my $ifStackHigherLayer = shift(@parts); + my $ifStackLowerLayer = shift(@parts); + + $entry->{ifStackHigherLayer} = $ifStackHigherLayer; + $entry->{ifStackLowerLayer} = $ifStackLowerLayer; + + if ( defined $IF->{$ifStackHigherLayer} and defined $IF->{$ifStackHigherLayer}{ifDescr} ) { + #delete $IF->{$ifStackHigherLayer}{ifStackLowerLayer}; + # in the interfaces table, add the details of what is an upper or lower interface. + if (ref($IF->{$ifStackHigherLayer}{ifStackLowerLayer}) eq "SCALAR") { + delete $IF->{$ifStackHigherLayer}{ifStackLowerLayer}; + } + push(@{$IF->{$ifStackHigherLayer}{ifStackLowerLayer}},$ifStackLowerLayer); + + # create a linkage in the GUI so people can see the relationships + $entry->{ifDescrHigherLayer} = $IF->{$ifStackHigherLayer}{ifDescr}; + $entry->{ifDescrHigherLayer_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$ifStackHigherLayer&node=$node"; + $entry->{ifDescrHigherLayer_id} = "node_view_$node"; + } + + if ( defined $IF->{$ifStackLowerLayer} and defined $IF->{$ifStackLowerLayer}{ifDescr} ) { + #delete $IF->{$ifStackLowerLayer}{ifStackHigherLayer}; + # in the interfaces table, add the details of what is an upper or lower interface. + if (ref($IF->{$ifStackLowerLayer}{ifStackHigherLayer}) eq "SCALAR") { + delete $IF->{$ifStackLowerLayer}{ifStackHigherLayer}; + } + push(@{$IF->{$ifStackLowerLayer}{ifStackHigherLayer}},$ifStackHigherLayer); + + # create a linkage in the GUI so people can see the relationships + $entry->{ifDescrLowerLayer} = $IF->{$ifStackLowerLayer}{ifDescr}; + $entry->{ifDescrLowerLayer_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$ifStackLowerLayer&node=$node"; + $entry->{ifDescrLowerLayer_id} = "node_view_$node"; + } + + dbg("WHAT: ifDescr=$IF->{$ifStackHigherLayer}{ifDescr} ifStackHigherLayer=$entry->{ifStackHigherLayer} ifStackLowerLayer=$entry->{ifStackLowerLayer} "); + + $changesweremade = 1; + } + } + + return ($changesweremade,undef); # report if we changed anything +} + + + +1; diff --git a/install/plugins/EricssonPPX.pm b/install/plugins/EricssonPPX.pm new file mode 100644 index 0000000..341ceb3 --- /dev/null +++ b/install/plugins/EricssonPPX.pm @@ -0,0 +1,156 @@ +# a small update plugin for discovering interfaces on FutureSoftware devices +# which requires custom snmp accesses +package EricssonPPX; +our $VERSION = "1.0.1"; + +use strict; + +use NMIS; # lnt +use func; # for the conf table extras +use rrdfunc; # for updateRRD +# Customer not running latest code, can not use this +#use snmp 1.1.0; # for snmp-related access +use Net::SNMP qw(oid_lex_sort); +use Data::Dumper; + +sub collect_plugin +{ + my (%args) = @_; + my ($node, $S, $C) = @args{qw(node sys config)}; + + my $NC = $S->ndcfg; + my $NI = $S->ndinfo; + + # this plugin deals only with things containing the right data ppxCardMEM + if ( not defined $NI->{ppxCardMEM} ) { + info("Prerequisite ppxCardMEM not found in $node"); + return (0,undef) + } + + info("Working on $node ppxCardMEM"); + + # Get the SNMP Session going. + #my $snmp = snmp->new(name => $node); + + my ($session, $error) = Net::SNMP->session( + -hostname => $NC->{node}{host}, + -port => $NC->{node}{port}, + -version => $NC->{node}{version}, + -community => $NC->{node}{community}, # v1/v2c + ); + + #return (2,"Could not open SNMP session to node $node: ".$snmp->error) + # if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})); + #return (2, "Could not retrieve SNMP vars from node $node: ".$snmp->error) + # if (!$snmp->testsession); + + my $changesweremade = 0; + + if ( $session ) { + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryCapacityValue.present.0.fastRam = Gauge32: 0 + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryCapacityValue.present.0.normalRam = Gauge32: 65536 + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryCapacityValue.present.0.sharedRam = Gauge32: 2048 + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryUsageValue.present.0.fastRam = Gauge32: 0 + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryUsageValue.present.0.normalRam = Gauge32: 37316 + #Nortel-MsCarrier-MscPassport-BaseShelfMIB::mscShelfCardMemoryUsageValue.present.0.sharedRam = Gauge32: 2048 + + #"mscShelfCardMemoryCapacityValue" "1.3.6.1.4.1.562.36.2.1.13.2.244.1.2" + #"mscShelfCardMemoryUsageValue" "1.3.6.1.4.1.562.36.2.1.13.2.245.1.2" + #"mscShelfCardMemoryUsageAvgValue" "1.3.6.1.4.1.562.36.2.1.13.2.276.1.2" + #"mscShelfCardMemoryUsageAvgMinValue" "1.3.6.1.4.1.562.36.2.1.13.2.277.1.2" + #"mscShelfCardMemoryUsageAvgMaxValue" "1.3.6.1.4.1.562.36.2.1.13.2.278.1.2" + + my $memCapacityOid = ".1.3.6.1.4.1.562.36.2.1.13.2.244.1.2"; + my $memUsageOid = ".1.3.6.1.4.1.562.36.2.1.13.2.245.1.2"; + my $memUsageAvgOid = ".1.3.6.1.4.1.562.36.2.1.13.2.276.1.2"; + my $memUsageMinOid = ".1.3.6.1.4.1.562.36.2.1.13.2.277.1.2"; + my $memUsageMaxOid = ".1.3.6.1.4.1.562.36.2.1.13.2.278.1.2"; + + my $fastRam = "0"; + my $normalRam = "1"; + my $sharedRam = "2"; + + # based on each of the cards we know about from CPU, we are going to look for each of the memory value. + foreach my $card (sort keys %{$NI->{ppxCardMEM}}) { + info("ppxCardMEM card $card"); + + my $snmpdata = $session->get_request( + -varbindlist => [ + "$memCapacityOid.$card.$fastRam", + "$memCapacityOid.$card.$normalRam", + "$memCapacityOid.$card.$sharedRam", + "$memUsageOid.$card.$fastRam", + "$memUsageOid.$card.$normalRam", + "$memUsageOid.$card.$sharedRam", + + "$memUsageAvgOid.$card.$fastRam", + "$memUsageAvgOid.$card.$normalRam", + "$memUsageAvgOid.$card.$sharedRam", + "$memUsageMinOid.$card.$fastRam", + "$memUsageMinOid.$card.$normalRam", + "$memUsageMinOid.$card.$sharedRam", + "$memUsageMaxOid.$card.$fastRam", + "$memUsageMaxOid.$card.$normalRam", + "$memUsageMaxOid.$card.$sharedRam", + ], + ); + + if ( $snmpdata ) { + #print Dumper $snmpdata; + my $data = { + 'memCapFastRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memCapacityOid.$card.$fastRam"} }, + 'memCapNormalRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memCapacityOid.$card.$normalRam"} }, + 'memCapSharedRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memCapacityOid.$card.$sharedRam"} }, + + 'memUsageFastRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageOid.$card.$fastRam"} }, + 'memUsageNormalRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageOid.$card.$normalRam"} }, + 'memUsageSharedRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageOid.$card.$sharedRam"} }, + + 'memAvgFastRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageAvgOid.$card.$fastRam"} }, + 'memAvgNormalRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageAvgOid.$card.$normalRam"} }, + 'memAvgSharedRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageAvgOid.$card.$sharedRam"} }, + + 'memMinFastRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMinOid.$card.$fastRam"} }, + 'memMinNormalRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMinOid.$card.$normalRam"} }, + 'memMinSharedRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMinOid.$card.$sharedRam"} }, + + 'memMaxFastRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMaxOid.$card.$fastRam"} }, + 'memMaxNormalRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMaxOid.$card.$normalRam"} }, + 'memMaxSharedRam' => { "option" => "GAUGE,0:U", "value" => $snmpdata->{"$memUsageMaxOid.$card.$sharedRam"} }, + }; + + # save the results to the node file. + $NI->{ppxCardMEM}{$card}{'memCapFastRam'} = $snmpdata->{"$memCapacityOid.$card.$fastRam"}; + $NI->{ppxCardMEM}{$card}{'memCapNormalRam'} = $snmpdata->{"$memCapacityOid.$card.$normalRam"}; + $NI->{ppxCardMEM}{$card}{'memCapSharedRam'} = $snmpdata->{"$memCapacityOid.$card.$sharedRam"}; + + if ( $snmpdata->{"$memCapacityOid.$card.$fastRam"} == 0 + and $snmpdata->{"$memCapacityOid.$card.$normalRam"} == 0 + and $snmpdata->{"$memCapacityOid.$card.$sharedRam"} == 0 + ) { + info ("Card has no memory information, removing from display"); + delete $NI->{ppxCardMEM}{$card}; + } + + my $filename = updateRRD(data=>$data, sys=>$S, type=>"ppxCardMEM", index => $card); + if (!$filename) + { + return (2, "UpdateRRD failed!"); + } + } + else { + info ("Problem with SNMP session to $node: ".$session->error()); + + } + } + + $changesweremade = 1; + return ($changesweremade,undef); # report if we changed anything + + } + else { + info ("Could not open SNMP session to node $node: ".$error); + return (2, "Could not open SNMP session to node $node: ".$error) + } + +} diff --git a/install/plugins/TestPlugin.pm b/install/plugins/TestPlugin.pm index 78a5686..82c96d8 100644 --- a/install/plugins/TestPlugin.pm +++ b/install/plugins/TestPlugin.pm @@ -12,7 +12,8 @@ sub after_update_plugin my ($nodes, $S, $C) = @args{qw(nodes sys config)}; logMsg("The test plugin was run in the after_update phase"); - logMsg("Nodes that this update handled: ".join(", ",@$nodes)) if (ref($nodes) eq "ARRAY"); + # this was hanging on the telmex servers + #logMsg("Nodes that this update handled: ".join(", ",@$nodes)) if (ref($nodes) eq "ARRAY"); return (0,undef); } @@ -24,7 +25,8 @@ sub after_collect_plugin logMsg("The test plugin was run in the after_collect phase"); - logMsg("Nodes that this collect handled: ".join(", ",@$nodes)) if (ref($nodes) eq "ARRAY"); + # this was hanging on the telmex servers + #logMsg("Nodes that this collect handled: ".join(", ",@$nodes)) if (ref($nodes) eq "ARRAY"); return (0,undef); } diff --git a/install/plugins/ciscoMacTable.pm b/install/plugins/ciscoMacTable.pm index e79ca0f..1ccebcf 100644 --- a/install/plugins/ciscoMacTable.pm +++ b/install/plugins/ciscoMacTable.pm @@ -30,7 +30,7 @@ # To make sense of Cisco VLAN Bridge information. package ciscoMacTable; -our $VERSION = "1.1.0"; +our $VERSION = "1.2.0"; use strict; @@ -94,9 +94,20 @@ sub update_plugin # Get the connected devices if the VLAN is operational if ( $entry->{vtpVlanState} eq "operational" ) { - my $snmp = snmp->new(name => $node); + my %nodeconfig = %{$NC->{node}}; # copy required because we modify it... + + # community string: @ + # https://www.cisco.com/c/en/us/support/docs/ip/simple-network-management-protocol-snmp/40367-camsnmp40367.html + my $magic = $nodeconfig{community}.'@'.$entry->{vtpVlanIndex}; + $nodeconfig{community} = $magic; + + $nodeconfig{host_addr} = $NI->{system}->{host_addr}; - if (!$snmp->open(config => $NC->{node}, host_addr => $NI->{system}->{host_addr})) + # nmisng::snmp doesn't fall back to global config + my $max_repetitions = $nodeconfig{max_repetitions} || $C->{snmp_max_repetitions}; + + my $snmp = snmp->new(name => $node); + if (!$snmp->open(config => \%nodeconfig )) { logMsg("Could not open SNMP session to node $node: ".$snmp->error); } diff --git a/install/plugins/hwMusaBoard.pm b/install/plugins/hwMusaBoard.pm new file mode 100644 index 0000000..51f2819 --- /dev/null +++ b/install/plugins/hwMusaBoard.pm @@ -0,0 +1,76 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for converting the cdp index into interface name. + +package hwMusaBoard; +our $VERSION = "1.0.0"; + +use strict; + +use func; # for the conf table extras +use NMIS; + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + # anything to do? + + my $hwMusaBoard = 0; + + $hwMusaBoard = 1 if (ref($NI->{hwMusaBoard}) eq "HASH"); + + return (0,undef) if not $hwMusaBoard; + + my $changesweremade = 0; + + info("Working on $node hwMusaBoard"); + + #BoardFrameSlot + for my $key (keys %{$NI->{hwMusaBoard}}) + { + # lets get the VRF Name first. + my $entry = $NI->{hwMusaBoard}->{$key}; + + if ( $entry->{index} =~ /(\d+)\.(.+)$/ ) { + my $frame = $1; + my $slot = $2; + $entry->{BoardFrameSlot} = "$frame/$slot"; + $changesweremade = 1; + } + } + + return ($changesweremade,undef); # report if we changed anything +} + +1; diff --git a/install/plugins/jnxCoStable.pm b/install/plugins/jnxCoStable.pm new file mode 100644 index 0000000..a2c468a --- /dev/null +++ b/install/plugins/jnxCoStable.pm @@ -0,0 +1,89 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for converting the cdp index into interface name. + +package jnxCoStable; +our $VERSION = "1.0.0"; + +use strict; + +use func; # for the conf table extras +use NMIS; + +use Data::Dumper; + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + # anything to do? + + my $IFD = $S->ifDescrInfo(); # interface info indexed by ifDescr + + return (0,undef) if (ref($NI->{Juniper_CoS}) ne "HASH"); + my $changesweremade = 0; + + + info("Working on $node jnxCoStable"); + + + + + for my $key (keys %{$NI->{Juniper_CoS}}) + { + my $entry = $NI->{Juniper_CoS}->{$key}; + + if ( $entry->{index} =~ /(\d+)\.\d+\.(.+)$/ ) { + + $changesweremade = 1; + + my $intIndex = $1; + my $FCcodename = $2; + my $FCname = join("", map { chr($_) } split(/\./,$FCcodename)); + $entry->{jnxCosFcName} = $FCname . ' Class' ; + + + $entry->{ifIndex} = $intIndex; + $entry->{IntName} = $IF->{$entry->{ifIndex}}{ifDescr}; + $entry->{IntName_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$entry->{ifIndex}&node=$node"; + $entry->{cosDescription} = $entry->{IntName} . '-' . $FCname . '-Class'; + + + info("Found COS Entry with interface $entry->{IntName} and $entry->{jnxCosFcName} "); + } + } + + return ($changesweremade,undef); # report if we changed anything +} + +1; diff --git a/install/plugins/jnxDCUtable.pm b/install/plugins/jnxDCUtable.pm new file mode 100644 index 0000000..b83c04a --- /dev/null +++ b/install/plugins/jnxDCUtable.pm @@ -0,0 +1,86 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for adding the interface name to the sytem health section using the index from DCU. + +package jnxDCUtable; +our $VERSION = "1.1.0"; + +use strict; + +use func; # required for logMsg + +use NMIS; + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + + + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + + my $IFD = $S->ifDescrInfo(); # interface info indexed by ifDescr + + return (0,undef) if (ref($NI->{jnxDestinationClassUsage}) ne "HASH"); + my $changesweremade = 0; + + info("jnxDCUtable plugin update-phase Working on $node DCU Tables"); + + + for my $key (keys %{$NI->{jnxDestinationClassUsage}}) + { + my $entry = $NI->{jnxDestinationClassUsage}{$key}; + my @parts; + + my $DCUname = $entry->{jnxDcuStatsClName}; + + $changesweremade = 1; + + @parts = split(/\./,$entry->{index}); + + $entry->{ifIndex} = shift(@parts); + + $entry->{ifDescr} = $IF->{$entry->{ifIndex}}{ifDescr}; + info("Found interface $entry->{ifDescr} with $DCUname"); + $entry->{ifDescr_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$entry->{ifIndex}&node=$node"; + + $entry->{jnxDcuStatsClName} = "$entry->{ifDescr}-$DCUname"; + + + + + } + + return ($changesweremade,undef); # report if we changed anything +} + +1; diff --git a/install/plugins/jnxSCUtable.pm b/install/plugins/jnxSCUtable.pm new file mode 100644 index 0000000..038d51d --- /dev/null +++ b/install/plugins/jnxSCUtable.pm @@ -0,0 +1,86 @@ +# +# Copyright Opmantek Limited (www.opmantek.com) +# +# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM +# +# This file is part of Network Management Information System ("NMIS"). +# +# NMIS is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# NMIS 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with NMIS (most likely in a file named LICENSE). +# If not, see +# +# For further information on NMIS or for a license other than GPL please see +# www.opmantek.com or email contact@opmantek.com +# +# User group details: +# http://support.opmantek.com/users/ +# +# ***************************************************************************** +# +# a small update plugin for adding the interface name to the sytem health section using the index from SCU. + +package jnxSCUtable; +our $VERSION = "1.1.0"; + +use strict; + +use func; # required for logMsg + +use NMIS; + +sub update_plugin +{ + my (%args) = @_; + my ($node,$S,$C) = @args{qw(node sys config)}; + + + + my $NI = $S->ndinfo; + my $IF = $S->ifinfo; + + my $IFD = $S->ifDescrInfo(); # interface info indexed by ifDescr + + return (0,undef) if (ref($NI->{jnxSourceClassUsage}) ne "HASH"); + my $changesweremade = 0; + + info("jnxSCUtable plugin update-phase Working on $node SCU Tables"); + + + for my $key (keys %{$NI->{jnxSourceClassUsage}}) + { + my $entry = $NI->{jnxSourceClassUsage}{$key}; + my @parts; + + my $SCUname = $entry->{jnxScuStatsClName}; + + + $changesweremade = 1; + + + @parts = split(/\./,$entry->{index}); + + $entry->{ifIndex} = shift(@parts); + + $entry->{ifDescr} = $IF->{$entry->{ifIndex}}{ifDescr}; + info("Found interface $entry->{ifDescr} with $SCUname"); + $entry->{ifDescr_url} = "/cgi-nmis8/network.pl?conf=$C->{conf}&act=network_interface_view&intf=$entry->{ifIndex}&node=$node"; + + + $entry->{jnxScuStatsClName} = "$entry->{ifDescr}-$SCUname"; + + } + + return ($changesweremade,undef); # report if we changed anything +} + +1; diff --git a/install/scripts/http b/install/scripts/http index 88b9e67..758a4b9 100755 --- a/install/scripts/http +++ b/install/scripts/http @@ -1,3 +1,2 @@ -send: HEAD / HTTP/1.0 -send: +send: HEAD / HTTP/1.0\r\n\r\n expect: 200 OK diff --git a/install/scripts/http-moved b/install/scripts/http-moved index bed41dd..de35df9 100755 --- a/install/scripts/http-moved +++ b/install/scripts/http-moved @@ -1,3 +1,2 @@ -send: HEAD / HTTP/1.0 -send: +send: HEAD / HTTP/1.0\r\n\r\n expect: 301 Moved Permanently diff --git a/install/scripts/http-redir b/install/scripts/http-redir index 534c58a..bb736e1 100755 --- a/install/scripts/http-redir +++ b/install/scripts/http-redir @@ -1,3 +1,2 @@ -send: HEAD / HTTP/1.0 -send: +send: HEAD / HTTP/1.0\r\n\r\n expect: 302 Redirect diff --git a/install/scripts/jira-activity-check b/install/scripts/jira-activity-check index 66f97e1..73b64c4 100755 --- a/install/scripts/jira-activity-check +++ b/install/scripts/jira-activity-check @@ -68,7 +68,7 @@ elsif ($res->is_success) print "API Access succeeded but query failed: $rdata->{errorMessages}\n"; exit 30; } - if ($rdata->{total}) + if (exists($rdata->{total})) { print "API Access and query successful.\nactivity_count=$rdata->{total}\n"; exit 100; diff --git a/install/scripts/pop3 b/install/scripts/pop3 index 20425a4..5eade43 100755 --- a/install/scripts/pop3 +++ b/install/scripts/pop3 @@ -1,2 +1,2 @@ -send: QUIT -expect: OK +send: QUIT +expect: OK diff --git a/install/snmptrapd.options b/install/snmptrapd.options index 651db10..3558a0b 100644 --- a/install/snmptrapd.options +++ b/install/snmptrapd.options @@ -1,3 +1,8 @@ -#OPTIONS="-Lsd -p /var/run/snmptrapd.pid" -#OPTIONS="-Lo -Lf /var/log/snmptrapd.log -p /var/run/snmptrapd.pid -OPTIONS="-p /var/run/snmptrapd.pid -m ALL -M /usr/local/nmis8/mibs/traps" +# this is without dns resolution, with mib resolution from NMIS' sources, +# and with logging to syslog (facility daemon) but only severities 0 to 5 +OPTIONS="-n -LS0-5d /var/run/snmptrapd.pid -m ALL -M /usr/local/nmis8/mibs/traps" + +# similar, but default log levels +#OPTIONS="-Lsd -p /var/run/snmptrapd.pid -m ALL -M /usr/local/nmis8/mibs/traps" +# similar but logging to stderror and a specific log file +#OPTIONS="-Le -Lf /var/log/snmptrapd.log -p /var/run/snmptrapd.pid -m ALL -M /usr/local/nmis8/mibs/traps" diff --git a/lib/Auth.pm b/lib/Auth.pm index f6aa6bd..ae916df 100644 --- a/lib/Auth.pm +++ b/lib/Auth.pm @@ -58,7 +58,7 @@ # password=>$Q->{auth_password},headeropts=>$headeropts) ; # package Auth; -our $VERSION = "1.2.0"; +our $VERSION = "1.3.0"; use strict; use vars qw(@ISA @EXPORT); @@ -107,8 +107,7 @@ use Crypt::PasswdMD5; # for the apache-specific md5 crypt flavour # for handling errors in javascript use JSON::XS; -# You should change this to be unique for your site -# +# You should set config's auth_web_key (or edit this source file), so that cookies are unique for your site my $CHOCOLATE_CHIP = '8fhmgBC4YSVcZMnBsWtY32KQvTE9JBeuIp1y'; my $auth_user_name_regex = qr/[\w \-\.\@\`\']+/; @@ -140,6 +139,7 @@ sub new { privlevel => 0, # default all cookie => undef, groups => undef, + all_groups_allowed => undef, }; bless $self, $class; $self->_auth_init; @@ -1449,7 +1449,7 @@ sub update_failure_counter open(F,">$userstatefile") or return "cannot write $userstatefile: $!"; print F encode_json($userdata); close(F); - setFileProtDiag(file => $userstatefile, username => $C->{nmis_user}, + setFileProtDiag(file => $userstatefile, username => $C->{nmis_user}, groupname => $C->{nmis_group}, permission => $C->{os_fileperm}); # ignore problems with that @@ -1525,7 +1525,8 @@ sub SetUser { # set default privileges to lowest level $self->{priv} = "anonymous"; $self->{privlevel} = 5; - $self->_GetPrivs($self->{user}); + delete $self->{all_groups_allowed}; # bsts, if the auth object gets reused + $self->_GetPrivs($self->{user}); # this potentially sets all_groups_allowed return 1; } else { @@ -1541,21 +1542,29 @@ sub InGroup { my $self = shift; my $group = shift; return 1 unless $self->{_require}; + # If user can see all groups, they immediately pass - if ( $self->{groups} eq "all" ) { - logAuth("InGroup: $self->{user}, all $group, 1") if $debug; + if ( $self->{all_groups_allowed} ) + { + logAuth("InGroup: $self->{user}, all group: ok for $group") + if $debug; return 1; } - return 0 unless defined $group or $group; - foreach my $g (@{$self->{groups}}) { - logAuth(" DEBUG AUTH: @{$self->{groups}} g=$g group=$group") if $debug; - if ( lc($g) eq lc($group) ) { - logAuth("InGroup: $self->{user}, $group, 1") if $debug; + return 0 if (!$group); # fixme why after the all logic? + + foreach my $g (@{$self->{groups}}) + { + if (lc($g) eq lc($group)) + { + logAuth("InGroup: $self->{user}, ok for $group") if $debug; return 1; } - } - logAuth("InGroup: $self->{user}, $group, 0") if $debug; + + logAuth("InGroup: $self->{user}, groups: " + .join(",", @{$self->{groups}}) + .", NOT ok for $group") + if $debug; return 0; } @@ -1624,28 +1633,37 @@ sub _GetPrivs { } logAuth("INFO User \"$user\" has priv=$self->{priv} and privlevel=$self->{privlevel}") if $debug; -# dbg("USER groups \n".Dumper($C->{group_list}) ); - - my @groups = split /,/, $UT->{$user}{groups}; - if ( not @groups and $C->{auth_default_groups} ne "" ) { - @groups = split /,/, $C->{auth_default_groups}; - my $ext = getExtension(dir=>'conf'); - logAuth("INFO Groups not found for User \"$user\" using groups configured in Config.$ext -> auth_default_groups"); + # groups come from the user sertting or the auth_default_groups + my $grouplistraw = $UT->{$user}{groups}; + if (!$grouplistraw && $C->{auth_default_groups}) + { + $grouplistraw = $C->{auth_default_groups}; + logAuth("INFO Groups not found for User \"$user\", using auth_default_groups from configuration"); } - if ( grep { $_ eq 'all' } @groups) { - @{$self->{groups}} = sort split(',',$C->{group_list}); - # put the virtual network group on the list - push @{$self->{groups}}, "network"; - } elsif ( $UT->{$user}{groups} eq "none" or $UT->{$user}{groups} eq "" ) { - @{$self->{groups}} = []; - } else { - # note: the main health status graphs uses the implied virtual group network, - # this group must be explicitly stated if you want to see this graph - @{$self->{groups}} = @groups; - } - map { stripSpaces($_) } @{$self->{groups}}; + # leading/trailing space is gone after stripspaces, rest after split + my @groups = sort(split /\s*,\s*/, stripSpaces($grouplistraw)); + # note: the main health status graphs uses the implied virtual group network, + # this group must be explicitly stated if you want to see this graph + push @groups, "network"; + # is the user authorised for all (known and unknown) groups? then record that + if ( grep { $_ eq 'all' } @groups) + { + $self->{all_groups_allowed} = 1; + $self->{groups} = \@groups; + } + elsif ($UT->{$user}{groups} eq "none" + or !$grouplistraw) + { + $self->{groups} = []; + delete $self->{all_groups_allowed}; + } + else + { + $self->{groups} = \@groups; + delete $self->{all_groups_allowed}; + } return 1; } diff --git a/lib/NMIS.pm b/lib/NMIS.pm index 12b9310..abb0122 100644 --- a/lib/NMIS.pm +++ b/lib/NMIS.pm @@ -27,17 +27,17 @@ # # ***************************************************************************** package NMIS; -our $VERSION = "8.6.1G"; +our $VERSION = "8.6.2G"; use NMIS::uselib; use lib "$NMIS::uselib::rrdtool_lib"; use strict; use RRDs; -use Time::ParseDate; use Time::Local; +use DateTime; use Net::hostent; -use Socket; +use Socket 2.001; # for getnameinfo() used by resolveDNStoAddr use func; use csv; use notify; @@ -47,7 +47,11 @@ use DBfunc; use URI::Escape; use JSON::XS 2.01; use File::Basename; +use File::Copy; +use Clone; +use List::Util 1.33; use CGI qw(); # very ugly but createhrbuttons needs it :( +use NMIS::UUID; #! Imports the LOCK_ *constants (eg. LOCK_UN, LOCK_EX) use Fcntl qw(:DEFAULT :flock); @@ -90,7 +94,6 @@ use Exporter; loadNodeConfTable has_nodeconf get_nodeconf update_nodeconf - loadOutageTable loadInterfaceTypes loadCfgTable findCfgEntry @@ -106,6 +109,7 @@ use Exporter; checkEvent notify + outageCheck nodeStatus PreciseNodeStatus @@ -134,9 +138,7 @@ use Exporter; convertConfFiles statusNumber logMessage - outageCheck - outageRemove - sendTrap + eventToSMTPPri dutyTime resolveDNStoAddr @@ -376,20 +378,24 @@ sub checkNodeName { #================================================================== -# this small helper takes an optional section and a require config item name, -# and returns the structure info for that item from loadCfgTable +# this small helper looks for a display/validation rule entry in a table-xyz datastructure +# and returns the structure info for that item +# args: item name (required), +# section (optional, if not given all sections are trawled) +# table (optional live structure, if not given the Config table is loaded) +# # returns: hashref (keys display, value etc.) or undef if not found sub findCfgEntry { my (%args) = @_; - my ($section,$item) = @args{qw(section item)}; + my ($section,$item, $meta) = @args{qw(section item table)}; - my $meta = loadCfgTable(); - for my $maybesection (defined $section? ($section) : keys %$meta) + $meta ||= loadCfgTable(); + for my $maybesection (defined $section? ($section) : sort keys %$meta) { for my $entry (@{$meta->{$maybesection}}) { - if ($entry->{$item}) + if (exists($entry->{$item})) { return $entry->{$item}; } @@ -398,290 +404,36 @@ sub findCfgEntry return undef; } -# this returns an almost config-like structure that describes the well-known config keys, -# how to display them and what options they have -# args: none! -sub loadCfgTable { +# this loads a Table- config structure (for the gui) +# and returns the substructure - outermost is always hash, +# substructure is usually an array (except for Table-Config, which is one level deeper) +# +# args: table name (e.g. Nodes), defaults to "Config", +# user (optional, if given will be set in %ENV for any dynamic tables that need it) +# +# returns: (array or hash)ref or undef on error +sub loadCfgTable +{ my %args = @_; - my $table = $args{table}; # fixme ignored, has no function - - my %Cfg = ( - 'online' => [ - { 'nmis_docs_online' => { display => 'text', value => ['https://community.opmantek.com/']}}, - ], - - 'modules' => [ - { 'display_opmaps_widget' => { display => 'popup', value => ["true", "false"]}}, - ], - - 'directories' => [ - { '' => { display => 'text', value => ['/usr/local/nmis']}}, - { '' => { display => 'text', value => ['/bin']}}, - { '' => { display => 'text', value => ['/cgi-bin']}}, - { '' => { display => 'text', value => ['/conf']}}, - { '' => { display => 'text', value => ['']}}, - { '' => { display => 'text', value => ['/logs']}}, - { '' => { display => 'text', value => ['/menu']}}, - { '' => { display => 'text', value => ['/models']}}, - { '' => { display => 'text', value => ['/var']}}, - { '' => { display => 'text', value => ['/menu']}}, - { 'database_root' => { display => 'text', value => ['/database']}}, - { 'log_root' => { display => 'text', value => ['']}}, - { 'mib_root' => { display => 'text', value => ['/mibs']}}, - { 'report_root' => { display => 'text', value => ['/htdocs/reports']}}, - { 'script_root' => { display => 'text', value => ['/scripts']}}, - { 'web_root' => { display => 'text', value => ['/htdocs']}} - ], - - 'system' => [ - { 'group_list' => { display => 'text', value => ['']}}, - { 'roletype_list' => { display => 'text', value => ['']}}, - { 'nettype_list' => { display => 'text', value => ['']}}, - { 'nodetype_list' => { display => 'text', value => ['']}}, - { 'nmis_host' => { display => 'text', value => ['localhost']}}, - { 'domain_name' => { display => 'text', value => ['']}}, - { 'cache_summary_tables' => { display => 'popup', value => ["true", "false"]}}, - { 'cache_var_tables' => { display => 'popup', value => ["true", "false"]}}, - { 'page_refresh_time' => { display => 'text', value => ['60']}}, - { 'os_posix' => { display => 'popup', value => ["true", "false"]}}, - { 'os_cmd_read_file_reverse' => { display => 'text', value => ['tac']}}, - { 'os_cmd_file_decompress' => { display => 'text', value => ['gzip -d -c']}}, - { 'os_kernelname' => { display => 'text', value => ['']}}, - { 'os_fileperm' => { display => 'text', value => ['0775']}}, - { 'report_files_max' => { display => 'text', value => ['60']}}, - { 'loc_sysLoc_format' => { display => 'text', value => ['']}}, - { 'loc_from_DNSloc' => { display => 'popup', value => ["true", "false"]}}, - { 'loc_from_sysLoc' => { display => 'popup', value => ["true", "false"]}}, - { 'cbqos_cm_collect_all' => { display => 'popup', value => ["true", "false"]}}, - { 'buttons_in_logs' => { display => 'popup', value => ["true", "false"]}}, - { 'node_button_in_logs' => { display => 'popup', value => ["true", "false"]}}, - { 'page_bg_color_full' => { display => 'popup', value => ["true", "false"]}}, - { 'http_req_timeout' => { display => 'text', value => ['30']}}, - { 'ping_timeout' => { display => 'text', value => ['500']}}, - { 'server_name' => { display => 'text', value => ['localhost']}}, - { 'response_time_threshold' => { display => 'text', value => ['3']}}, - { 'nmis_user' => { display => 'text', value => ['nmis']}}, - { 'nmis_group' => { display => 'text', value => ['nmis']}}, - { 'fastping_timeout' => { display => 'text', value => ['300']}}, - { 'fastping_packet' => { display => 'text', value => ['56']}}, - { 'fastping_retries' => { display => 'text', value => ['3']}}, - { 'fastping_count' => { display => 'text', value => ['3']}}, - { 'fastping_sleep' => { display => 'text', value => ['60']}}, - { 'fastping_node_poll' => { display => 'text', value => ['300']}}, - { 'ipsla_collect_time' => { display => 'text', value => ['60']}}, - { 'ipsla_bucket_interval' => { display => 'text', value => ['180']}}, - { 'ipsla_extra_buckets' => { display => 'text', value => ['5']}}, - { 'ipsla_mthread' => { display => 'popup', value => ["true", "false"]}}, - { 'ipsla_maxthreads' => { display => 'text', value => ['10']}}, - { 'ipsla_mthreaddebug' => { display => 'popup', value => ["false", "true"]}}, - { 'ipsla_dnscachetime' => { display => 'text', value => ['3600']}}, - { 'ipsla_control_enable_other' => { display => 'popup', value => ["true", "false"]}}, - { 'fastping_timeout' => { display => 'text', value => ['300']}}, - { 'fastping_packet' => { display => 'text', value => ['56']}}, - { 'fastping_retries' => { display => 'text', value => ['3']}}, - { 'fastping_count' => { display => 'text', value => ['3']}}, - { 'fastping_sleep' => { display => 'text', value => ['60']}}, - { 'fastping_node_poll' => { display => 'text', value => ['300']}}, - { 'default_graphtype' => { display => 'text', value => ['abits']}}, - { 'ping_timeout' => { display => 'text', value => ['300']}}, - { 'ping_packet' => { display => 'text', value => ['56']}}, - { 'ping_retries' => { display => 'text', value => ['3']}}, - { 'ping_count' => { display => 'text', value => ['3']}}, - { 'global_collect' => { display => 'popup', value => ["true", "false"]}}, - { 'wrap_node_names' => { display => 'popup', value => ["false", "true"]}}, - { 'nmis_summary_poll_cycle' => { display => 'popup', value => ["true", "false"]}}, - { 'snpp_server' => { display => 'text', value => ['']}}, - { 'snmp_timeout' => { display => 'text', value => ['5']}}, - { 'snmp_retries' => { display => 'text', value => ['1']}}, - { 'snmp_stop_polling_on_error' => { display => 'popup', value => ["false", "true"]}}, - ], - - 'url' => [ - { '' => { display => 'text', value => ['/nmis8']}}, - { '' => { display => 'text', value => ['/cgi-nmis8']}}, - { '' => { display => 'text', value => ['/menu8']}}, - { 'web_report_root' => { display => 'text', value => ['/reports']}} - - ], - - 'tools' => [ - { 'view_ping' => { display => 'popup', value => ["true", "false"]}}, - { 'view_trace' => { display => 'popup', value => ["true", "false"]}}, - { 'view_telnet' => { display => 'popup', value => ["true", "false"]}}, - { 'view_mtr' => { display => 'popup', value => ["true", "false"]}}, - { 'view_lft' => { display => 'popup', value => ["true", "false"]}} - ], - - 'files' => [ - { 'styles' => { display => 'text', value => ['/nmis.css']}}, - { 'syslog_log' => { display => 'text', value => ['/cisco.log']}}, - { 'event_log' => { display => 'text', value => ['/event.log']}}, - { 'outage_log' => { display => 'text', value => ['/outage.log']}}, - { 'help_file' => { display => 'text', value => ['/help.pod.html']}}, - { 'nmis' => { display => 'text', value => ['/nmiscgi.pl']}}, - { 'nmis_log' => { display => 'text', value => ['/nmis.log']}} - ], - - 'email' => [ - { 'mail_server' => { display => 'text', value => ['mail.domain.com']}}, - { 'mail_domain' => { display => 'text', value => ['domain.com']}}, - { 'mail_from' => { display => 'text', value => ['nmis@domain.com']}}, - { 'mail_combine' => { display => 'popup', value => ['true','false']}}, - { 'mail_from' => { display => "text", value => ['nmis@yourdomain.com']}}, - { 'mail_use_tls' => { display => 'popup', value => ['true','false']}}, - { 'mail_server_port' => { display => "text", value => ['25']}}, - { 'mail_server_ipproto' => { display => "popup", value => ['','ipv4','ipv6']}}, - { 'mail_user' => { display => "text", value => ['your mail username']}}, - { 'mail_password' => { display => "text", value => ['']}}, - ], - - 'menu' => [ - { 'menu_title' => { display => 'text', value => ['NMIS']}}, - { 'menu_types_active' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_types_full' => { display => 'popup', value => ["true", "false", "defer"]}}, - { 'menu_types_foldout' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_groups_active' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_groups_full' => { display => 'popup', value => ["true", "false", "defer"]}}, - { 'menu_groups_foldout' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_vendors_active' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_vendors_full' => { display => 'popup', value => ["true", "false", "defer"]}}, - { 'menu_vendors_foldout' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_maxitems' => { display => 'text', value => ['30']}}, - { 'menu_suspend_link' => { display => 'popup', value => ["true", "false"]}}, - { 'menu_start_page_id' => { display => 'text', value => ['']}} - ], - - 'icons' => [ - { 'normal_net_icon' => { display => 'text', value => ['/img/network-green.gif']}}, - { 'arrow_down_green' => { display => 'text', value => ['/img/arrow_down_green.gif']}}, - { 'arrow_up_big' => { display => 'text', value => ['/img/bigup.gif']}}, - { 'logs_icon' => { display => 'text', value => ['/img/logs.jpg']}}, - { 'mtr_icon' => { display => 'text', value => ['/img/mtr.jpg']}}, - { 'arrow_up' => { display => 'text', value => ['/img/arrow_up.gif']}}, - { 'help_icon' => { display => 'text', value => ['/img/help.jpg']}}, - { 'telnet_icon' => { display => 'text', value => ['/img/telnet.jpg']}}, - { 'back_icon' => { display => 'text', value => ['/img/back.jpg']}}, - { 'lft_icon' => { display => 'text', value => ['/img/lft.jpg']}}, - { 'fatal_net_icon' => { display => 'text', value => ['/img/network-red.gif']}}, - { 'trace_icon' => { display => 'text', value => ['/img/trace.jpg']}}, - { 'nmis_icon' => { display => 'text', value => ['/img/nmis.jpg']}}, - { 'summary_icon' => { display => 'text', value => ['/img/summary.jpg']}}, - { 'banner_image' => { display => 'text', value => ['/img/NMIS_Logo.gif']}}, - { 'map_icon' => { display => 'text', value => ['/img/australia-line.gif']}}, - { 'minor_net_icon' => { display => 'text', value => ['/img/network-yellow.gif']}}, - { 'arrow_down_big' => { display => 'text', value => ['/img/bigdown.gif']}}, - { 'ping_icon' => { display => 'text', value => ['/img/ping.jpg']}}, - { 'unknown_net_icon' => { display => 'text', value => ['/img/network-white.gif']}}, - { 'doc_icon' => { display => 'text', value => ['/img/doc.jpg']}}, - { 'arrow_down' => { display => 'text', value => ['/img/arrow_down.gif']}}, - { 'arrow_up_red' => { display => 'text', value => ['/img/arrow_up_red.gif']}}, - { 'major_net_icon' => { display => 'text', value => ['/img/network-amber.gif']}}, - { 'critical_net_icon' => { display => 'text', value => ['/img/network-red.gif']}} - ], - - 'authentication' => [ - { 'auth_method_1' => { display => 'popup', value => ['apache','htpasswd','radius','tacacs','ldap','ldaps','ms-ldap']}}, - { 'auth_method_2' => { display => 'popup', value => ['apache','htpasswd','radius','tacacs','ldap','ldaps','ms-ldap']}}, - { 'auth_expire' => { display => 'text', value => ['+20min']}}, - { 'auth_htpasswd_encrypt' => { display => 'popup', value => ['crypt','md5','plaintext']}}, - { 'auth_htpasswd_file' => { display => 'text', value => ['/users.dat']}}, - { 'auth_ldap_server' => { display => 'text', value => ['']}}, - { 'auth_ldaps_server' => { display => 'text', value => ['']}}, - { 'auth_ldap_attr' => { display => 'text', value => ['']}}, - { 'auth_ldap_context' => { display => 'text', value => ['']}}, - { 'auth_ms_ldap_server' => { display => 'text', value => ['']}}, - { 'auth_ms_ldap_dn_acc' => { display => 'text', value => ['']}}, - { 'auth_ms_ldap_dn_psw' => { display => 'text', value => ['']}}, - { 'auth_ms_ldap_base' => { display => 'text', value => ['']}}, - { 'auth_ms_ldap_attr' => { display => 'text', value => ['']}}, - { 'auth_radius_server' => { display => 'text', value => ['']}}, - { 'auth_radius_secret' => { display => 'text', value => ['secret']}}, - { 'auth_tacacs_server' => { display => 'text', value => ['']}}, - { 'auth_tacacs_secret' => { display => 'text', value => ['secret']}}, - { 'auth_web_key' => { display => 'text', value => ['thisismysecretkey']}} - ], - - 'escalation' => [ - { 'escalate0' => { display => 'text', value => ['300']}}, - { 'escalate1' => { display => 'text', value => ['900']}}, - { 'escalate2' => { display => 'text', value => ['1800']}}, - { 'escalate3' => { display => 'text', value => ['2400']}}, - { 'escalate4' => { display => 'text', value => ['3000']}}, - { 'escalate5' => { display => 'text', value => ['3600']}}, - { 'escalate6' => { display => 'text', value => ['7200']}}, - { 'escalate7' => { display => 'text', value => ['10800']}}, - { 'escalate8' => { display => 'text', value => ['21600']}}, - { 'escalate9' => { display => 'text', value => ['43200']}}, - { 'escalate10' => { display => 'text', value => ['86400']}} - ], - - 'daemons' => [ - { 'daemon_ipsla_active' => { display => 'popup', value => ['true','false']}}, - { 'daemon_ipsla_filename' => { display => 'text', value => ['ipslad.pl']}}, - { 'daemon_fping_active' => { display => 'popup', value => ['true','false']}}, - { 'daemon_fping_filename' => { display => 'text', value => ['fpingd.pl']}} - ], - - 'metrics' => [ - { 'weight_availability' => { display => 'text', value => ['0.1']}}, - { 'weight_int' => { display => 'text', value => ['0.2']}}, - { 'weight_mem' => { display => 'text', value => ['0.1']}}, - { 'weight_cpu' => { display => 'text', value => ['0.1']}}, - { 'weight_reachability' => { display => 'text', value => ['0.3']}}, - { 'weight_response' => { display => 'text', value => ['0.2']}}, - { 'metric_health' => { display => 'text', value => ['0.4']}}, - { 'metric_availability' => { display => 'text', value => ['0.2']}}, - { 'metric_reachability' => { display => 'text', value => ['0.4']}} - ], - - 'graph' => [ - { 'graph_amount' => { display => 'text', value => ['48']}}, - { 'graph_unit' => { display => 'text', value => ['hours']}}, - { 'graph_factor' => { display => 'text', value => ['2']}}, - { 'graph_width' => { display => 'text', value => ['700']}}, - { 'graph_height' => { display => 'text', value => ['250']}}, - { 'graph_split' => { display => 'popup', value => ['true','false']}}, - { 'win_width' => { display => 'text', value => ['835']}}, - { 'win_height' => { display => 'text', value => ['570']}} - ], - - 'tables NMIS4' => [ - { 'Interface_Table' => { display => 'text', value => ['']}}, - { 'Interface_Key' => { display => 'text', value => ['']}}, - { 'Escalation_Table' => { display => 'text', value => ['']}}, - { 'Escalation_Key' => { display => 'text', value => ['']}}, - { 'Locations_Table' => { display => 'text', value => ['']}}, - { 'Locations_Key' => { display => 'text', value => ['']}}, - { 'Nodes_Table' => { display => 'text', value => ['']}}, - { 'Nodes_Key' => { display => 'text', value => ['']}}, - { 'Users_Table' => { display => 'text', value => ['']}}, - { 'Users_Key' => { display => 'text', value => ['']}}, - { 'Contacts_Table' => { display => 'text', value => ['']}}, - { 'Contacts_Key' => { display => 'text', value => ['']}} - ], - - 'mibs' => [ - { 'full_mib' => { display => 'text', value => ['nmis_mibs.oid']}} - ], - - 'database' => [ - { 'db_events_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_nodes_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_users_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_locations_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_contacts_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_privmap_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_escalations_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_services_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_iftypes_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_access_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_logs_sql' => { display => 'popup', value => ['true','false']}}, - { 'db_links_sql' => { display => 'popup', value => ['true','false']}} - ] - ); + my $tablename = $args{table} || "Config"; - return \%Cfg; + # some tables contain complex code, call auth methods etc, + # and need to know who the originator is + my $oldcontext = $ENV{"NMIS_USER"}; + if (my $usercontext = $args{user}) + { + $ENV{"NMIS_USER"} = $usercontext; + } + my $goodies = loadGenericTable("Table-$tablename"); + $ENV{"NMIS_USER"} = $oldcontext; # let's not leave a mess behind. + + if (ref($goodies) ne "HASH" or !keys %$goodies) + { + logMsg("ERROR, failed to load Table-$tablename"); + return undef; + } + return $goodies->{$tablename}; } sub loadRMENodes { @@ -759,6 +511,10 @@ sub loadNodeSummary { my $SUM; my $nodesum = "nmis-nodesum"; + + # logically if I am a standalone server or I am a master I can use this. + my $master_server_priority = $C->{master_server_priority} || 1; + # I should now have an up to date file, if I don't log a message if (existFile(dir=>'var',name=>$nodesum) ) { dbg("Loading $nodesum"); @@ -768,6 +524,9 @@ sub loadNodeSummary { for (keys %{$NS->{$node}}) { $SUM->{$node}{$_} = $NS->{$node}{$_}; } + if ( not defined $SUM->{$node}{server_priority} ) { + $SUM->{$node}{server_priority} = $master_server_priority; + } } } } @@ -780,6 +539,8 @@ sub loadNodeSummary { ## don't process server localhost for opHA2 next if $srv eq "localhost"; + my $server_priority = $ST->{$srv}{server_priority} || 5; + my $slavenodesum = "nmis-$srv-nodesum"; dbg("Processing Slave $srv for $slavenodesum"); # I should now have an up to date file, if I don't log a message @@ -787,8 +548,18 @@ sub loadNodeSummary { my $NS = loadTable(dir=>'var',name=>$slavenodesum); for my $node (keys %{$NS}) { if ( $group eq "" or $group eq $NS->{$node}{group} ) { - for (keys %{$NS->{$node}}) { - $SUM->{$node}{$_} = $NS->{$node}{$_}; + if ( not exists $SUM->{$node} + or $SUM->{$node}{server} eq $srv + or ( exists $SUM->{$node} + and $SUM->{$node}{server_priority} + and $SUM->{$node}{server_priority} < $server_priority + ) + ) { + for (keys %{$NS->{$node}}) { + $SUM->{$node}{$_} = $NS->{$node}{$_}; + } + $SUM->{$node}{server_priority} = $server_priority; + $SUM->{$node}{server} = $srv; } } } @@ -798,9 +569,6 @@ sub loadNodeSummary { return $SUM; } - - - # this is the most official reporter of node status, and should be # used instead of just looking at local system info nodedown # @@ -977,7 +745,7 @@ sub getLevelLogEvent { else { $mdl_level = 'Major'; # not found, use default - logMsg("node=$NI->{system}{name}, event=$event, role=$role not found in class=event of model=$NI->{system}{nodeModel}"); + dbg("no custom event level found for node=$NI->{system}{name}, event=$event, role=$role, model=$NI->{system}{nodeModel}, using '$mdl_level'"); } } elsif ( $event =~ /^Alert/i ) { @@ -1004,11 +772,8 @@ sub getLevelLogEvent { return ($level,$log,$syslog); } - - - - - +# extract summary data for a given period from rrd files, +# via non-graph rrd graph declarations sub getSummaryStats { my %args = @_; @@ -1075,11 +840,8 @@ sub getSummaryStats push @option, ("--start", "$start", "--end", "$end") ; - # escape any : chars which might be in the database name, e.g handling C: in the RPN - $db =~ s/:/\\:/g; - { - no strict; + no strict; # *shudder* this is wrong, so wrong $database = $db; # global $speed = $IF->{$index}{ifSpeed} if $index ne ""; $inSpeed = $IF->{$index}{ifSpeed} if $index ne ""; @@ -1088,9 +850,10 @@ sub getSummaryStats $outSpeed = $IF->{$index}{ifSpeedOut} if $index ne "" and $IF->{$index}{ifSpeedOut}; # read from Model and translate variable ($database etc.) rrd options + # note that all inputs need colon-escaping, not just the database foreach my $str (@{$M->{stats}{type}{$type}}) { my $s = $str; - $s =~ s{\$(\w+)}{if(defined${$1}){${$1};}else{"ERROR, no variable \$$1 ";}}egx; + $s =~ s{\$(\w+)}{if(defined${$1}){postcolonial(${$1});}else{"ERROR, no variable \$$1 ";}}egx; if ($s =~ /ERROR/) { logMsg("ERROR ($S->{name}) model=$NI->{system}{nodeModel} type=$type ($str) in expanding variables, $s"); return; # error @@ -1129,17 +892,23 @@ sub getSummaryStats return; } -### 2011-12-29 keiths, added for consistent nodesummary generation +# whatever it is that goes into rrdgraph arguments, colons are Not Good +sub postcolonial +{ + my ($unsafe) = @_; + # but escaping already escaped colons isn't that much better + $unsafe =~ s/(?{node_summary_field_list} and $C->{node_summary_field_list} ne "" ) { $node_summary_field_list = $C->{node_summary_field_list}; @@ -1163,7 +932,7 @@ sub getNodeSummary { $nt{$nd}{nodeType} = $NI->{system}{nodeType}; $nt{$nd}{nodeModel} = $NI->{system}{nodeModel}; $nt{$nd}{nodeVendor} = $NI->{system}{nodeVendor}; - $nt{$nd}{lastUpdateSec} = $NI->{system}{lastUpdateSec}; + $nt{$nd}{last_poll} = $NI->{system}{last_poll}; $nt{$nd}{sysName} = $NI->{system}{sysName} ; $nt{$nd}{server} = $C->{'server_name'}; @@ -1195,13 +964,14 @@ sub getNodeSummary { $nt{$nd}{nodestatus} = "reachable"; } - my ($otgStatus,$otgHash) = outageCheck(node=>$nd,time=>time()); + my ($otgStatus,$otgHash) = outageCheck(node=>$nd, time=>time()); my $outageText; + if ( $otgStatus eq "current" or $otgStatus eq "pending") { my $color = ( $otgStatus eq "current" ) ? "#00AA00" : "#FFFF00"; - my $outageText = "node=$OT->{$otgHash}{node}
start=".returnDateStamp($OT->{$otgHash}{start}) - ."
end=".returnDateStamp($OT->{$otgHash}{end})."
change=$OT->{$otgHash}{change}"; + my $outageText = "node=$nd
start=".returnDateStamp($otgHash->{actual_start}) + ."
end=".returnDateStamp($otgHash->{actual_end})."
change=$otgHash->{change_id}"; } $nt{$nd}{outage} = $otgStatus; $nt{$nd}{outageText} = $outageText; @@ -1812,113 +1582,45 @@ sub overallNodeStatus { my $NT = loadNodeTable(); my $NS = loadNodeSummary(); - #print STDERR &returnDateStamp." overallNodeStatus: netType=$netType roleType=$roleType\n"; - - if ( $group eq "" and $customer eq "" and $business eq "" and $netType eq "" and $roleType eq "" ) { - foreach $node (sort keys %{$NT} ) { - if (getbool($NT->{$node}{active})) { - my $nodedown = 0; - my $outage = ""; - if ( $NT->{$node}{server} eq $C->{server_name} ) { - ### 2013-08-20 keiths, check for SNMP Down if ping eq false. - my $down_event = "Node Down"; - $down_event = "SNMP Down" if getbool($NT->{$node}{ping},"invert"); - $nodedown = eventExist($node, $down_event, undef)? 1:0; # returns the event filename - - ($outage,undef) = outageCheck(node=>$node,time=>time()); - } - else { - $outage = $NS->{$node}{outage}; - if ( getbool($NS->{$node}{nodedown})) { - $nodedown = 1; - } - } - - if ( $nodedown and $outage ne 'current' ) { - ($event_status) = eventLevel("Node Down",$NT->{$node}{roleType}); - } - else { - ($event_status) = eventLevel("Node Up",$NT->{$node}{roleType}); - } - - ++$statusHash{$event_status}; - ++$statusHash{count}; + foreach $node (sort keys %{$NT} ) + { + next if (!getbool($NT->{$node}{active})); + + if ( + ( $group eq "" and $customer eq "" and $business eq "" and $netType eq "" and $roleType eq "" ) + or + ( $netType ne "" and $roleType ne "" + and $NT->{$node}{net} eq "$netType" && $NT->{$node}{role} eq "$roleType" ) + or ($group ne "" and $NT->{$node}{group} eq $group) + or ($customer ne "" and $NT->{$node}{customer} eq $customer) + or ($business ne "" and $NT->{$node}{businessService} =~ /$business/ ) ) + { + my $nodedown = 0; + my $outage = ""; + if ( $NT->{$node}{server} eq $C->{server_name} ) { + ### 2013-08-20 keiths, check for SNMP Down if ping eq false. + my $down_event = "Node Down"; + $down_event = "SNMP Down" if getbool($NT->{$node}{ping},"invert"); + $nodedown = eventExist($node, $down_event, undef)? 1:0; # returns the event filename + + ($outage,undef) = outageCheck(node=>$node,time=>time()); } - } - } - elsif ( $netType ne "" and $roleType ne "" ) { - foreach $node (sort keys %{$NT} ) { - if (getbool($NT->{$node}{active})) { - if ( $NT->{$node}{net} eq "$netType" && $NT->{$node}{role} eq "$roleType" ) { - my $nodedown = 0; - my $outage = ""; - if ( $NT->{$node}{server} eq $C->{server_name} ) - { - ### 2013-08-20 keiths, check for SNMP Down if ping eq false. - my $down_event = "Node Down"; - $down_event = "SNMP Down" if getbool($NT->{$node}{ping},"invert"); - $nodedown = eventExist($node, $down_event, undef)? 1 : 0; - - ($outage,undef) = outageCheck(node=>$node,time=>time()); - } - else { - $outage = $NS->{$node}{outage}; - if ( getbool($NS->{$node}{nodedown})) { - $nodedown = 1; - } - } - - if ( $nodedown and $outage ne 'current' ) { - ($event_status) = eventLevel("Node Down",$NT->{$node}{roleType}); - } - else { - ($event_status) = eventLevel("Node Up",$NT->{$node}{roleType}); - } - - ++$statusHash{$event_status}; - ++$statusHash{count}; + else { + $outage = $NS->{$node}{outage}; + if ( getbool($NS->{$node}{nodedown})) { + $nodedown = 1; } } - } - } - elsif ( $group ne "" or $customer ne "" or $business ne "" ) { - foreach $node (sort keys %{$NT} ) { - if ( - getbool($NT->{$node}{active}) - and ( ($group ne "" and $NT->{$node}{group} eq $group) - or ($customer ne "" and $NT->{$node}{customer} eq $customer) - or ($business ne "" and $NT->{$node}{businessService} =~ /$business/ ) - ) - ) { - my $nodedown = 0; - my $outage = ""; - if ( $NT->{$node}{server} eq $C->{server_name} ) - { - ### 2013-08-20 keiths, check for SNMP Down if ping eq false. - my $down_event = "Node Down"; - $down_event = "SNMP Down" if getbool($NT->{$node}{ping},"invert"); - $nodedown = eventExist($node, $down_event, undef)? 1:0; - ($outage,undef) = outageCheck(node=>$node,time=>time()); - } - else { - $outage = $NS->{$node}{outage}; - if ( getbool($NS->{$node}{nodedown})) { - $nodedown = 1; - } - } - - if ( $nodedown and $outage ne 'current' ) { - ($event_status) = eventLevel("Node Down",$NT->{$node}{roleType}); - } - else { - ($event_status) = eventLevel("Node Up",$NT->{$node}{roleType}); - } - - ++$statusHash{$event_status}; - ++$statusHash{count}; - #print STDERR &returnDateStamp." overallNodeStatus: $node $group $event_status event=$statusHash{$event_status} count=$statusHash{count}\n"; + if ( $nodedown and $outage ne 'current' ) { + ($event_status) = eventLevel("Node Down",$NT->{$node}{roleType}); } + else { + ($event_status) = eventLevel("Node Up",$NT->{$node}{roleType}); + } + + ++$statusHash{$event_status}; + ++$statusHash{count}; } } @@ -2166,118 +1868,666 @@ sub loadEnterpriseTable { } -sub loadOutageTable { - my $OT = loadTable(dir=>'conf',name=>'Outage'); # get in cache -} +# outage api + +# outage data/argument structure: # -# check outage of node -# return status,key where status is pending or current, key is hash key of event table +# id (unique key, automatically generated on create, must be used for update and delete) +# frequency ('once', 'daily', 'weekly', 'monthly') +# start, end (a date/date+time/partial format that's suitable for the given frequency), +# change_id (required but free-form text, used to tag events), +# description (optional, free-form descriptive text), +# options (hash substructure that selects optional behaviours for this outage) +# nostats (default undef, if set to 1 only 'U' values are written to rrds during the outage) +# selector (hash substructure that defines what devices this outage covers) +# two category keys, 'node' and 'config' +# under these there can be any number of key => value filter expressions +# all filter expressions must match for the selector to match # -sub outageCheck { - my %args = @_; - my $node = $args{node}; - my $time = $args{time}; +# selector key X needs to be a CONFIG! property of the node if under node, +# (plus nodeModel from the node info), or a global nmis property if under config. +# +# value: either array, or string or regex-string ('/.../' or '/.../i') +# array: set of acceptable values; one or more must meet strict equality test for the selector succeed +# single string: strict equality +# regex-string: identified property must match - my $OT = loadOutageTable(); - # Get each of the nodes info in a HASH for playing with - foreach my $key (sort keys %{$OT}) { - if (($time-300) > $OT->{$key}{end}) { - outageRemove(key=>$key); # passed - } else { - if ( $node eq $OT->{$key}{node}) { - if ($time >= $OT->{$key}{start} and $time <= $OT->{$key}{end} ) { - return "current",$key; +# create new or update existing outage +# note that updates are absolute, not relative to existing outage! +# you must pass all desired arguments, not only ones you want changed +# +# args: id IFF updating, +# frequency/start/end/description/change_id/options/selector, +# meta (hash, optional, for audit logging, keys user and details. if missing, user will +# be set from os user of the current process) +# returns: hashref, keys success/error, id +sub update_outage +{ + my (%args) = @_; + + # validate the args first + # lock and load existing outages, + # create new one or update existing one, + # save and unlock + + my $meta = ref($args{meta}) eq "HASH"? $args{meta} : {}; + $meta->{user} ||= (getpwuid($<))[0]; + + my (%newrec, $op_create); + my $outid = $args{id}; + + if (!defined($outid) or $outid eq "") # 0 is ok, empty is not + { + $outid = NMIS::UUID::getRandomUUID(); + $op_create = 1; + } + $newrec{id} = $outid; + + # copy simple args + for my $copyable (qw(description change_id)) + { + $newrec{$copyable} = $args{$copyable}; + } + $newrec{options} = ref($args{options}) eq "HASH"? $args{options} : {}; # make sure it's a hash + + # check freq and freq vs start/end + my $freq = $args{frequency}; + return { error => "invalid frequency \"$freq\"!" } + if (!defined $freq or $freq !~ /^(once|daily|weekly|monthly)$/); + $newrec{frequency} = $args{frequency}; + + my %parsedtimes; + for my $check (qw(start end)) + { + my $doesitparse = $freq eq "once"? + ( ($args{$check} =~ /^\d+(\.\d+)?$/)? + $args{$check} : func::parseDateTime($args{$check}) || func::getUnixTime($args{$check}) ) + : _abs_time(relative => $args{$check}, frequency => $freq); + + return { error => "invalid $check argument \"$args{$check}\" for frequency $freq!" } + if (!$doesitparse); + + $parsedtimes{$check} = $doesitparse; + # for one-offs let's store the parsed value + # as it could have been a relative input like "now + 2 days"... + if ($freq eq "once") + { + $newrec{$check} = $doesitparse; + } + else + { + $newrec{$check} = $args{$check}; + } + } + return { error => "invalid times, start is later than end!" } + if ($freq eq "once" && $parsedtimes{start} >= $parsedtimes{end}); + + # quick/rough sanity check of selectors + $newrec{selector} = {}; + if (ref($args{selector}) eq "HASH") + { + for my $cat (qw(node config)) + { + my $catsel = $args{selector}->{$cat}; + next if (ref($catsel) ne "HASH"); + + for my $onesel (keys %$catsel) + { + # one string, or an array of strings + return { error => "invalid selector content for \"$cat.$onesel\"!" } + if (ref($catsel->{$onesel}) and ref($catsel->{$onesel}) ne "ARRAY"); + + if (ref($catsel->{$onesel}) eq "ARRAY") + { + # fix up any holes if item N was deleted but N+1... exist + $newrec{selector}->{$cat}->{$onesel} = [ grep( defined($_), @{$catsel->{$onesel}}) ]; } - elsif ($time < $OT->{$key}{start}) { - return "pending",$key; + elsif (defined $catsel->{$onesel}) + { + $newrec{selector}->{$cat}->{$onesel} = $catsel->{$onesel}; + } + else + { + delete $newrec{selector}->{$cat}->{$onesel}; } } } } - # check also dependency - my $NT = loadNodeTable(); - foreach my $nd ( split(/,/,$NT->{$node}{depend}) ) { - foreach my $key (sort keys %{$OT}) { - if ( $nd eq $OT->{$key}{node}) { - if ($time >= $OT->{$key}{start} and $time <= $OT->{$key}{end} ) { - # check if this node is down - my $NI = loadNodeInfoTable($nd); - if (getbool($NI->{system}{nodedown})) { - return "current",$key; - } - } + # inputs look good, lock and load! + + # except that loadtable doesn't allow file creation on the fly, only readfiletohash + # which is much lowerlevel wrt arguments :-/ + if (!existFile(dir => "conf", name => "Outages")) + { + writeTable(dir => "conf", name => "Outages", data => {}); + } + my ($data, $fh) = loadTable(dir => "conf", name => "Outages", lock => 1); + + return { error => "failed to lock Outages file: $!" } if (!$fh); + $data //= {}; # empty file is ok + + if ($op_create && ref($data->{$outid})) + { + close($fh); # unlock + return { error => "cannot create outage with id $outid: already existing!" }; + } + $data->{$outid} = \%newrec; + writeTable(dir => "conf", name => "Outages", handle => $fh, data => $data); + + func::audit_log(who => $meta->{user}, + what => ($op_create? "create_outage" : "update_outage"), + where => $outid, how => "ok", defails => $meta->{details}, when => undef); + + return { success => 1, id => $outid}; +} + +# reads an old-style outage.nmis and converts any current or future outages +# to the new format, then renames the file +# returns: undef if ok, error message otherwise +sub upgrade_outages +{ + my $C = loadConfTable(); # likely cached + + # we're clearly done if no conf/Outage.nmis exists + my $oldoutagefile = func::getFileName(file => $C->{''}."/Outage"); + return undef if (!-f $oldoutagefile); + + # load and lock the existing file + my ($old, $fh) = loadTable( dir=>'conf', name=>'Outage', lock=>'true'); + for my $outagekey (keys %$old) + { + my $orec = $old->{$outagekey}; + next if ($orec->{end} < time); + + my $res = update_outage(frequency => "once", + start => $orec->{start}, + end => $orec->{end}, + change_id => $orec->{change}, + selector => { node => { name => $orec->{node} } }, + meta => { user => ((getpwuid($<))[0]), # normally that's root + details => "automatic upgrade_outages" } ); + if (!$res->{success}) + { + close $fh; + return "failed to convert outage: $res->{error}"; + } + } + + # finally, rename the old file away + if (!rename($oldoutagefile, "$oldoutagefile.disabled")) + { + my $problem = $!; + close $fh; + return "cannot rename $oldoutagefile: $problem"; + } + close $fh; + + logMsg("INFO NMIS has successfully upgraded the Outage data structure, and the old configuration file was renamed to \"$oldoutagefile.disabled\"."); + + return undef; +} + + + +# take a relative/incomplete time and day specification and make into absolute timestamp +# args: relative (date + time, frequency-specific!), +# frequency (daily, weekly, monthly), +# base (optional, absolute base time; if not given now is used) +# +# the times are absolutified relative to now, so can be in the past or the future! +# returns: unix time if parseable, undef if not +sub _abs_time +{ + my (%args) = @_; + + my ($rel,$frequency) = @args{qw(relative frequency)}; + return undef if ($frequency !~ /^(once|daily|weekly|monthly)$/); + + my %wdlist = ("mon" => 1, "tue" => 2, "wed" => 3, "thu" => 4, "fri" => 5, "sat" => 6, "sun" => 7); + my $timezone = "local"; + my $dt = $args{base}? DateTime->from_epoch(epoch => $args{base}, time_zone => $timezone) + : DateTime->now(time_zone => $timezone); + + if ($frequency eq "weekly") + { + # format: weekday hh:mm(:ss)?, weekday is shortname! + # pull off and mangle the weekday first + if ($rel =~ s/^\s*(\S+)\s+//) + { + my $wd = lc(substr($1,0,3)); + return undef if !$wdlist{$wd}; + + # truncate to week begin, then add X-1 days (monday is day 1!, but DT-weekstart is monday too...) + $dt = $dt->truncate(to => "week")->add("days" => $wdlist{$wd}-1); + } + } + elsif ($frequency eq "monthly") + { + # format: DayNum hh:mm(:ss)? DayNum==1 means first day of the month, DayNum==-1 means LAST day of the month etc. + if ($rel =~ s/^(-?\d+)\s+//) + { + my $monthday = $1; + $dt = $dt->truncate(to => "month"); + if ($monthday <= 0) + { + $dt->add(months => 1)->subtract(days => -$monthday); + } + else + { + eval { $dt->set(day => $monthday); }; + return undef if $@; } } } + + if ($frequency eq "daily") + { + $dt = $dt->truncate(to => "day"); + } + else + { + return undef if !$dt; + } + + # all inputs must have a time component + # format: hh:mm(:ss)?, with 00:00 meaning day before and 24:00 day after + + if ($rel =~ /^\s*(\d+):(\d+)(:(\d+))?\s*$/) + { + my ($h,$m,$s) = ($1,$2,$4); + $s ||= 0; + + return $dt->add(days => 1)->epoch + if ($h == 24 and $m == 0 and $s == 0); # handle 24:00:00 + + eval { $dt->set_hour($h)->set_minute($m)->set_second($s) }; + return $@? undef: $dt->epoch; + } + else + { + return undef; # hh:mm(:ss)? is required + } } -sub outageRemove { - my %args = @_; - my $key = $args{key}; +# advances (or reduces) timestamp by X intervals, based on the given frequency +# args: timestamp, frquency, count (default: +1) +# returns new timestamp +sub _prev_next_interval +{ + my (%args) = @_; + my ($ts, $freq, $count) = @args{qw(timestamp frequency count)}; - my $C = loadConfTable(); - my $time = time(); - my $string; + my %freq2delta = ("daily" => { days => 1}, "weekly" => {weeks => 1}, + "monthly" => { months => 1 }); + return $ts if (!$freq or !$freq2delta{$freq} or (defined $count and !$count)); + $count ||= 1; - my ($OT,$handle) = loadTable(dir=>'conf',name=>'Outage',lock=>'true'); + my $timezone = "local"; + my $dt = DateTime->from_epoch(epoch => $ts, time_zone => $timezone); + my %delta = %{$freq2delta{$freq}}; - # dont log pending - if ($time > $OT->{$key}{start}) { - $string = ", Node $OT->{$key}{node}, Start $OT->{$key}{start}, End $OT->{$key}{end}, " - ."Change $OT->{$key}{change}, Closed $time, User $OT->{$key}{user}"; + if ($count < 0) + { + $delta{(keys %delta)[0]} = -$count; # only one key + return $dt->subtract(%delta)->epoch; + } + else + { + $delta{(keys %delta)[0]} = $count; + return $dt->add(%delta)->epoch; } +} - delete $OT->{$key}; +# remove existing outage +# args: id, optional meta (for audit logging, keys user, details) +# returns: hashrev, keys success/error +sub remove_outage +{ + my (%args) = @_; + my $id = $args{id}; - writeTable(dir=>'conf',name=>'Outage',data=>$OT,handle=>$handle); + return { error => "cannot remove outage without id argument!" } + if (!$id); - my @problems; + my $meta = ref($args{meta}) eq "HASH"? $args{meta} : {}; + $meta->{user} ||= (getpwuid($<))[0]; + + # lock and load the outages, + # delete the indicated one, + # save and unlock + my ($data, $fh) = loadTable(dir => "conf", name => "Outages", lock => "true"); + return { error => "failed to lock Outage file: $!" } if (!$fh); + $data //= {}; + + delete $data->{$id}; + writeTable(dir => "conf", name => "Outages", handle => $fh, data => $data); + + func::audit_log(who => $meta->{user}, + what => "remove_outage", + where => $id, + how => "ok", + defails => $meta->{details}, + when => undef); + + return { success => 1}; +} + +# find outages, all or filtered +# args: filter (optional, hashref of outage properties => check values) +# note: filters are verbatim/passive/inert, ie. checked against the +# raw outage schedule - NOT evaluated with any nodes' nodeinfo/models etc! +# +# filter properties: id, description, change_id, frequency/start/end, +# options.nostats, selector.node.X, selector.config.Y - must be given in dotted form! +# filter check values can be: qr// or plain string/number. +# for array selectors one or more elems must match for the filter to match. +# +# returns: hashref of success/error, outages (=array of matching outages) +sub find_outages +{ + my (%args) = @_; + my $filter = ref($args{filter}) eq "HASH"? $args{filter} : {}; + + my $data = loadTable(dir => "conf", name => "Outages") + if (existFile(dir => "conf", name => "Outages")); # or we get lots of log noise + $data //= {}; + + # unfiltered? + return { success => 1, outages => [ values %$data ] } + if (!keys %$filter); - if ($string ne '') { - # log this action but DON'T DEADLOCK - logMsg locks, too! - if ( open($handle,">>$C->{outage_log}") ) { - if ( flock($handle, LOCK_EX) ) { - if ( not print $handle returnDateStamp()." $string\n" ) { - push(@problems, "cannot write file $C->{outage_log}: $!"); + my @matches; + SCRATCHMONKEY: + for my $candidate (values %$data) + { + for my $filterprop (keys %$filter) + { + my ($have, $diag) = func::follow_dotted($candidate, $filterprop); + next SCRATCHMONKEY if ($diag); # requested thing not present or wrong structure + # none of the array elems match? (or the one and only thing doesn't? + my @maybes = (ref($have) eq "ARRAY")? @$have: ($have); + + my $expected = $filter->{$filterprop}; + next SCRATCHMONKEY if ( List::Util::none { ref($expected) eq "Regexp"? + ($_ =~ $expected) : + ($_ eq $expected) } (@maybes) ); + } + push @matches, $candidate; # survived! + } + + return { success => 1, outages => \@matches }; +} + +# find active/future/past outages for a given context, +# ie. one node and a time - or potential outages, if only +# given time. +# +# args: time (a unix timestamp, fractional is ok, required), +# node or uuid or a live and init'd sys object (optional), +# +# returns: hashref, with keys success/error, past, current, future: arrays (can be empty) +# +# current: outages that fully apply - these are amended with actual_start/actual_end unix TS, +# and sorted by actual_start. +# past: past one-off (not recurring ones!) outages for this node +# future: future outages for this node, also with actual_start/actual_end (of the next instance), +# sorted by actual_start. +sub check_outages +{ + my (%args) = @_; + my $when = $args{time}; + my $S = $args{sys}; # optional + + return { error => "cannot check outages without valid time argument!" } + if (!$when or $when !~ /^\d+(\.d+)?$/); + + my $outagedata = loadTable(dir => "conf", name => "Outages") + if (existFile(dir => "conf", name => "Outages")); + $outagedata //= {}; + # no outages, no problem + return { success => 1, future => [], past => [], current => [] } + if (!keys %$outagedata); + + # get the data for selectors + my $C = loadConfTable; # cached + + my $who; + if (ref($S) eq "Sys") # that has all info ready... + { + $who = Clone::clone($S->ndcfg->{node}); # but let's not mess up sys datastructures + $who->{nodeModel} = $S->ndinfo->{system}->{nodeModel}; + } + elsif ($args{uuid} or $args{node}) + { + my $LNT = loadLocalNodeTable(); + if ($args{uuid}) + { + $who = (grep($_->{uuid} eq $args{uuid}, values %$LNT))[0]; # at most one + return { error => "no node with uuid \"$args{uuid}\" exists!" } if (!$who); + $who = Clone::clone($who); # no polluting of lnt with nodeModel + } + else + { + $who = Clone::clone($LNT->{ $args{node} }); # no polluting of lnt with nodeModel + return { error => "no node named \"$args{node}\" exists!" } if (!$who); + } + + # also pull the node info for the nodeModel property + my $ninfo = loadNodeInfoTable( $who->{name} ); + $who->{nodeModel} = $ninfo->{system}->{nodeModel}; + } + + my (@future,@past,@current); + for my $outid (keys %$outagedata) + { + my $maybeout = $outagedata->{$outid}; + + # let's check all selectors, iff there is something to check against + if ($who) + { + my $rulematches = 1; + for my $selcat (qw(config node)) + { + next if (ref($maybeout->{selector}->{$selcat}) ne "HASH"); + + for my $propname (keys %{$maybeout->{selector}->{$selcat}}) + { + my $actual = ($selcat eq "config"? + $C->{$propname} : $who->{$propname}); + + # choices can be: regex, or fixed string, or array of fixed strings + my $expected = $maybeout->{selector}->{$selcat}->{$propname}; + + # list of precise matches + if (ref($expected) eq "ARRAY") + { + $rulematches = 0 if (! List::Util::any { $actual eq $_ } @$expected); + } + # or a regex-like string + elsif ($expected =~ m!^/(.*)/(i)?$!) + { + my ($re,$options) = ($1,$2); + my $regex = ($options? qr{$re}i : qr{$re}); + $rulematches = 0 if ($actual !~ $regex); + } + # or a single precise match + else + { + $rulematches = 0 if ($actual ne $expected); + } + + last if (!$rulematches); } - } else { - push(@problems, "cannot lock file $C->{outage_log}: $!"); + last if (!$rulematches); } - close $handle; - map { logMsg("ERROR (nmis) $_") } (@problems); + # didn't survive all selector rules? note that no selectors === match + next if (!$rulematches); + } - setFileProt($C->{outage_log}); - } else { - logMsg("ERROR (nmis) cannot open file $C->{outage_log}: $!"); + # how about the time? + my $intime; + if ($maybeout->{frequency} eq "once") + { + if ($when < $maybeout->{start}) + { + push @future, { %$maybeout, + actual_start => $maybeout->{start}, + actual_end => $maybeout->{end} }; # convenience only + } + elsif ($when >= $maybeout->{start} && $when <= $maybeout->{end}) + { + push @current, { %$maybeout, + actual_start => $maybeout->{start}, + actual_end => $maybeout->{end} }; # convenience only + $intime = 1; + } + else # ie. > $maybeout->{end} + { + push @past, $maybeout; + } } + elsif ($maybeout->{frequency} =~ /^(daily|weekly|monthly)$/) + { + # absolute time is going to be 'near' when, but that's not quite good enough + my $start = _abs_time(relative => $maybeout->{start}, + frequency => $maybeout->{frequency}, + base => $when); + return { error => "outage \"$outid\" has invalid start \"$maybeout->{start}\"!" } + if (!defined $start); + + my $end = _abs_time(relative => $maybeout->{end}, + frequency => $maybeout->{frequency}, + base => $when); + return { error => "outage \"$outid\" has invalid end \"$maybeout->{end}\"!" } + if (!defined $end); + + # start after end? (e.g. daily, start 1400, end 0200) -> start must go back one interval + # (or end would have to go forward one) + $start = _prev_next_interval(timestamp => $start, frequency => $maybeout->{frequency}, + count => -1) if ($start > $end); + + # advance or retreat until closest to when + if ($when < $start && $when < $end) # retreat + { + while ($when < $start && $when < $end) + { + $start = _prev_next_interval(timestamp => $start, + frequency => $maybeout->{frequency}, count => -1); + $end = _prev_next_interval(timestamp => $end, + frequency => $maybeout->{frequency}, count => -1); + } + } + elsif ($when > $start && $when > $end) # advance + { + while ($when > $start && $when > $end) + { + $start = _prev_next_interval(timestamp => $start, + frequency => $maybeout->{frequency}, count => 1); + $end = _prev_next_interval(timestamp => $end, + frequency => $maybeout->{frequency}, count => 1); + } + } + + # before both start and end is obviously future + if ($when < $start) + { + push @future, { %$maybeout, actual_start => $start, actual_end => $end }; + } + # but *after* both start and end is also future, just plus one or more repeat intervals + elsif ($when > $end) + { + push @future, { %$maybeout, + actual_start => _prev_next_interval(timestamp => $start, + frequency => $maybeout->{frequency}, + count => 1), + actual_end => _prev_next_interval(timestamp => $end, + frequency => $maybeout->{frequency}, + count => 1), + }; + } + # and current is inbetween + elsif ($when >= $start && $when <= $end) + { + push @current, { %$maybeout, actual_start => $start, actual_end => $end }; + $intime = 1; + } + } + else + { + return { error => "outage \"$outid\" has invalid frequency!" }; + } + + next if (!$intime); } + + # sort current and future list by the actual start time + @current = sort { $a->{actual_start} <=> $b->{actual_start} } @current; + @future = sort { $a->{actual_start} <=> $b->{actual_start} } @future; + + return { success => 1, past => \@past, current => \@current, future => \@future }; } -### HIGHLY EXPERIMENTAL! -#sub sendTrap { -# my %arg = @_; -# use SNMP_util; -# my @servers = split(",",$arg{server}); -# foreach my $server (@servers) { -# print "Sending trap to $server\n"; -# #my($host, $ent, $agent, $gen, $spec, @vars) = @_; -# snmptrap( -# $server, -# ".1.3.6.1.4.1.4818", -# "127.0.0.1", -# 6, -# 1000, -# ".1.3.6.1.4.1.4818.1.1000", -# "int", -# "2448816" -# ); -# } -#} +# compat wrapper around check_outages +# checks outage(s) for one node X and all nodes that X depends on +# +# fixme: why check dependency nodes at all? why only if those are down? +# and only if no direct future outages? +# +# args: node (name), time (unix ts), both required +# returns: nothing or ('current', FIRST current outage record) +# or ('pending', FIRST future outage) +# +sub outageCheck +{ + my %args = @_; + + my $node = $args{node}; + my $time = $args{time}; + + my $nodeoutages = check_outages(node => $node, time => $time); + if (!$nodeoutages->{success}) + { + logMessage("ERROR failed to check $node outages: $nodeoutages->{error}"); + return; + } + if (@{$nodeoutages->{current}}) + { + return ("current", $nodeoutages->{current}->[0]); + } + elsif (@{$nodeoutages->{future}}) + { + return ("pending", $nodeoutages->{future}->[0]); + } + # if neither current nor future, check dependency nodes with + # current outages and that are down + my $NT = loadNodeTable(); + foreach my $nd ( split(/,/,$NT->{$node}{depend}) ) + { + # ignore nonexistent stuff, defaults and circular self-dependencies + next if ($nd =~ m!^(N/A|$node)?$!); + my $depoutages = check_outages(node => $nd, time => $time); + if (!$depoutages->{success}) + { + logMessage("ERROR failed to check $nd outages: $depoutages->{error}"); + return; + } + if (@{$depoutages->{current}}) + { + # check if this node is down + my $NI = loadNodeInfoTable($nd); + if (getbool($NI->{system}{nodedown})) + { + return ("current", $depoutages->{current}->[0]); + } + } + } + return; +} # small translator from event level to priority: header for email sub eventToSMTPPri { @@ -2340,23 +2590,41 @@ sub dutyTime { } -sub resolveDNStoAddr { - my $dns = shift; - my $addr; - my $oct; +# quick dns lookup for names +# args: name +# returns: list of addresses (or empty array) +sub resolve_dns_name +{ + my ($lookup) = @_; + my @results; - # convert node name to octal ip address - if ($dns ne "" ) { - if ($dns !~ /\d+\.\d+\.\d+\.\d+/) { - my $h = gethostbyname($dns); - return if not $h; - $addr = inet_ntoa($h->addr) ; - } else { $addr = $dns; } - return $addr if $addr =~ /\d+\.\d+\.\d+\.\d+/; + # full ipv6 support works only with newer socket module + my ($err,@possibles) = Socket::getaddrinfo($lookup,'', + {socktype => SOCK_RAW}); + return () if ($err); + + for my $address (@possibles) + { + my ($err,$ipaddr) = Socket::getnameinfo( + $address->{addr}, + Socket::NI_NUMERICHOST(), + Socket::NIx_NOSERV()); + push @results, $ipaddr if (!$err and $ipaddr ne $lookup); # suppress any nop results } - return; + return @results; } +# wrapper around resolve_dns_name, +# returns the _first_ available ip _v4_ address or undef +sub resolveDNStoAddr +{ + my ($name) = @_; + + my @addrs = resolve_dns_name($name); + my @v4 = grep(/^\d+.\d+.\d+\.\d+$/, @addrs); + + return $v4[0]; +} # create http for a clickable graph sub htmlGraph { @@ -2372,7 +2640,7 @@ sub htmlGraph { $target = $group; } - my $id = "$target-$intf-$graphtype"; + my $id = uri_escape("$target-$intf-$graphtype"); # intf and node are unsafe my $C = loadConfTable(); my $width = $args{width}; # graph size @@ -2382,18 +2650,19 @@ sub htmlGraph { my $urlsafenode = uri_escape($node); my $urlsafegroup = uri_escape($group); + my $urlsafeintf = uri_escape($intf); my $time = time(); - my $clickurl = "$C->{'node'}?conf=$C->{conf}&act=network_graph_view&graphtype=$graphtype&group=$urlsafegroup&intf=$intf&server=$server&node=$urlsafenode"; + my $clickurl = "$C->{'node'}?conf=$C->{conf}&act=network_graph_view&graphtype=$graphtype&group=$urlsafegroup&intf=$urlsafeintf&server=$server&node=$urlsafenode"; if( getbool($C->{display_opcharts}) ) { - my $graphLink = "$C->{'rrddraw'}?conf=$C->{conf}&act=draw_graph_view&group=$urlsafegroup&graphtype=$graphtype&node=$urlsafenode&intf=$intf&server=$server". + my $graphLink = "$C->{'rrddraw'}?conf=$C->{conf}&act=draw_graph_view&group=$urlsafegroup&graphtype=$graphtype&node=$urlsafenode&intf=$urlsafeintf&server=$server". "&start=&end=&width=$width&height=$height&time=$time"; my $retval = qq|
|; } else { - my $src = "$C->{'rrddraw'}?conf=$C->{conf}&act=draw_graph_view&group=$urlsafegroup&graphtype=$graphtype&node=$urlsafenode&intf=$intf&server=$server". + my $src = "$C->{'rrddraw'}?conf=$C->{conf}&act=draw_graph_view&group=$urlsafegroup&graphtype=$graphtype&node=$urlsafenode&intf=$urlsafeintf&server=$server". "&start=&end=&width=$width&height=$height&time=$time"; ### 2012-03-28 keiths, changed graphs to come up in their own Window with the target of node, handy for comparing graphs. return qq| @@ -2527,7 +2796,14 @@ sub createHrButtons # and let's combine these in a 'diagnostic' menu as well push @out, "
"; if ($NI->{system}{server} eq $C->{server_name}) { + my $location_field_name = "sysLocation"; + if ( defined $C->{location_field_name} and $C->{location_field_name} ne "" ) { + $location_field_name = $C->{location_field_name}; + } + push @out, CGI::td({class=>'header litehead'}, CGI::a({class=>'wht',href=>"tables.pl?conf=$confname&act=config_table_show&table=Contacts&key=".uri_escape($NI->{system}{sysContact})."&node=$urlsafenode&refresh=$refresh&widget=$widget&server=$server"},"contact")) if $NI->{system}{sysContact} ne ''; push @out, CGI::td({class=>'header litehead'}, - CGI::a({class=>'wht',href=>"tables.pl?conf=$confname&act=config_table_show&table=Locations&key=".uri_escape($NI->{system}{sysLocation})."&node=$urlsafenode&refresh=$refresh&widget=$widget&server=$server"},"location")) - if $NI->{system}{sysLocation} ne ''; + CGI::a({class=>'wht',href=>"tables.pl?conf=$confname&act=config_table_show&table=Locations&key=".uri_escape($NI->{system}{$location_field_name})."&node=$urlsafenode&refresh=$refresh&widget=$widget&server=$server"},"location")) + if $NI->{system}{$location_field_name} ne ''; } push @out, "
  • Diagnostic ▾
      "; - push @out, CGI::li(CGI::a({class=>'wht',href=>"telnet://$NI->{system}{host}",target=>'_blank'},"telnet")) + # drill-in for the node's collect/update time + push @out, CGI::li(CGI::a({class=>"wht", + href=> "$C->{''}/node.pl?conf=$confname&act=network_graph_view&widget=false&node=$urlsafenode&graphtype=polltime", + target=>"_blank"}, + "Collect/Update Runtime")); + + push @out, CGI::li(CGI::a({class=>'wht', + href=>"telnet://$NI->{system}{host}",target=>'_blank'},"telnet")) if (getbool($C->{view_telnet})); if (getbool($C->{view_ssh})) { @@ -2558,12 +2834,17 @@ sub createHrButtons push @out, "
"; @@ -3630,7 +3911,7 @@ sub checkEvent my %args = @_; my $S = $args{sys}; - my $node = $S->{node}; + my $node = $S->{node}; # WARNING: this is the lowercased name! my $event = $args{event}; my $element = $args{element}; my $details = $args{details}; @@ -3677,7 +3958,8 @@ sub checkEvent } elsif ( $event =~ /Proactive/ ) { - if ( defined(my $value = $args{value}) and defined(my $reset = $args{reset}) ) + my ($value,$reset) = @args{"value","reset"}; + if (defined $value and defined $reset) { # but only if we have cleared the threshold by 10% # for thresholds where high = good (default 1.1) @@ -3708,20 +3990,23 @@ sub checkEvent { $event =~ s/down/Up/i; } + elsif ($event =~ /\Wopen($|\W)/i) + { + $event =~ s/(\W)open($|\W)/$1Closed$2/i; + } # event was renamed/inverted/massaged, need to get the right control record # this is likely not needed $thisevent_control = $events_config->{$event} || { Log => "true", Notify => "true", Status => "true"}; - $details .= " Time=$outage"; + $details .= ($details? " " : "") . "Time=$outage"; ($level,$log,$syslog) = getLevelLogEvent(sys=>$S, event=>$event, level=>'Normal'); - my $OT = loadOutageTable(); - - my ($otg,$key) = outageCheck(node=>$node,time=>time()); + # the REAL node name is required, not lowercased! + my ($otg,$outageinfo) = outageCheck(node => $S->{name}, time=>time()); if ($otg eq 'current') { - $details .= " outage_current=true change=$OT->{$key}{change}"; + $details .= " outage_current=true change=$outageinfo->{change_id}"; } # now we save the new up event, and move the old down event into history @@ -3793,6 +4078,7 @@ sub notify my $element = $args{element}; my $details = $args{details}; my $level = $args{level}; + my $node = $S->{name}; my $log; my $syslog; @@ -3845,12 +4131,9 @@ sub notify my $is_stateless = ($C->{non_stateful_events} !~ /$event/ or getbool($thisevent_control->{Stateful}))? "false": "true"; - ### 2016-04-30 ks adding outage tagging to event when opened. - my $OT = loadOutageTable(); - - my ($otg,$key) = outageCheck(node=>$node,time=>time()); + my ($otg,$outageinfo) = outageCheck(node=>$node,time=>time()); if ($otg eq 'current') { - $details .= " outage_current=true change=$OT->{$key}{change}"; + $details .= ($details? " ":""). "outage_current=true change=$outageinfo->{change_id}"; } # Create and store this new event; record whether stateful or not @@ -4216,10 +4499,29 @@ sub rename_node return(1, "New node $new already exists, NOT overwriting!") if ($newnoderec); + # rename must not interfere with collect or update, + # so let's check those lockfiles - then acquire an update lock + # fixme: existsPollLock + createPollLock is insufficiently atomic to be perfectly reliable) + for my $nogoodlock (qw(collect update)) + { + return (1, "Cannot rename node while an $nogoodlock operation is running!") + if (existsPollLock(type => $nogoodlock, conf => $C->{conf}, node => $old)); + } + my $lockHandle = createPollLock(type => "update", + conf => $C->{conf}, + node => $old); + return (1, "Failed to create lock for rename operation") if (!$lockHandle); + $newnoderec = { %$oldnoderec }; $newnoderec->{name} = $new; $nodeinfo->{$new} = $newnoderec; + # save the nodes file away, so that we can restore it if needed + my $nodesfile = $C->{''}."/Nodes.nmis"; + my $backupfile = $C->{''}."/Nodes.nmis.pre-rename"; + unlink($backupfile); # make room, make room; should not be present + File::Copy::mv($nodesfile, $backupfile); # that should preserve the permissions + # now write out the new nodes file, so that the new node becomes # workable (with sys etc) # fixme lowprio: if db_nodes_sql is enabled we need to use a @@ -4230,7 +4532,12 @@ sub rename_node # then hardlink the var files - do not delete anything yet! my @todelete; my $vardir = $C->{''}; - opendir(D, $vardir) or return(1, "cannot read dir $vardir: $!"); + if (!opendir(D, $vardir)) + { + File::Copy::mv($backupfile,$nodesfile); + releasePollLock(handle => $lockHandle, type => "update", conf => $C->{conf}, node => $old); + return(1, "cannot read dir $vardir: $!"); + } for my $fn (readdir(D)) { if ($fn =~ /^$old-(node|view)\.(\S+)$/i) @@ -4238,9 +4545,15 @@ sub rename_node my ($component,$ext) = ($1,$2); my $newfn = lc("$new-$component.$ext"); push @todelete, "$vardir/$fn"; + print STDERR "Renaming/linking var/$fn to $newfn\n" if ($wantdiag); - link("$vardir/$fn", "$vardir/$newfn") or - return(1,"cannot hardlink $fn to $newfn: $!"); + unlink("$vardir/$newfn") if (-e "$vardir/$newfn"); # would conflict, we want to overwrite + if (!link("$vardir/$fn", "$vardir/$newfn")) + { + File::Copy::mv($backupfile,$nodesfile); + releasePollLock(handle => $lockHandle, type => "update", conf => $C->{conf}, node => $old); + return(1,"cannot hardlink $fn to $newfn: $!"); + } } } closedir(D); @@ -4309,6 +4622,9 @@ sub rename_node cleanEvent($old,$args{originator}); print STDERR "Successfully renamed node $old to $new\n" if ($wantdiag); + releasePollLock(handle => $lockHandle, type => "update", conf => $C->{conf}, node => $old); + unlink($backupfile); + return (0,undef); } diff --git a/lib/NMIS/UUID.pm b/lib/NMIS/UUID.pm index 604114d..2ad9755 100644 --- a/lib/NMIS/UUID.pm +++ b/lib/NMIS/UUID.pm @@ -28,54 +28,21 @@ # # ***************************************************************************** package NMIS::UUID; -our $VERSION = "1.3.0"; +our $VERSION = "1.4.0"; use strict; use Fcntl qw(:DEFAULT :flock); -use NMIS; use func; use UUID::Tiny qw(:std); use vars qw(@ISA @EXPORT); +# fixme: get rid of the export - counterproductive use Exporter; @ISA = qw(Exporter); -@EXPORT = qw(auditNodeUUID createNodeUUID getUUID); +@EXPORT = qw(createNodeUUID getUUID); -# check which nodes do not have UUID's. -sub auditNodeUUID { - #load nodes - #foreach node - # Does it have a UUID? - # Print exception - #done - my $C = loadConfTable(); - my $success = 1; - my $LNT = loadLocalNodeTable(); - my $UUID_INDEX; - foreach my $node (sort keys %{$LNT}) { - if (!keys %{$LNT->{$node}}) - { - print "ERROR: $node is completely blank!\n"; - } - elsif ( $LNT->{$node}{uuid} eq "" ) { - print "ERROR: $node does not have a UUID\n"; - } - else { - print "Node: $node, UUID: $LNT->{$node}{uuid}\n" if $C->{debug}; - if ($UUID_INDEX->{$LNT->{$node}{uuid}} ne "" ) { - print "ERROR: the improbable has happened, a UUID conflict has been found for $LNT->{$node}{uuid}, between $node and $UUID_INDEX->{$LNT->{$node}{uuid}}\n"; - } - else { - $UUID_INDEX->{$LNT->{$node}{uuid}} = $node; - $UUID_INDEX->{$node} = $LNT->{$node}{uuid}; - } - } - } - return $success; -} - # translate between data::uuid and uuid::tiny namespace constants # namespace_ (url,dns,oid,x500) in data::uuid correspond to UUID_NS_ in uuid::tiny my %known_namespaces = map { my $varname = "UUID_NS_$_"; @@ -87,12 +54,14 @@ my %known_namespaces = map { my $varname = "UUID_NS_$_"; # # args: one node name (optional) # returns the number of nodes that were changed + +# note: caller of this function must have loaded the NMIS module! sub createNodeUUID { my ($justonenode) = @_; my $C = loadConfTable(); - my $LNT = loadLocalNodeTable(); + my $LNT = NMIS::loadLocalNodeTable(); my ($UUID_INDEX, $changed_nodes, $mustupdate); @@ -168,6 +137,14 @@ sub getUUID return $uuid; } +# small helper that generates a random-based uuid +# args: none +# returns: uuid string +sub getRandomUUID +{ + return create_uuid_as_string(UUID_RANDOM); +} + # create a new namespaced uuid from concat of all components that are passed in # if there's a configured namespace prefix that is used; otherwise the UUID_NS_URL is used w/o prefix. # returns: uuid string diff --git a/lib/Sys.pm b/lib/Sys.pm index e516a6b..ef8a38c 100644 --- a/lib/Sys.pm +++ b/lib/Sys.pm @@ -27,7 +27,7 @@ # # ***************************************************************************** package Sys; -our $VERSION = "2.0.0"; +our $VERSION = "2.1.0"; use strict; use lib "../../lib"; @@ -41,7 +41,7 @@ use WMI; use Fcntl qw(:DEFAULT :flock); use Data::Dumper; $Data::Dumper::Indent = 1; -use List::Util; +use List::Util 1.33; # older versions have no working any() use Clone; # the sys constructor does next to nothing, just roughly setup the structure @@ -122,10 +122,12 @@ sub status # # node config is loaded if snmp or wmi args are true # args: node (mostly required, or name), snmp (defaults to 1), wmi (defaults to the value for snmp), -# update (defaults to 0), cache_models (see code comments for defaults), force (defaults to 0) +# update (defaults to 0), cache_models (see code comments for defaults), force (defaults to 0), +# policy (default unset) # # update means ignore model loading errors, also disables cache_models # force means ignore the old node file, only relevant if update is enabled as well. +# if policy is given (hashref of ping/wmi/snmp => numeric seconds) then the rrd db params are overridden # # returns: 1 if _everything_ was successful, 0 otherwise, also sets details for status() sub init @@ -138,11 +140,14 @@ sub init $self->{debug} = $args{debug}; $self->{update} = getbool($args{update}); + my $policy = $args{policy}; # optional + # flag for init snmp accessor, default is yes my $snmp = getbool(exists $args{snmp}? $args{snmp}: 1); # ditto for wmi, but default from snmp my $wantwmi = getbool(exists $args{wmi}? $args{wmi} : $snmp); + my $C = loadConfTable(); # needed to determine the correct dir; generally cached and a/v anyway if (ref($C) ne "HASH" or !keys %$C) { @@ -248,8 +253,8 @@ sub init my $curmodel = $self->{info}{system}{nodeModel}; my $loadthis = "Model"; - # get the specific model - if ($curmodel and not $self->{update}) + # get the node's model IFF its valid + if ($curmodel and $curmodel ne "Model" and not $self->{update}) { $loadthis = "Model-$curmodel"; } @@ -265,6 +270,94 @@ sub init # model loading failures are terminal return 0 if (!$self->loadModel(model => $loadthis)); + # if a policy is given, override the database timing part of the model data + # traverse all the model sections, find out which sections are subject to which timing policy + if (ref($policy) eq "HASH") + { + # must get that before it's overwritten + my $standardstep = $self->{mdl}->{database}->{db}->{timing}->{default}->{poll} // 300; + my %resizeme; # section name -> factor + + for my $topsect (keys %{$self->{mdl}}) + { + next if (ref($self->{mdl}->{$topsect}->{rrd}) ne "HASH"); + for my $subsect (keys %{$self->{mdl}->{$topsect}->{rrd}}) + { + my $interesting = $self->{mdl}->{$topsect}->{rrd}->{$subsect}; + my $haswmi = ref($interesting->{wmi}) eq "HASH"; + my $hassnmp = ref($interesting->{snmp}) eq "HASH"; + + if ($hassnmp and $haswmi) + { + dbg("section $subsect subject to both snmp and wmi poll policy overrides: " + . "poll snmp $policy->{snmp}, wmi $policy->{wmi}") if ($self->{debug} > 1); + # poll: smaller of the two, heartbeat: larger of the two + my $poll = defined($policy->{snmp})? $policy->{snmp} : 300; + $poll = $policy->{wmi} if (defined($policy->{wmi}) && $policy->{wmi} < $poll); + $poll ||= 300; + + my $heartbeat = defined($policy->{snmp})? $policy->{snmp} : 300; + $heartbeat = $policy->{wmi} if (defined($policy->{wmi}) && $policy->{wmi} > $heartbeat); + $heartbeat ||= 300; + $heartbeat *= 3; + + my $thistiming = $self->{mdl}->{database}->{db}->{timing}->{$subsect} ||= {}; + + $thistiming->{poll} = $poll; + $thistiming->{heartbeat} = $heartbeat; + + dbg("overrode rrd timings for $subsect with step $poll, heartbeat $heartbeat"); + $resizeme{$subsect} = $standardstep / $poll; + } + elsif ($haswmi or $hassnmp) + { + my $which = $hassnmp? "snmp" : "wmi"; + if (defined $policy->{$which}) + { + dbg("section \"$subsect\" subject to $which polling policy override: poll $policy->{$which}") + if ($self->{debug} > 1); + + my $thistiming = $self->{mdl}->{database}->{db}->{timing}->{$subsect} ||= {}; + $thistiming->{poll} = $policy->{$which} || 300; + $thistiming->{heartbeat} = 3*( $policy->{$which} || 900); + + $resizeme{$subsect} = $standardstep / $thistiming->{poll}; + } + } + } + } + # AND set the default to the snmp timing, to cover unmodelled sections + # (which are currently all snmp-based, e.g. hrsmpcpu) + if ($policy->{snmp}) # not null + { + $self->{mdl}->{database}->{db}->{timing}->{default}->{poll} = $policy->{snmp}; + $self->{mdl}->{database}->{db}->{timing}->{default}->{heartbeat} = 3* $policy->{snmp}; + $resizeme{default} = $standardstep / $policy->{snmp}; + } + + # increase the rows_* sizes for these sections, if the step is shorter than the default + # use 'default' or hardcoded default if missing + my $standardsize = (ref($self->{mdl}->{database}->{db}->{size}) eq "HASH" + && ref($self->{mdl}->{database}->{db}->{size}->{default}) eq "HASH"? + { %{$self->{mdl}->{database}->{db}->{size}->{default} }} # shallow clone required, default is ALSO changed! + : { step_day => 1, step_week => 6, step_month => 24, step_year => 288, + rows_day => 2304, rows_week => 1536, rows_month => 2268, rows_year => 1890 }); + for my $maybe (sort keys %resizeme) + { + my $factor = $resizeme{$maybe}; + next if ($factor <= 1); + + my $sizesection = $self->{mdl}->{database}->{db}->{size} ||= {}; + $sizesection->{$maybe} ||= { %$standardsize }; # shallow clone + for my $period (qw(day week month year)) + { + $sizesection->{$maybe}->{"rows_$period"} = + int($factor * $sizesection->{$maybe}->{"rows_$period"} + 0.5); # round up/down + } + dbg(sprintf("overrode rrd row counts for $maybe by factor %.2f",$factor)) if ($self->{debug} > 1); + } + } + # init the snmp accessor if snmp wanted and possible, but do not connect (yet) if ($self->{name} and $snmp and $thisnodeconfig->{collect}) { @@ -327,9 +420,10 @@ sub open # prime config for snmp, based mostly on cfg->node - cloned to not leak any of the updated bits my $snmpcfg = Clone::clone($self->{cfg}->{node}); - # check if numeric ip address is available for speeding up, conversion done by type=update - $snmpcfg->{host} = ( $self->{info}{system}{host_addr} - || $self->{cfg}{node}{host} || $self->{cfg}{node}{name} ); + # check if numeric ip address is available for speeding up, update done in getnodeinfo, AFTER this open + # hence host_addr must be ignored if this is type=update, or a bad one will never clear + $snmpcfg->{host} = (($self->{update}? undef : $self->{info}{system}{host_addr}) + || $self->{cfg}{node}{host} || $self->{cfg}{node}{name}); $snmpcfg->{timeout} = $args{timeout} || 5; $snmpcfg->{retries} = $args{retries} || 1; $snmpcfg->{oidpkt} = $args{oidpkt} || 10; @@ -726,6 +820,15 @@ sub getValues } } + # check if we should just skip any collect and leave this to a plugin to collect + # we need to have an rrd section so we can define the graphtypes. + if ($thissection->{skip_collect} and getbool($thissection->{skip_collect})) + { + dbg("skip_collect $thissection->{skip_collect} found for section=$sectionname",2); + $status{skipped} = "skipped $sectionname because skip_collect set to true"; + next; + } + # should we add graphtype to given (info) table? if (ref($tbl) eq "HASH") { @@ -1142,8 +1245,9 @@ sub loadModel createDir($modelcachedir); setFileProt($modelcachedir); } - my $thiscf = "$modelcachedir/$model.json"; + my $shortname = $model; $shortname =~ s/^Model-//; + my $thiscf = "$modelcachedir/$model.json"; if ($self->{cache_models} && -f $thiscf) { $self->{mdl} = readFiletoHash(file => $thiscf, json => 1, lock => 0); @@ -1166,6 +1270,11 @@ sub loadModel } else { + # prime the nodeModel property from the model's filename, + # ignoring whatever may be in the deprecated nodeModel property + # in the model file + $self->{mdl}->{system}->{nodeModel} = $shortname; + # continue with loading common Models foreach my $class (keys %{$self->{mdl}{'-common-'}{class}}) { @@ -1196,6 +1305,7 @@ sub loadModel } } + # if the loading has succeeded (cache or from source), optionally amend with rules from the policy if ($exit) { @@ -1218,7 +1328,7 @@ sub loadModel my ($sourcename,$propname) = ($1,$2); my $value = ($proppath eq "node.nodeModel"? - $model : ($sourcename eq "config"? $C : $self->{info}->{system} )->{$propname}); + $shortname : ($sourcename eq "config"? $C : $self->{info}->{system} )->{$propname}); $value = '' if (!defined($value)); # choices can be: regex, or fixed string, or array of fixed strings @@ -1244,7 +1354,7 @@ sub loadModel } else { - db("ERROR, ignoring policy $polnr with invalid property path \"$proppath\""); + dbg("ERROR, ignoring policy $polnr with invalid property path \"$proppath\""); $rulematches = 0; } next NEXTRULE if (!$rulematches); # all IF clauses must match @@ -1745,6 +1855,12 @@ sub writeNodeInfo delete $self->{info}{view_system}; delete $self->{info}{view_interface}; + # add legacy compat info, for opCharts which accesses + # lastUpdatePoll (which is set on type=update!) and lastUpdateSec (which is set on type=poll!) + # note that there's also the independent lastCollectPoll, but that is not set/used everywhere. + $self->{info}->{system}->{lastUpdateSec} = $self->{info}->{system}->{last_poll}; + $self->{info}->{system}->{lastUpdatePoll} = $self->{info}->{system}->{last_update}; + my $ext = getExtension(dir=>'var'); my $name = ($self->{node} ne "") ? "$self->{node}-node" : 'nmis-system'; diff --git a/lib/WMI.pm b/lib/WMI.pm index a54bc90..377eb63 100644 --- a/lib/WMI.pm +++ b/lib/WMI.pm @@ -197,7 +197,7 @@ sub _run_query { # don't want the wmic process to hang around, we stopped consuming its output # and it can't do anything useful anymore - kill("SIGKILL",$pid); + kill("KILL",$pid); unlink($tfn); return (error => "timeout after $timeout seconds"); } diff --git a/lib/func.pm b/lib/func.pm index d9d6fd4..ee71c4a 100644 --- a/lib/func.pm +++ b/lib/func.pm @@ -27,7 +27,7 @@ # # ***************************************************************************** package func; -our $VERSION = "1.5.3"; +our $VERSION = "1.6.0"; use strict; use Fcntl qw(:DEFAULT :flock :mode); @@ -35,10 +35,12 @@ use FindBin; # bsts; normally loaded by the caller use File::Path; use File::stat; use File::Spec; -use Time::ParseDate; # fixme: actually NOT used by func use Time::Local; +use Time::ParseDate; +use Time::Moment; use POSIX qw(); # we want just strftime use Cwd qw(); +use List::Util 1.33; # older versions have no usable any() use version 0.77; use JSON::XS; @@ -107,7 +109,6 @@ use Exporter; writeHashtoFile readFiletoHash - htmlElementValues logMsg logAuth2 logAuth @@ -137,10 +138,10 @@ use Exporter; # cache table -my $C_cache = undef; # configuration table cache +my $C_cache = undef; # that's the configuration structure cache my $C_modtime = undef; my %Table_cache; -my $Config_cache = undef; +my $Config_cache = undef; # that's the configuration name, e.g. Config. my $confdebug = 0; my $nmis_var; my $nmis_conf; @@ -148,7 +149,6 @@ my $nmis_models; my $nmis_logs; my $nmis_log; my $nmis_mibs; -my @htmlElements; # preset kernel name my $kernel = $^O; @@ -213,7 +213,17 @@ sub getCGIForm { sub convertIfName { my $ifName = shift; - $ifName =~ s/\W+/-/g; + + # new configuration option to handle customers with conflicting names, e.g. Gig0/0.1 and Gig0/0/1 + my $preserve_dot_in_ifdescr = getbool($C_cache->{preserve_dot_in_ifdescr}); + + if ( $preserve_dot_in_ifdescr ) { + $ifName =~ s/[^A-Za-z0-9_.]+/-/g; + } + else { + $ifName =~ s/\W+/-/g; + } + $ifName =~ s/\-$//g; $ifName = lc($ifName); return $ifName @@ -353,7 +363,7 @@ sub get_localtime my ($time) = @_; $time ||= time; - return POSIX::strftime("%a %b %H:%M:%S %Y %Z", localtime($time)); + return POSIX::strftime("%a %b %d %H:%M:%S %Y %Z", localtime($time)); } sub convertMonth { @@ -952,6 +962,8 @@ sub mtimeFile { # loadTable(dir=>'xx',name=>'yy',mtime=>'true') returns pointer of table and mtime of file # loadTable(dir=>'xx',name=>'yy',lock=>'true') returns pointer of table and handle without caching # extra argument: suppress_errors, if set loadTable will not log errors but just return +# +# attention: loadtable does NOT support create-on-the-fly with lock, even though readfiletohash does! sub loadTable { my %args = @_; my $dir = lc $args{dir}; # name of directory @@ -1168,8 +1180,12 @@ sub writeHashtoFile { } -### read file with lock containing data generated by Data::Dumper, option = lock -# this reads both json and nmis files. +### read file with lock containing data generated by Data::Dumper, +# this reads both json and nmis files +# args: file, lock, json +# returns: nothing, or hashref or (hashref,handle) +# note: if lock is true, a missing file is created on the fly! +# # fixme: passing json=false DOES NOT WORK if the config says use_json=true sub readFiletoHash { my %args = @_; @@ -1363,29 +1379,6 @@ sub dbgPolling { } } -# do nothing.. -sub htmlElementValues{}; - -# my %args = @_; -# my $element = $args{element}; -# my $value = $args{value}; -# my $script = $args{script}; -# -# if ($script ne '') { -# push @htmlElements,$script; -# } elsif ($element ne '') { -# push @htmlElements,"document.getElementById(\"$element\").innerHTML=\"$value\""; -# } else { -# print ""; -# @htmlElements = (); -# } -# -#} # this function logs to nmis_log in a safe, locked fashion @@ -1797,9 +1790,6 @@ sub loadConfTable { # on start of program parameters are defined return $C_cache if defined $C_cache and scalar @_ == 0; - # add extension if missing - $conf = $conf =~ /\./ ? $conf : "${conf}"; - if (($configfile=getConfFileName(conf=>$conf, dir=>$dir))) { # check if config file is updated, if not, use file cache @@ -1909,19 +1899,21 @@ sub writeConfData { # creates the dir in question, and all missing intermediate -# directories in the path. -sub createDir { - my $dir = shift; +# directories in the path; then fixes the ownership and permissions +# up to nmis base +sub createDir +{ + my ($dir) = @_; my $C = loadConfTable(); - if ( not -d $dir ) { - my $permission = "0770"; # default - if ( $C->{'os_execperm'} ne "" ) { - $permission = $C->{'os_execperm'} ; - } + + if ( not -d $dir ) + { + my $permission = $C->{'os_execperm'} || "0770"; my $umask = umask(0); mkpath($dir,{verbose => 0, mode => oct($permission)}); umask($umask); + setFileProtParents($dir); } } @@ -2213,11 +2205,12 @@ sub hexval { ### 2012-09-21 keiths, fixing up so NAN is not black. sub colorPercentHi { - use List::Util qw[min max]; my $val = shift; if ( $val =~ /^(\d+|\d+\.\d+)$/ ) { $val = 100 - int($val); - return '#' . hexval( int(min($val*2*2.55,255)) ) . hexval( int(min( (100-$val)*2*2.55,255)) ) .'00' ; + return '#' + . hexval( int(List::Util::min($val*2*2.55,255)) ) + . hexval( int(List::Util::min( (100-$val)*2*2.55,255)) ) .'00' ; } else { return '#AAAAAA'; @@ -2226,11 +2219,12 @@ sub colorPercentHi { ### 2012-09-21 keiths, fixing up so NAN is not black. sub colorPercentLo { - use List::Util qw[min max]; my $val = shift; if ( $val =~ /^(\d+|\d+\.\d+)$/ ) { $val = int($val); - return '#' . hexval( int(min($val*2*2.55,255)) ) . hexval( int(min( (100-$val)*2*2.55,255)) ) .'00' ; + return '#' + . hexval( int(List::Util::min($val*2*2.55,255)) ) + . hexval( int(List::Util::min( (100-$val)*2*2.55,255)) ) .'00' ; } else { return '#AAAAAA'; @@ -2483,7 +2477,7 @@ sub selftest } push @details, ["CRON daemon",$cron_status]; - if ($config->{daemon_fping_active} && !$fpingd_found) + if (getbool($config->{daemon_fping_active}) && !$fpingd_found) { $fpingd_status = "No ".$config->{daemon_fping_filename}." daemon seems to be running!"; $allok=0; @@ -2699,7 +2693,7 @@ sub update_operations_stamp # with type given, collects the processes that run that cmd AND have the same config # without type, collects ALL procs running perl and called nmis-something-... or nmis.pl, # NOT just the ones with this config! -# returns: hashref of pid -> info about the process, namely $0/cmdline and starttime +# returns: hashref of pid -> info about the process, namely $0/cmdline and starttime, possibly node sub find_nmis_processes { my (%args) = @_; @@ -2823,6 +2817,10 @@ sub getFilePollLock { my $node = $args{node}; my $C = loadConfTable(); + # conf should be short config name, without suffix, + # but the cgi scripts end up having 'Config.nmis' and the like. + $conf =~ s/\.nmis$//i; + my $lockFile = $C->{''}."/".lc($node)."-$conf-$type.lock"; return($lockFile); @@ -2910,4 +2908,160 @@ sub releasePollLock { return 0; } +# takes anything that time::parsedate understands, plus an optional timezone argument +# and returns full seconds (ie. unix epoch seconds in utc) +# +# if no timezone is given, the local timezone is used. +# attention: parsedate by itself does NOT understand the iso8601 format with timezone Z or +# with negative offset; relative time specs also don't work well with timezones OR dst changes! +# +# az recommends using parseDateTime || getUnixTime for max compat. +sub getUnixTime +{ + my ($timestring, $tzdef) = @_; + + # to make the tz-dependent stuff work, we MUST give parsedate a tz spec... + # - but we don't know the applicable offset until after we've parsed the + # time (== catch 22 when dst is involved) + # - and parsedate doesn't understand most timezone names, so we must compute a numeric offset...fpos. + # (== catch 22^2) + # - plus trying to fix in postprocessing with shift FAILS if the time was a relative one (e.g. now), + # and parsedate doesn't tell us whether the time in question was relative or absolute. fpos^2. + # + # best effort: take the current time's offset, hope it's applicable to the actual time in question + + my $tz = DateTime::TimeZone->new(name => 'local'); + + my $tmobj = Time::Moment->now_utc; # don't do any local timezone stuff + my $tzoffset = $tz->offset_for_datetime($tmobj); # in seconds + # want [+-]HHMM + my $tzspec = sprintf("%s%02u%02u", ($tzoffset < 0? "-":"+"), + (($tzoffset < 0? -$tzoffset: $tzoffset)/3600), + ($tzoffset%3600)/60); + + my $epochseconds = parsedate($timestring, ZONE => $tzspec); + return $epochseconds; +} + +# convert an iso8601/rfc3339 time into (fractional!) unix epoch seconds +# returns undef if the input string is invalid +# note: timezone suffixes ARE parsed and taken into account! +# if no tz suffix is present, use the local timezone +sub parseDateTime +{ + my ($dtstring) = @_; + # YYYY-MM-DDTHH:MM:SS.SSS, millis are optional + # also allowed: timezone suffixes Z, +NN, -NN, +NNMM, -NNMM, +NN:MM, -NN:MM + + # meh: time::moment strictly REQUIRES tz - just constructing with from_string() + # fails on implicit local zone (and is likely more expensive even with fixup work, as lenient is + # required because the damn thing otherwise refuses +NNMM as that has no ":"... + if ($dtstring =~ /^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?(Z|([\+-])(\d{2})\:?(\d{2})?)?/) + { + my $eleven = $11 // "00"; # datetime wants offsets as +-HHMM, nost just +-HH + my $tzn = (defined($8)? $8 eq "Z"? $8 : $9.$10.$eleven : undef); + my $tz = DateTime::TimeZone->new(name => $tzn // "local"); + + # oh the convolutions...make obj w/o tz, then figure out offset for THAT time, + # then apply the offset. meh. + my $when = Time::Moment->new(year => $1, month => $2, day => $3, + hour => $4, minute => $5, second => $6, + nanosecond => (defined $7? $7 * 1e9: 0)); + my $tzoffset = $tz->offset_for_datetime($when) / 60; + + my $inthezone = $when->with_offset_same_local($tzoffset); + return $inthezone->epoch + $inthezone->nanosecond / 1e9; + } + else + { + return undef; + } +} + +# small helper to handle X.Y.Z or X.N.M indirection into a deep structure +# takes anchor of structure, follows X.Y.Z or X.N.M or X.-N.M indirections +# +# args: structure (ref), path (string) +# returns: value (or undef), error: undef/0 for ok, 1 for nonexistent key/index, +# 2 for type mismatch (eg. hash expected but scalar or array observed) +sub follow_dotted +{ + my ($anchor, $path) = @_; + my ($error, $value); + + for my $indirection (split(/\./, $path)) + { + if (ref($anchor) eq "ARRAY" and $indirection =~ /^-?\d+$/) + { + if (!exists $anchor->[$indirection]) + { + return (undef, 1); + } + else + { + $anchor = $anchor->[$indirection]; + } + } + elsif (ref($anchor) eq "HASH") + { + if (!exists $anchor->{$indirection}) + { + return (undef, 1); + } + else + { + $anchor = $anchor->{$indirection}; + } + } + else + { + return (undef, 2); # type mismatch + } + } + $value = $anchor; + return ($value, 0); +} + +# append activity audit information to the one textual audit.log +# expects that the configuration has been loaded with loadConfTable! +# +# args: when (=unix ts), who (=user), +# what (=operation), where (=context), how (=success/failure/warning,info), details +# all required except when and details; all freeform except when, +# which must be numeric (but may be fractional) +# +# returns undef if ok, error otherwise +sub audit_log +{ + my (%args) = @_; + return "no config loaded, cannot determine log directory!" if (!$C_cache); + + for my $musthave (qw(who what where how)) + { + return "Missing argument \"$musthave\"!" if (!$args{$musthave}); + } + $args{details} ||= 'N/A'; + + my $auditlogfile = $C_cache->{''}."/audit.log"; + + # format is tab-delimited, any tabs in input are removed + # order: ts, who, what, where, how, details + my @output = ( returnDateStamp($args{when}||time), + map { s/\t+//g; $_ } (@args{qw(who what where how details)}) ); + + open(F, ">>$auditlogfile") or return "cannot open $auditlogfile for writing: $!"; + flock(F, LOCK_EX) or return "cannot lock $auditlogfile: $!"; + # add helpful header if file was empty + print F "# when\t\t\twho\twhat\twhere\thow\tdetails\n" if (! -s $auditlogfile); + + print F join("\t", @output),"\n"; + close(F); + + # fixme should handle errors at some point... + my $res = setFileProtDiag(file => $auditlogfile); + + return undef; +} + + 1; diff --git a/lib/opmantek_rrdfunc.pm b/lib/opmantek_rrdfunc.pm deleted file mode 100644 index c372ce3..0000000 --- a/lib/opmantek_rrdfunc.pm +++ /dev/null @@ -1,301 +0,0 @@ -# -## $Id: rrdfunc.pm,v 8.6 2012/04/28 00:59:36 keiths Exp $ -# -# Copyright (C) Opmantek Limited (www.opmantek.com) -# -# ALL CODE MODIFICATIONS MUST BE SENT TO CODE@OPMANTEK.COM -# -# This file is part of Network Management Information System (“NMIS”). -# -# NMIS is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# NMIS 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with NMIS (most likely in a file named LICENSE). -# If not, see -# -# For further information on NMIS or for a license other than GPL please see -# www.opmantek.com or email contact@opmantek.com -# -# User group details: -# http://support.opmantek.com/users/ -# -# ***************************************************************************** - -package opmantek_rrdfunc; - -use NMIS::uselib; -use lib "$NMIS::uselib::rrdtool_lib"; - -require 5; - -use strict; - -use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION); - -use Exporter; - -use RRDs 1.000.490; # from Tobias -use Statistics::Lite qw(min max range sum count mean median mode variance stddev statshash statsinfo); -use func; -use Sys; -use Data::Dumper; -#$Data::Dumper::Ident=1; -#$Data::Dumper::SortKeys=1; - -$VERSION = 2.0.1; - -@ISA = qw(Exporter); - -@EXPORT = qw( - rrdFetchGraphPData - ); - -sub error { - print "Content-type: text/html\n\n"; - # print start_html(); - print "Network: ERROR on getting graph
\n"; - print "Request not found\n"; - # print end_html; -} - -sub rrdFetchGraphPData { - my %args = @_; - - # print "rrdFetchGraphPData: \n".Dumper(\%args); - # Break the query up for the names - my $type = $args{obj}; - my $nodename = $args{node}; - my $debug = $args{debug}; - my $grp = $args{group}; - my $graphtype = $args{graphtype}; - my $graphstart = $args{graphstart}; - my $width = $args{width}; - my $height = $args{height}; - my $start = $args{start}; - my $end = $args{end}; - my $intf = $args{intf}; - my $item = $args{item}; - my $filename = $args{filename}; - - my $C = $args{C}; - # if( !defined($C) ) { - # if (!($C = loadConfTable(conf=>$args{conf},debug=>$debug))) { exit 1; }; - # } - - my $S = Sys::->new; # get system object - $S->init(name=>$nodename,snmp=>'false'); - my $NI = $S->ndinfo; - my $IF = $S->ifinfo; - - ### 2012-02-06 keiths, handling default graph length - # default is hours! - my $graphlength = $C->{graph_amount}; - if ( $C->{graph_unit} eq "days" ) { - $graphlength = $C->{graph_amount} * 24; - } - - if ( $start eq "" or $start == 0) { - $start = time() - ($graphlength*3600); - } - if ( $end eq "" or $end == 0) { - $end = time(); - } - - my $ERROR; - my $graphret; - my $xs; - my $ys; - my @options; - my @opt; - my $db; - - my ($r_start,$r_end,$r_width,$types,$ds_names,$legend,$colours,$pdata,$chart_options); - my ($hash_data,$hash_head); - - if ($graphtype eq 'metrics') { - $item = $args{group}; - $intf = ""; - } - - - if ($graphtype =~ /cbqos/) { - @opt = graphCBQoS(sys=>$S,graphtype=>$graphtype,intf=>$intf,item=>$item,start=>$start,end=>$end,width=>$width,height=>$height); - } elsif ($graphtype eq "calls") { - @opt = graphCalls(sys=>$S,graphtype=>$graphtype,intf=>$intf,item=>$item,start=>$start,end=>$end,width=>$width,height=>$height); - } else { - - my $getDBName =$S->getDBName(graphtype=>$graphtype,index=>$intf,item=>$item); - # print "getDBName = ".Dumper($getDBName); - if (!($db = $S->getDBName(graphtype=>$graphtype,index=>$intf,item=>$item)) ) { # get database name from node info - error(); - return 0; - } - - my $graph; - if (!($graph = loadTable(dir=>'models',name=>"Graph-$graphtype"))) { - logMsg("ERROR reading Graph-$graphtype"); - error(); - return 0; - } - - my $title = 'standard'; - my $vlabel = 'standard'; - my $size = 'standard'; - my $ttl; - my $lbl; - - $title = 'short' if $width <= 400 and $graph->{title}{short} ne ""; - - $vlabel = 'short' if $width <= 400 and $graph->{vlabel}{short} ne ""; - - $vlabel = 'split' if getbool($C->{graph_split}) and $graph->{vlabel}{split} ne ""; - - $size = 'small' if $width <= 400 and $graph->{option}{small} ne ""; - - - if (($ttl = $graph->{title}{$title}) eq "") { - logMsg("no title->$title found in Graph-$graphtype"); - } - if (($lbl = $graph->{vlabel}{$vlabel}) eq "") { - logMsg("no vlabel->$vlabel found in Graph-$graphtype"); - } - - @opt = ( - "--title", $ttl, - "--vertical-label", $lbl, - "--start", $start, - "--end", $end, - "--width", $width, - "--height", $height, - "--imgformat", "PNG", - "--interlaced", - "--disable-rrdtool-tag", - "--color", 'BACK#ffffff', # Background Color - "--color", 'SHADEA#ffffff', # Left and Top Border Color - "--color", 'SHADEB#ffffff', # was CFCFCF - "--color", 'CANVAS#FFFFFF', # Canvas (Grid Background) - "--color", 'GRID#E2E2E2', # Grid Line ColorGRID#808020' - "--color", 'MGRID#EBBBBB', # Major Grid Line ColorMGRID#80c080 - "--color", 'FONT#222222', # Font Color - "--color", 'ARROW#924040', # Arrow Color for X/Y Axis - "--color", 'FRAME#808080' # Canvas Frame Color - ); - - if ($width > 400) { - push(@opt,"--font", $C->{graph_default_font_standard}) if $C->{graph_default_font_standard}; - } - else { - push(@opt,"--font", $C->{graph_default_font_small}) if $C->{graph_default_font_small}; - } - - # add option rules - foreach my $str (@{$graph->{option}{$size}}) { - push @opt, $str; - } - } - - # define length of graph - my $l; - if (($end - $start) < 3600) { - $l = int(($end - $start) / 60) . " minutes"; - } elsif (($end - $start) < (3600*48)) { - $l = int(($end - $start) / (3600)) . " hours"; - } else { - $l = int(($end - $start) / (3600*24)) . " days"; - } - - { - # scalars must be global - no strict; - if ($intf ne "") { - $indx = $intf; - $ifDescr = $IF->{$intf}{ifDescr}; - $ifSpeed = $IF->{$intf}{ifSpeed}; - $ifSpeedIn = $IF->{$intf}{ifSpeed}; - $ifSpeedOut = $IF->{$intf}{ifSpeed}; - $ifSpeedIn = $IF->{$intf}{ifSpeedIn} if $IF->{$intf}{ifSpeedIn}; - $ifSpeedOut = $IF->{$intf}{ifSpeedOut} if $IF->{$intf}{ifSpeedOut}; - if ($ifSpeed eq "auto" ) { - $ifSpeed = 10000000; - } - - if ( $IF->{$intf}{ifSpeedIn} and $IF->{$intf}{ifSpeedOut} ) { - $speed = "IN\\: ". convertIfSpeed($ifSpeedIn) ." OUT\\: ". convertIfSpeed($ifSpeedOut); - } - else { - $speed = convertIfSpeed($ifSpeed); - } - } - $node = $NI->{system}{name}; - $datestamp_start = returnDateStamp($start); - $datestamp_end = returnDateStamp($end); - $datestamp = returnDateStamp(time); - $database = $db; - $group = $grp; - $itm = $item; - $length = $l; - $split = getbool($C->{graph_split}) ? -1 : 1 ; - $GLINE = getbool($C->{graph_split}) ? "AREA" : "LINE1" ; - $weight = 0.983; - - foreach my $str (@opt) { - $str =~ s{\$(\w+)}{if(defined${$1}){${$1};}else{"ERROR, no variable \'\$$1\' ";}}egx; - if ($str =~ /ERROR/) { - logMsg("ERROR in expanding variables, $str"); - return; - } - push @options,$str; - } - } - - # Do the graph! - ### 2012-01-30 keiths, deprecating the need for Win32 support and Image::Resize. But leaving there for PersistentPerl (to be sure). - # This works around a bug in RRDTool which doesn't like writing to STDOUT on Win32! - # Also PersistentPerl needs this workaround - if ( $^O eq "MSWin32" ) { # or (eval {require PersistentPerl} && PersistentPerl->i_am_perperl) ) { - my $buff; - my $random = int(rand(1000)) + 25; - my $tmpimg = "$C->{''}/rrdDraw-$random.png"; - - print "Content-type: image/png\n\n"; - # ($begin,$step,$names,$data) = RRDs::graphfetch($tmpimg, @options); - if ( -f $tmpimg ) { - - open(IMG,"$tmpimg") or logMsg("$NI->{system}{name}, ERROR: problem with $tmpimg; $!"); - binmode(IMG); - binmode(STDOUT); - while (read(IMG, $buff, 8 * 2**10)) { - print STDOUT $buff; - } - close(IMG); - unlink($tmpimg) or logMsg("$NI->{system}{name}, Can't delete $tmpimg: $!"); - } - } else { - # print "Getting graphfetch, options=\n".Dumper(\@options); - # ($begin,$step,$types,$name,$data) = RRDs::graphfetch('-', @options); - my $begin; - my $end; - ($r_start,$r_end,$r_width,$types,$ds_names,$legend,$colours,$pdata,$chart_options) = RRDs::fetch_graph_pdata('-', @options); - # print STDERR "Graphfetch returned start=$r_start,end=$r_end,width=$r_width\n"; - # print STDERR "Graphfetch returned begin=$begin, width=$width,\n name=".Dumper($name)."\n data=".Dumper($pdata)."\n"; - - if ($ERROR = RRDs::error) { - logMsg("$db Graphing Error for $graphtype: $ERROR"); - - } else { - #return "GIF Size: ${xs}x${ys}\n"; - #print "Graph Return:\n",(join "\n", @$graphret),"\n\n"; - } - } - return ($r_start,$r_end,$r_width,$types,$ds_names,$legend,$colours,$pdata,$chart_options); -} - -1; diff --git a/lib/ping.pm b/lib/ping.pm index 2ae6738..505dcf2 100644 --- a/lib/ping.pm +++ b/lib/ping.pm @@ -120,7 +120,7 @@ sub ext_ping { die unless $@ eq "alarm\n"; # propagate unexpected errors # timed out: kill child - kill $pid; + kill('TERM',$pid); close(PING); # ... and set return values to dead values diff --git a/lib/rrdfunc.pm b/lib/rrdfunc.pm index 937bf91..75409f5 100644 --- a/lib/rrdfunc.pm +++ b/lib/rrdfunc.pm @@ -27,7 +27,7 @@ # # ***************************************************************************** package rrdfunc; -our $VERSION = "2.3.0"; +our $VERSION = "2.4.1"; use NMIS::uselib; use lib "$NMIS::uselib::rrdtool_lib"; @@ -38,7 +38,7 @@ use vars qw(@ISA @EXPORT); use Exporter; -use RRDs 1.000.490; # from Tobias +use RRDs 1.000.490; use Statistics::Lite; use POSIX qw(); # for strftime @@ -80,6 +80,8 @@ sub getUpdateStats # returns: hash of time->dsname=value, list(ref) of dsnames (plus 'time', 'date'), and meta data hash # metadata hash: actual begin and end as per rrd, and step # +# args: sys, graphtype, mode (all required), +# index or item (synthesised from one another), # optional: hours_from and hours_to (default: no restriction) sub getRRDasHash { @@ -98,10 +100,8 @@ sub getRRDasHash $S = Sys::->new(); # get base Model containing database info $S->init; } - - # fixme: longterm/lowprio, maybe add a type parameter that's not translated, and have the caller take care of it? - my $section = $S->getTypeName(graphtype=>$graphtype, index=> (defined $index? $index : $item)); - my $db = getFileName(sys=>$S, type=>(defined $section? $section : $graphtype), index=>$index, item=>$item); + # let sys reason through graphtype/sections, and index vs item + my $db = $S->getDBName(graphtype=>$graphtype, index=>$index, item=>$item); my ($begin,$step,$name,$data) = RRDs::fetch($db, $args{mode},"--start",$args{start},"--end",$args{end}); my %s; @@ -158,7 +158,11 @@ sub getRRDasHash # retrieves rrd data and computes a number of descriptive stats # this uses the sys object to translate from graphtype to section (Sys::getTypeName) -# args: hour_from hour_to define the daily period [from,to]. +# +# args: sys, graphtype (required), +# index or item (synthesisted from each other), +# hour_from hour_to define the daily period [from,to]. +# # if from > to then the meaning is inverted and data OUTSIDE the [to,from] interval is returned # for midnight use either 0 or 24, depending on whether you want the inside or outside interval # @@ -188,9 +192,8 @@ sub getRRDStats $S->init; } - # fixme: longterm/lowprio, maybe add a type parameter that's not translated, and have the caller take care of it? - my $section = $S->getTypeName(graphtype=>$graphtype, index=> (defined $index? $index : $item)); - my $db = getFileName(sys=>$S, type=>(defined $section? $section : $graphtype), index=>$index, item=>$item); + # let sys reason through graphtype/sections, and index vs item + my $db = $S->getDBName(graphtype=>$graphtype, index=>$index, item=>$item); if ( ! defined $args{mode} ) { $args{mode} = "AVERAGE"; } if ( -r $db ) { @@ -474,6 +477,10 @@ sub getFileName # arsg: sys, data (absolutely required), type/index/item (more or less required), extras (optional), # database (optional, if set overrides the internal file naming logic) # +# if node has marker node_was_reset or outage_nostats, then inbound +# data is IGNORED and 'U' is written instead +# (except for type "health", DS "outage", "polltime" and "updatetime", which are always let through) +# # returns: the database file name; sets the internal error indicator sub updateRRD { @@ -482,10 +489,10 @@ sub updateRRD my ($S,$data,$type,$index,$item,$database,$extras) = @args{"sys","data","type","index","item","database","extras"}; - my $NI = $S->{info}; - my $IF = $S->{intf}; + my $NI = $S->ndinfo; ++ $stats{nodes}->{$S->{name}}; + dbg("Starting RRD Update Process, type=$type, index=$index, item=$item"); # use heuristic or given database? @@ -541,7 +548,9 @@ sub updateRRD my (@options, @ds); my @values = ("N"); # that's 'reading is for Now' - dbg("node was reset, inserting U values") if ($NI->{system}->{node_was_reset}); + # if the node has gone through a reset, then insert a U to avoid spikes - but log once only + dbg("node was reset, inserting U values") if ($NI->{admin}->{node_was_reset}); + dbg("node has current outage with nostats option, inserting U values") if ($NI->{admin}->{outage_nostats}); foreach my $var (keys %{$data}) { # handle the nosave option @@ -552,8 +561,10 @@ sub updateRRD } push @ds, $var; - # if the node has gone through a reset, then insert a U to avoid spikes - but log once only - if ($NI->{system}->{node_was_reset}) + + # type health, ds outage, polltime, updatetime: are never overridden + if ( ($NI->{admin}->{node_was_reset} or $NI->{admin}->{outage_nostats}) + and ($type ne "health" or $var !~ /^(outage|polltime|updatetime)$/)) { push @values, 'U'; } @@ -638,38 +649,42 @@ sub updateRRD dbg("Finished"); } # end updateRRD -# -# define the DataSource configuration for RRD -# +# the optionsRRD function creates the configuration options +# for creating an rrd file. +# args: sys, data, type (all pretty much required), +# index (optional, for string expansion) +# returns: array of rrdcreate parameters; updates global %stats sub optionsRRD { my %args = @_; - my $S = $args{sys}; # optional,needed for parsing range + + my $S = $args{sys}; my $data = $args{data}; my $type = $args{type}; my $index = $args{index}; # optional - my $time = 30*int(time/30); - my $START = $time; - my @options; - - dbg("type $type"); + dbg("type $type, index $index"); + undef $stats{error}; - my $M; - if ($S eq "") { - $M = Sys::->new(); # load base model - $M->init; - } else { - $M = $S; + if (ref($S) ne "Sys") + { + $S = Sys->new; # create generic object with base model info + $S->init; } + my $mdlinfo = $S->mdl; - # rrd step, from model database or our standard 5min - my $RRD_poll = $M->{mdl}{database}{db}{poll} || 300; - # rrd heartbeat, either from model database or 3 times the step - # note: overridable by passing in 'heartbeat' in data! - my $RRD_hbeat = $M->{mdl}{database}{db}{hbeat} || ($RRD_poll*3); + # find out rrd step and heartbeat values, possibly use type-specific values (which the polling policy would supply) + my $timinginfo = (ref($mdlinfo->{database}) eq "HASH" + && ref($mdlinfo->{database}->{db}) eq "HASH" + && ref($mdlinfo->{database}->{db}->{timing}) eq "HASH")? + $mdlinfo->{database}->{db}->{timing}->{$type} // $mdlinfo->{database}->{db}->{timing}->{"default"} : undef; + $timinginfo //= { heartbeat => 900, poll => 300 }; + # note: heartbeat is overridable per DS by passing in 'heartbeat' in data! + dbg("timing options for this file of type $type: step $timinginfo->{poll}, heartbeat $timinginfo->{heartbeat}"); - @options = ("-b", $START, "-s", $RRD_poll); + # align the start time with the step interval, but reduce by one interval so that we can send data immediately + my $starttime = time - (time % $timinginfo->{poll}) - $timinginfo->{poll}; + my @options = ("-b", $starttime, "-s", $timinginfo->{poll}); # $data{ds_name}{value} contains the values # $data{ds_name}{option} contains the info for creating the dds, format is "source,low:high,heartbeat" @@ -677,7 +692,7 @@ sub optionsRRD # is for overriding the rrdfile-level heartbeat. range and heartbeat are optional, the ',' are clearly needed # even if you skip range but provide heartbeat. # - # default is GAUGE,"U:U",standard heartbeat + # default is GAUGE,"U:U", and the standard heartbeat foreach my $id (sort keys %{$data}) { if (length($id) > 19) @@ -698,51 +713,35 @@ sub optionsRRD ($source,$range,$heartbeat) = split (/\,/,$data->{$id}{option}); - # no CVARs as no section given + # no CVARs possible as no section given $range = $S->parseString(string=>$range, type=>$type, index=>$index) if $S ne ""; $source = uc $source; } $source ||= "GAUGE"; $range ||= "U:U"; - $heartbeat ||= $RRD_hbeat; + $heartbeat ||= $timinginfo->{heartbeat}; dbg("ID of data is $id, source $source, range $range, heartbeat $heartbeat",2); push @options,"DS:$id:$source:$heartbeat:$range"; } - my $DB; - if (exists $M->{mdl}{database}{db}{size}{$type}) { - $DB = $M->{mdl}{database}{db}{size}{$type}; - } elsif (exists $M->{mdl}{database}{db}{size}{default}) { - $DB = $M->{mdl}{database}{db}{size}{default}; - dbg("INFO, using database format \'default\'"); - } + # now figure out the consolidation parameters, again possibly type-specific plus fallback + my $sizeinfo = (ref($mdlinfo->{database}) eq "HASH" + && ref($mdlinfo->{database}->{db}) eq "HASH" + && ref($mdlinfo->{database}->{db}->{size}) eq "HASH")? + $mdlinfo->{database}->{db}->{size}->{$type} // $mdlinfo->{database}->{db}->{size}->{"default"} : undef; + $sizeinfo //= { step_day => 1, step_week => 6, step_month => 24, step_year => 288, + rows_day => 2304, rows_week => 1536, rows_month => 2268, rows_year => 1890 }; - if ($DB eq "") - { - dbg("ERROR ($S->{name}) database format for type=$type not found"); - $stats{error} = "($S->{name}) database format for type=$type not found"; - } - else + for my $period (qw(day week month year)) { - push @options,"RRA:AVERAGE:0.5:$DB->{step_day}:$DB->{rows_day}"; - push @options,"RRA:AVERAGE:0.5:$DB->{step_week}:$DB->{rows_week}"; - push @options,"RRA:AVERAGE:0.5:$DB->{step_month}:$DB->{rows_month}"; - push @options,"RRA:AVERAGE:0.5:$DB->{step_year}:$DB->{rows_year}"; - push @options,"RRA:MAX:0.5:$DB->{step_day}:$DB->{rows_day}"; - push @options,"RRA:MAX:0.5:$DB->{step_week}:$DB->{rows_week}"; - push @options,"RRA:MAX:0.5:$DB->{step_month}:$DB->{rows_month}"; - push @options,"RRA:MAX:0.5:$DB->{step_year}:$DB->{rows_year}"; - push @options,"RRA:MIN:0.5:$DB->{step_day}:$DB->{rows_day}"; - push @options,"RRA:MIN:0.5:$DB->{step_week}:$DB->{rows_week}"; - push @options,"RRA:MIN:0.5:$DB->{step_month}:$DB->{rows_month}"; - push @options,"RRA:MIN:0.5:$DB->{step_year}:$DB->{rows_year}"; - - return @options; + for my $rra (qw(AVERAGE MIN MAX)) + { + push @options, join(":", "RRA", $rra, 0.5, $sizeinfo->{"step_$period"}, $sizeinfo->{"rows_$period"}); + } } - return; -} # end optionsRRD - + return @options; +} ### createRRRDB now checks if RRD exists and only creates if doesn't exist. ### also add node directory create for node directories, if rrd is not found @@ -836,7 +835,6 @@ sub createRRD if ( -f $database and -r $database and -w $database ) { logMsg("INFO ($S->{name}) created RRD $database"); - sleep 1; # wait at least 1 sec to avoid rrd 1 sec step errors as next call is RRDBupdate } else { diff --git a/lib/sapi.pm b/lib/sapi.pm index 6e95e3a..2534272 100644 --- a/lib/sapi.pm +++ b/lib/sapi.pm @@ -27,7 +27,7 @@ # # ***************************************************************************** package sapi; -our $VERSION = "2.0.0"; +our $VERSION = "2.1.0"; use strict; use vars qw(@ISA @EXPORT); @@ -65,7 +65,6 @@ sub sapi_send { my $SH = shift; my $msg = shift; - my $nonewline = shift; defined send($SH,$msg,0) or return 0,$!; return 1,undef; @@ -127,8 +126,13 @@ sub sapi last; } - if ($type eq "send") { # Send a string - ($ok,$errmsg) = sapi_send($SH,$str."\n"); + if ($type eq "send") # Send a string + { + # if the string contains none of the typical line terminator escapes (\r or \n) + # and none of the generic \x{hex} escapes, fall back to \n + $str .= "\n" + if (!($str =~ s/(\\[rn]|\\x\{\w+\})/qq{"$1"}/gee)); + ($ok,$errmsg) = sapi_send($SH,$str); } elsif ($str eq "EOF") { # Receive data until EOF $eof = 0; @@ -173,8 +177,9 @@ sub sapi } sapi_close($SH); - return 0,$errmsg . "Partial results (if any):$result" if $errmsg; - return 1,$result; + return ($errmsg? + (0, $errmsg . "Partial results (if any): $result") + : ( 1, $result)); } 1; @@ -208,6 +213,9 @@ Scripts are in the form send: more_data_to_send expect: another_string_to_match +If the data_to_send does not contain '\r' or '\n' or other '\x{hex}' +escapes, then a newline character (\n) is appended to the data_to_send. + For example, to see if a HTTP server is responding you might use the following script: @@ -217,12 +225,10 @@ the following script: All expect values are case-insensitive with the exception of a special value: EOF which means to wait until an EOF condition occurs. + For example, to query a HTTP server for all header information you might use the following script: - send: HEAD / HTTP/1.0 - send: + send: HEAD / HTTP/1.0\r\n\r\n expect: EOF -(Note to send a blank line "\n" as in the above example, do not give -send: any value) diff --git a/menu/js/chart.js b/menu/js/chart.js deleted file mode 100644 index 3d94a71..0000000 --- a/menu/js/chart.js +++ /dev/null @@ -1,286 +0,0 @@ -Highcharts.setOptions({ - global: { - useUTC: false - } -}); - -$(function($) { - loadCharts(); -}); - -var loadCharts = function(dialogHandle) { - selector = ".chartDiv" - if( dialogHandle !== undefined ) { - selector = "#"+dialogHandle.attr("id")+" "+selector; - } - $(selector).each( function() { - chartDiv = this; - chartSpan = this.childNodes[0]; - chartUrl = $(this).attr("data-chart-url"); - loadChart(chartUrl, chartSpan, chartDiv); - }); -} - -var unLoadCharts = function(dialogHandle) { - selector = ".chartDiv" - if( dialogHandle !== undefined ) { - selector = "#"+dialogHandle.attr("id")+" "+selector; - } - $(selector).each( function() { - chartDiv = this; - cleanUpChart(chartDiv); - }); -} -function loadChart(url, chartSpan, chartDiv) { - if( url === undefined || url == "" ) { - console.log("url to load chart not defined"); - return; - } - $.ajax({ - url : url, - async : false, - dataType: "json", - type : 'GET', - cache : false, - success : function(data) { - drawChart(data, chartSpan, chartDiv); - }, - error: function (x, e) { - if (x.status == 405 ) { - // this is essentially a made-up status code that tells us to re-autheticate - document.location = document.location.href; - } - } - }); -} - -function drawChart(jsonData, renderTo, div ) -{ - if( jsonData === null ) { - console.log("Error loading data"); - return; - } - - data = jsonData.data; - - for( i = 0; i < data.length; i++ ) { - data.selected = true; - } - - titleText = jsonData.titleText; - titleOnClick = $(div).attr("data-title-onclick"); - if( titleOnClick !== undefined ) { - titleText = "
"+titleText+""; - } - chartWidth = $(div).attr("data-chart-width"); - chartHeight = $(div).attr("data-chart-height"); - - titleStyle = null; - itemStyle = null; - axisStyle = null; - if( chartWidth <= 400 ) { - Highcharts.setOptions({ - chart: { - style: { - fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font - fontSize: '9px' - } - } - }); - titleStyle = { fontSize: '10px' }; - itemStyle = { - cursor: 'pointer', - color: '#274b6d', - fontSize: '9px' - }; - axisStyle = { - color: '#6D869F', - fontWeight: 'bold', - fontSize: '9px' - }; - } - else { - Highcharts.setOptions({ - chart: { - style: { - fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font - fontSize: '12px' - } - } - }); - titleStyle = { - color: '#3E576F', - fontSize: '16px' - }; - itemStyle = { - cursor: 'pointer', - color: '#274b6d', - fontSize: '12px' - }; - axisStyle = { - color: '#6D869F', - fontWeight: 'bold' - }; - } - - cleanUpChart(div); - - graph = new Highcharts.Chart({ - chart: { - renderTo: renderTo, - height: chartHeight, - width: chartWidth, - plotBackgroundColor: null, - plotBorderWidth: null, - plotShadow: false, - zoomType: 'x', - events: { - selection: function(event) { - if ( typeof(graph_point_click_event) != "undefined" ) { - graph_chart_selection(event); - } - } - }, - }, - legend: { - enabled: true, - itemStyle: itemStyle - }, - rangeSelector : { - enabled : false - }, - credits: { - enabled: false - }, - xAxis: { - type: 'datetime', - maxZoom: 60000, - title: { - text: "" - }, - labels : { - style: axisStyle - } - }, - yAxis: - [ - { - title: { text: jsonData.yAxis0TitleText }, - showEmpty: true, - offset: 20, - max: jsonData.max, - min: jsonData.min, - labels : { - style: axisStyle - } - }, - { - title: { text: jsonData.yAxis1TitleText }, - opposite: true, - showEmpty: false, - offset: 20, - } - - ], - plotOptions: { - series: { - showCheckbox: false, - stacking: jsonData.stacking, - marker: { - enabled: false - }, - point: { - events: { - click: function() { - if ( typeof(graph_point_click_event) != "undefined" ) { - graph_point_click_event(this); - } - } - } - } - }, - line : { - lineWidth : 2 - } - }, - title: { - text: titleText, - useHTML: true, - style: titleStyle - }, - // subtitle: { - // text: jsonData.subTitle, - // align: 'right', - // x: -10 - // }, - tooltip: { - valueDecimals: 0, - valueSuffix: jsonData.toolTipSuffix, - shared: true, - - }, - series: data - }); - widthAdjustment = -10; - setChart(renderTo, graph, div, widthAdjustment); - $(div).append("

"+jsonData.subTitle+"

") - return graph; -} - -function setChart(renderTo, graph, div, widthAdjustment) -{ - $(div).data("chart", graph ); - $(graph).data("enclosingDiv", div ); - $(graph).data("renderTo", renderTo ); - $(graph).data("widthAdjustment", widthAdjustment ); -} - -function cleanUpChart(div) -{ - graph = $(div).data("chart"); - if( graph !== undefined ) - { - graph.destroy(); - $(div).data("chart", undefined); - } -} - -$(function($) { - var waitForFinalEvent = (function () { - var timers = {}; - return function (callback, ms, uniqueId) { - if (!uniqueId) { - uniqueId = "Don't call this twice without a uniqueId"; - } - if (timers[uniqueId]) { - clearTimeout (timers[uniqueId]); - } - timers[uniqueId] = setTimeout(callback, ms); - }; - })(); - $(window).resize(function () { - waitForFinalEvent(function(){ - resizeCharts(); - }, 1000, "windowResize"); - }); -}); - -var resizeCharts = function(dialogHandle) { - selector = ".chartDiv" - if( dialogHandle !== undefined ) { - selector = "#"+dialogHandle.attr("id")+" "+selector; - } - $(selector).each( function() { - graph = $(this).data("chart"); - enclosingDiv = $(graph).data("enclosingDiv"); - widthAdjustment = $(graph).data("widthAdjustment"); - resizeGraph( graph, enclosingDiv, widthAdjustment); - }); -} - -function resizeGraph(graph, div, widthAdjustment) -{ - renderTo = $("#"+$(graph).data("renderTo")); - console.log("width: "+$(div).width(), "renderto width:" + renderTo.width()) - graph.setSize( $(div).parent().width() + widthAdjustment, graph.chartHeight ); -} diff --git a/menu/js/commonv8.js b/menu/js/commonv8.js index a6dfacb..5d48476 100644 --- a/menu/js/commonv8.js +++ b/menu/js/commonv8.js @@ -36,6 +36,7 @@ var menu_url_base = '/menu8'; var widget_refresh_glob = 180; var opCharts = false; var config = 'Config'; +var windowObjectReference = {}; // recreate vars that are expected @@ -554,6 +555,14 @@ function createDialog(opt) { // drop refresh timer $.doTimeout( id ); + // We find if this widget had opened a window using viewwnd() + // if it has we delete its window reference. + var hasWindowTitle = objData.options.title.replace(/\W+/g,'_'); + var hasWindow = windowObjectReference[hasWindowTitle]; + if(hasWindow){ + delete windowObjectReference[hasWindowTitle]; + } + // leave our widget attribs on the DOM , so we can reopen, just as it was when we were closed. // set a flag so this dormant state can be found. objData.status = false; @@ -1225,12 +1234,54 @@ function checkBoxes(checkbox,name) { /*=================================================================*/ -function viewwndw(wndw,url,width,height) +// Opens a new window and stores a referance to this window in a top level object +// wndw: (string) window name, used to ID the window for refreshing and keeping track +// url: (string) url for the window to open +// width: (int) width of the window +// height: (int) height of the window +// initLocation: (string) to referance if this function was invoked by the server or client +// returns a window object +function viewwndw(wndw,url,width,height,initLocation) +{ + // Make a clean window name + var windowName = wndw.replace(/\W+/g,'_'); + // Get a widow with using the windows name as a key + var newWindow = windowObjectReference[windowName]; + + //We are checking if there is currently a window + if(newWindow !== undefined){ + // if we have a window and its open we should refresh it + if(!newWindow.closed) + { + newWindow = createNewWindow(windowName, url, width, height); + + // If the window is closed and this function is being called by the client + }else if(newWindow.closed && initLocation !== 'server') + { + newWindow = createNewWindow(windowName, url, width, height); + } + } + else + { //Create a new window and put it into an object using the windows name as a key; + newWindow = createNewWindow(windowName, url, width, height); + windowObjectReference[windowName] = newWindow; + } +}; +// Creates a new windows object +// wndw: (string) window name, used to ID the window for refreshing and keeping track +// url: (string) url for the window to open +// width: (int) width of the window +// height: (int) height of the window +// returns a window object +function createNewWindow(wndw,url,width,height) { var attrib = "scrollbars=yes,resizable=yes,width=" + width + ",height=" + height; - ViewWindow = window.open(url,wndw.replace(/\W+/g,'_'),attrib); - ViewWindow.focus(); + var viewWindow = window.open(url,wndw,attrib); + viewWindow.focus(); + return viewWindow; }; + + function viewdoc(url,width,height) { viewwndw("ViewWindow",url,width,height) diff --git a/menu/js/highcharts.js b/menu/js/highcharts.js deleted file mode 100644 index 327d865..0000000 --- a/menu/js/highcharts.js +++ /dev/null @@ -1,270 +0,0 @@ -/* - Highcharts JS v3.0.1 (2013-04-09) - - (c) 2009-2013 Torstein Hønsi - - License: www.highcharts.com/license -*/ -(function(){function v(a,b){var c;a||(a={});for(c in b)a[c]=b[c];return a}function y(){var a,b=arguments.length,c={},d=function(a,b){var c,h;for(h in b)b.hasOwnProperty(h)&&(c=b[h],typeof a!=="object"&&(a={}),a[h]=c&&typeof c==="object"&&Object.prototype.toString.call(c)!=="[object Array]"&&typeof c.nodeType!=="number"?d(a[h]||{},c):b[h]);return a};for(a=0;a3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+Q(a-c).toFixed(f).slice(2):"")}function ua(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function Ea(a,b){for(var c="{",d=!1,e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h< -i;h++)e=e[g[h]];if(f.length)f=f.join(":"),g=/\.([0-9])/,h=N.lang,i=void 0,/f$/.test(f)?(i=(i=f.match(g))?i[1]:-1,e=Na(e,i,h.decimalPoint,f.indexOf(",")>-1?h.thousandsSep:"")):e=Ua(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function ib(a,b,c,d){var e,c=o(c,1);e=a/c;b||(b=[1,2,2.5,5,10],d&&d.allowDecimals===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=E[jb]&& -(i.setMilliseconds(0),i.setSeconds(j>=E[Va]?0:k*T(i.getSeconds()/k)));if(j>=E[Va])i[Bb](j>=E[Oa]?0:k*T(i[kb]()/k));if(j>=E[Oa])i[Cb](j>=E[oa]?0:k*T(i[lb]()/k));if(j>=E[oa])i[mb](j>=E[Pa]?1:k*T(i[Qa]()/k));j>=E[Pa]&&(i[Db](j>=E[va]?0:k*T(i[Xa]()/k)),h=i[Ya]());j>=E[va]&&(h-=h%k,i[Eb](h));if(j===E[Wa])i[mb](i[Qa]()-i[nb]()+o(d,1));b=1;h=i[Ya]();for(var d=i.getTime(),m=i[Xa](),l=i[Qa](),i=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;dc&&(c=a[b]);return c}function Ga(a, -b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Ra(a){$a||($a=U(wa));a&&$a.appendChild(a);$a.innerHTML=""}function qa(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else O.console&&console.log(c)}function ia(a){return parseFloat(a.toPrecision(14))}function Ha(a,b){xa=o(a,b.animation)}function Hb(){var a=N.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";Za=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,o(c,1),o(g,0),o(h, -0),o(i,0))).getTime()};kb=b+"Minutes";lb=b+"Hours";nb=b+"Day";Qa=b+"Date";Xa=b+"Month";Ya=b+"FullYear";Bb=c+"Minutes";Cb=c+"Hours";mb=c+"Date";Db=c+"Month";Eb=c+"FullYear"}function ra(){}function Ia(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function ob(a,b){this.axis=a;if(b)this.options=b,this.id=b.id}function Ib(a,b,c,d,e,f){var g=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.stack=e;this.percent=f==="percent";this.alignOptions= -{align:b.align||(g?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(g?"middle":c?"bottom":"top"),y:o(b.y,g?4:c?14:-6),x:o(b.x,g?c?-6:6:0)};this.textAlign=b.textAlign||(g?c?"right":"left":"center")}function ab(){this.init.apply(this,arguments)}function pb(){this.init.apply(this,arguments)}function qb(a,b){this.init(a,b)}function rb(a,b){this.init(a,b)}function sb(){this.init.apply(this,arguments)}var x,z=document,O=window,I=Math,t=I.round,T=I.floor,ja=I.ceil,q=I.max,K=I.min,Q=I.abs,Y=I.cos, -ca=I.sin,Ja=I.PI,bb=Ja*2/360,ya=navigator.userAgent,Jb=O.opera,Da=/msie/i.test(ya)&&!Jb,cb=z.documentMode===8,db=/AppleWebKit/.test(ya),eb=/Firefox/.test(ya),Kb=/(Mobile|Android|Windows Phone)/.test(ya),sa="http://www.w3.org/2000/svg",Z=!!z.createElementNS&&!!z.createElementNS(sa,"svg").createSVGRect,Rb=eb&&parseInt(ya.split("Firefox/")[1],10)<4,$=!Z&&!Da&&!!z.createElement("canvas").getContext,Sa,fb=z.documentElement.ontouchstart!==x,Lb={},tb=0,$a,N,Ua,xa,ub,E,ta=function(){},za=[],wa="div",S="none", -Mb="rgba(192,192,192,"+(Z?1.0E-4:0.002)+")",zb="millisecond",jb="second",Va="minute",Oa="hour",oa="day",Wa="week",Pa="month",va="year",Nb="stroke-width",Za,kb,lb,nb,Qa,Xa,Ya,Bb,Cb,mb,Db,Eb,aa={};O.Highcharts=O.Highcharts?qa(16,!0):{};Ua=function(a,b,c){if(!r(b)||isNaN(b))return"Invalid date";var a=o(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[lb](),g=d[nb](),h=d[Qa](),i=d[Xa](),j=d[Ya](),k=N.lang,m=k.weekdays,d=v({a:m[g].substr(0,3),A:m[g],d:ua(h),e:h,b:k.shortMonths[i],B:k.months[i],m:ua(i+1),y:j.toString().substr(2, -2),Y:j,H:ua(f),I:ua(f%12||12),l:f%12||12,M:ua(d[kb]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:ua(d.getSeconds()),L:ua(t(b%1E3),3)},Highcharts.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};Fb.prototype={wrapColor:function(a){if(this.color>=a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};E=function(){for(var a=0,b=arguments,c=b.length,d={};a-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift=0;if(b.length)for(a=c.length;b.length{point.key}
', -pointFormat:'{series.name}: {point.y}
',shadow:!0,snap:Kb?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer",color:"#909090",fontSize:"9px"}}};var X=N.plotOptions,W=X.line;Hb();var ma=function(a){var b=[],c,d;(function(a){a&&a.stops?d=Ka(a.stops, -function(a){return ma(a[1])}):(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[u(c[1]),u(c[2]),u(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))?b=[u(c[1],16),u(c[2],16),u(c[3],16),1]:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(a))&&(b=[u(c[1]),u(c[2]),u(c[3]),1])})(a);return{get:function(c){var f;d?(f=y(a),f.stops=[].concat(f.stops),n(d,function(a,b){f.stops[b]=[f.stops[b][0], -a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)n(d,function(b){b.brighten(a)});else if(Ca(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=u(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}};ra.prototype={init:function(a,b){this.element=b==="span"?U(b):z.createElementNS(sa,b);this.renderer=a;this.attrSetters={}},opacity:1,animate:function(a,b,c){b= -o(b,xa,!0);Ta(this);if(b){b=y(b);if(c)b.complete=c;vb(this,a,b)}else this.attr(a),c&&c()},attr:function(a,b){var c,d,e,f,g=this.element,h=g.nodeName.toLowerCase(),i=this.renderer,j,k=this.attrSetters,m=this.shadows,l,p,s=this;fa(a)&&r(b)&&(c=a,a={},a[c]=b);if(fa(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),s=A(g,c)||this[c]||0,c!=="d"&&c!=="visibility"&&(s=parseFloat(s));else{for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c].call(this,d,c),e!==!1){e!==x&&(d=e);if(c==="d")d&& -d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text")for(e=0;el&&/[ \-]/.test(b.textContent||b.innerText))L(b,{width:l+"px",display:"block",whiteSpace:"normal"}),k=l;l=a.fontMetrics(b.style.fontSize).b;B=s<0&&-k;w=p<0&&-m;wb=s*p<0;B+=p*l*(wb?1-h:h);w-=s*l*(j?wb?h:1-h:1);i&&(B-=k*h*(s<0?-1:1),j&&(w-=m*h*(p<0?-1:1)),L(b,{textAlign:g}));this.xCorr=B;this.yCorr=w}L(b,{left:e+B+"px",top:f+w+"px"});if(db)m=b.offsetHeight;this.cTT=G}}else this.alignOnAdd=!0},updateTransform:function(){var a=this.translateX|| -0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation,g=[];e&&(a+=this.attr("width"),b+=this.attr("height"));(a||b)&&g.push("translate("+a+","+b+")");e?g.push("rotate(90) scale(-1,1)"):f&&g.push("rotate("+f+" "+(this.x||0)+" "+(this.y||0)+")");(r(c)||r(d))&&g.push("scale("+o(c,1)+" "+o(d,1)+")");g.length&&A(this.element,"transform",g.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer; -f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||fa(c))this.alignTo=d=c||"renderer",ga(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=o(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x||0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=t(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=t(g); -this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d=this.rotation;c=this.element;var e=this.styles,f=d*bb;if(!a){if(c.namespaceURI===sa||b.forExport){try{a=c.getBBox?v({},c.getBBox()):{width:c.offsetWidth,height:c.offsetHeight}}catch(g){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){b=a.width;c=a.height;if(Da&&e&&e.fontSize==="11px"&&c.toPrecision(3)==="22.7")a.height=c=14;if(d)a.width= -Q(c*ca(f))+Q(b*Y(f)),a.height=Q(c*Y(f))+Q(b*ca(f))}this.bBox=a}return a},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.hide()}})},add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=A(f,"zIndex"),h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ= -!0,g=u(g);if(c.handleZ)for(c=0;cg||!r(g)&&r(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;D(this,"add");return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a=this,b=a.element||{},c=a.shadows,d,e;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;Ta(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(e=0;e/g,'').replace(/<(i|em)>/g,'').replace(//g,"").split(//g),f=b.childNodes,g=/style="([^"]+)"/,h=/href="([^"]+)"/,i=A(b,"x"),j=a.styles,k=j&&j.width&&u(j.width),m=j&&j.lineHeight,l=f.length;l--;)b.removeChild(f[l]);k&&!a.added&&this.box.appendChild(b);e[e.length-1]===""&&e.pop();n(e,function(e,f){var l,o=0,e=e.replace(//g,"|||");l=e.split("|||");n(l,function(e){if(e!==""||l.length===1){var p={},n=z.createElementNS(sa,"tspan"),q;g.test(e)&&(q= -e.match(g)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),A(n,"style",q));h.test(e)&&!d&&(A(n,"onclick",'location.href="'+e.match(h)[1]+'"'),L(n,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");n.appendChild(z.createTextNode(e));o?p.dx=0:p.x=i;A(n,p);!o&&f&&(!Z&&d&&L(n,{display:"block"}),A(n,"dy",m||c.fontMetrics(/px$/.test(n.style.fontSize)?n.style.fontSize:j.fontSize).h,db&&n.offsetHeight));b.appendChild(n);o++;if(k)for(var e=e.replace(/([^\^])-/g, -"$1- ").split(" "),r,t=[];e.length||t.length;)delete a.bBox,r=a.getBBox().width,p=r>k,!p||e.length===1?(e=t,t=[],e.length&&(n=z.createElementNS(sa,"tspan"),A(n,{dy:m||16,x:i}),q&&A(n,"style",q),b.appendChild(n),r>k&&(k=r))):(n.removeChild(n.firstChild),t.unshift(e.pop())),e.length&&n.appendChild(z.createTextNode(e.join(" ").replace(/- /g,"-")))}})})},button:function(a,b,c,d,e,f,g){var h=this.label(a,b,c,null,null,null,null,null,"button"),i=0,j,k,m,l,p,a={x1:0,y1:0,x2:0,y2:1},e=y({"stroke-width":1, -stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);m=e.style;delete e.style;f=y(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);l=f.style;delete f.style;g=y(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#9BD"],[1,"#CDF"]]}},g);p=g.style;delete g.style;J(h.element,"mouseenter",function(){h.attr(f).css(l)});J(h.element,"mouseleave",function(){j=[e,f,g][i];k=[m,l,p][i];h.attr(j).css(k)});h.setState=function(a){(i= -a)?a===2&&h.attr(g).css(p):h.attr(e).css(m)};return h.on("click",function(){d.call(h)}).attr(e).css(v({cursor:"default"},m))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=t(a[1])-b%2/2);a[2]===a[5]&&(a[2]=a[5]=t(a[2])+b%2/2);return a},path:function(a){var b={fill:S};Ba(a)?b.d=a:V(a)&&v(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=V(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},arc:function(a,b,c,d,e,f){if(V(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end, -a=a.x;return this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0})},rect:function(a,b,c,d,e,f){e=V(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e,fill:S});return e.attr(V(a)?a:e.crisp(f,a,b,q(c,0),q(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects,e=d.length;this.width=a;this.height=b;for(this.boxWrapper[o(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return r(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a, -b,c,d,e){var f={preserveAspectRatio:S};arguments.length>1&&v(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f},symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(t(b),t(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),v(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&v(g,f);else if(i.test(a))k=function(a,b){a.element&&(a.attr({width:b[0], -height:b[1]}),a.alignByTranslate||a.translate(t((d-b[0])/2),t((e-b[1])/2)))},j=a.match(i)[1],a=Lb[j],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),U("img",{onload:function(){k(g,Lb[j]=[this.width,this.height])},src:j}));return g},symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M", -a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR,h=e.open,i=Y(f),j=ca(f),k=Y(g),g=ca(g),e=e.end-f');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=U(c);this.renderer=a;this.attrSetters={}},add:function(a){var b=this.renderer,c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();D(this,"add");return this},updateTransform:ra.prototype.htmlUpdateTransform,attr:function(a,b){var c, -d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,m=this.shadows,l,p=this.attrSetters,s=this;fa(a)&&r(b)&&(c=a,a={},a[c]=b);if(fa(a))c=a,s=c==="strokeWidth"||c==="stroke-width"?this.strokeweight:this[c];else for(c in a)if(d=a[c],l=!1,e=p[c]&&p[c].call(this,d,c),e!==!1&&d!==null){e!==x&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),l=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");e=d.length;l=[];for(var o;e--;)if(Ca(d[e]))l[e]= -t(d[e]*10)-5;else if(d[e]==="Z")l[e]="x";else if(l[e]=d[e],d.isArc&&(d[e]==="wa"||d[e]==="at"))o=d[e]==="wa"?1:-1,l[e+5]===l[e+7]&&(l[e+7]-=o),l[e+6]===l[e+8]&&(l[e+8]-=o);d=l.join(" ")||"x";f.path=d;if(m)for(e=m.length;e--;)m[e].path=m[e].cutOff?this.cutOffPath(d,m[e].cutOff):d;l=!0}else if(c==="visibility"){if(m)for(e=m.length;e--;)m[e].style[c]=d;h==="DIV"&&(d=d==="hidden"?"-999em":0,cb||(g[c]=d?"visible":"hidden"),c="top");g[c]=d;l=!0}else if(c==="zIndex")d&&(g[c]=d),l=!0;else if(la(c,["x","y", -"width","height"])!==-1)this[c]=d,c==="x"||c==="y"?c={x:"left",y:"top"}[c]:d=q(0,d),this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,l=!0;else if(c==="class"&&h==="DIV")f.className=d;else if(c==="stroke")d=i.color(d,f,c),c="strokecolor";else if(c==="stroke-width"||c==="strokeWidth")f.stroked=d?!0:!1,c="strokeweight",this[c]=d,Ca(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]||U(i.prepVML([""]),null,null,f))[c]=d||"solid",this.dashstyle=d,l=!0;else if(c=== -"fill")if(h==="SPAN")g.color=d;else{if(h!=="IMG")f.filled=d!==S?!0:!1,d=i.color(d,f,c,this),c="fillcolor"}else if(c==="opacity")l=!0;else if(h==="shape"&&c==="rotation")this[c]=d,f.style.left=-t(ca(d*bb)+1)+"px",f.style.top=t(Y(d*bb))+"px";else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),l=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,l=!0;l||(cb?f[c]=d:A(f,c,d))}return s},clip:function(a){var b=this,c;a?(c=a.members,ga(c,b),c.push(b),b.destroyClip= -function(){ga(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:cb?"inherit":"rect(auto)"});return b.css(a)},css:ra.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Ra(a)},destroy:function(){this.destroyClip&&this.destroyClip();return ra.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=O.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=u(a[c-2])-10*b; -return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,m,l,p,s;k&&typeof k.value!=="string"&&(k="x");l=k;if(a){p=o(a.width,3);s=(a.opacity||0.15)/p;for(e=1;e<=3;e++){m=p*2+1-2*e;c&&(l=this.cutOffPath(k.value,m+0.5));j=[''];h=U(g.prepVML(j),null,{left:u(i.left)+o(a.offsetX,1),top:u(i.top)+o(a.offsetY,1)});if(c)h.cutOff=m+1;j=[''];U(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this}};F=ea(ra,F);var na={Element:F,isIE8:ya.indexOf("MSIE 8.0")>-1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(wa);e=d.element;e.style.position="relative";a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.setSize(b,c,!1);if(!z.namespaces.hcv)z.namespaces.add("hcv","urn:schemas-microsoft-com:vml"), -z.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e=this.createElement(),f=V(a);return v(e,{members:[],left:f?a.x:a,top:f?a.y:b,width:f?a.width:c,height:f?a.height:d,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+t(a?e:d)+ -"px,"+t(a?f:b)+"px,"+t(a?b:f)+"px,"+t(a?d:e)+"px)"};!a&&cb&&c==="DIV"&&v(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e=this,f,g=/^rgba/,h,i,j=S;a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var k,m,l=a.linearGradient||a.radialGradient,p,s,o,B,w,q="",a=a.stops,r,t=[],x=function(){h=['']; -U(e.prepVML(h),null,null,b)};p=a[0];r=a[a.length-1];p[0]>0&&a.unshift([0,p[1]]);r[0]<1&&a.push([1,r[1]]);n(a,function(a,b){g.test(a[1])?(f=ma(a[1]),k=f.get("rgb"),m=f.get("a")):(k=a[1],m=1);t.push(a[0]*100+"% "+k);b?(o=m,B=k):(s=m,w=k)});if(c==="fill")if(i==="gradient")c=l.x1||l[0]||0,a=l.y1||l[1]||0,p=l.x2||l[2]||0,l=l.y2||l[3]||0,q='angle="'+(90-I.atan((l-a)/(p-c))*180/Ja)+'"',x();else{var j=l.r,v=j*2,P=j*2,H=l.cx,C=l.cy,y=b.radialReference,u,j=function(){y&&(u=d.getBBox(),H+=(y[0]-u.x)/u.width- -0.5,C+=(y[1]-u.y)/u.height-0.5,v*=y[2]/u.width,P*=y[2]/u.height);q='src="'+N.global.VMLRadialGradientURL+'" size="'+v+","+P+'" origin="0.5,0.5" position="'+H+","+C+'" color2="'+w+'" ';x()};d.added?j():J(d,"add",j);j=B}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=ma(a),h=["<",c,' opacity="',f.get("a"),'"/>'],U(this.prepVML(h),null,null,b),j=f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b? -(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="','style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},rect:function(a,b,c,d,e,f){if(V(a))b=a.y,c=a.width,d=a.height,f=a.strokeWidth,a=a.x;var g=this.symbol("rect");g.r=e;return g.attr(g.crisp(f,a,b,q(c,0),q(d,0)))},invertChild:function(a,b){var c=b.style;L(a,{flip:"x", -left:u(c.width)-1,top:u(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=Y(f),i=ca(f),j=Y(g),k=ca(g);if(g-f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d){return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){var f=a+c,g=b+d,h;!r(e)||!e.r?f=Aa.prototype.symbols.square.apply(0, -arguments):(h=K(e.r,c,d),f=["M",a+h,b,"L",f-h,b,"wa",f-2*h,b,f,b+2*h,f-h,b,f,b+h,"L",f,g-h,"wa",f-2*h,g-2*h,f,g,f,g-h,f-h,g,"L",a+h,g,"wa",a,g-2*h,a+2*h,g,a+h,g,a,g-h,"L",a,b+h,"wa",a,b,a+2*h,b+2*h,a,b+h,a+h,b,"x","e"]);return f}}};Highcharts.VMLRenderer=F=function(){this.init.apply(this,arguments)};F.prototype=y(Aa.prototype,na);Sa=F}var Qb;if($)Highcharts.CanVGRenderer=F=function(){sa="http://www.w3.org/1999/xhtml"},F.prototype.symbols={},Qb=function(){function a(){var a=b.length,d;for(d=0;dj&&(c=!1)):h+k>l&&(h=l-k,d&& -h+m0&&b.height>0){f=y({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=t.text(f.text,0,0).attr({align:f.textAlign||f.align,rotation:f.rotation,zIndex:w}).css(f.style).add();b=[s[1],s[4], -o(s[6],s[1])];s=[s[2],s[5],o(s[7],s[2])];c=Fa(b);k=Fa(s);g.align(f,!1,{x:c,y:k,width:pa(b)-c,height:pa(s)-k});g.show()}else g&&g.hide();return a},destroy:function(){ga(this.axis.plotLinesAndBands,this);Ga(this,this.axis)}};Ib.prototype={destroy:function(){Ga(this,this.axis)},setTotal:function(a){this.cum=this.total=a},render:function(a){var b=this.options,c=b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,0,0,b.useHTML).css(b.style).attr({align:this.textAlign, -rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c=this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(this.percent?100:this.total,0,0,0,1),c=c.translate(0),c=Q(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight,f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g-c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e.attr({visibility:this.options.crop===!1||d.isInsidePlot(f.x,f.y)?Z?"inherit":"visible":"hidden"})}};ab.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L", -second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:M,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#4d759e", -fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{align:"right",x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return this.total},style:M.style}},defaultLeftAxisOptions:{labels:{align:"right",x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{align:"left",x:8,y:null},title:{rotation:90}}, -defaultBottomAxisOptions:{labels:{align:"center",x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{align:"center",x:0,y:-5},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.staggerLines=this.horiz&&d.labels.staggerLines;this.userOptions= -b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=r(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"?0.5:0;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0; -this.stacks={};this._stacksTouched=0;this.min=this.max=null;var f,d=this.options.events;la(this,a.axes)===-1&&(a.axes.push(this),a[c?"xAxis":"yAxis"].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed===x)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)J(this,f,d[f]);if(this.isLog)this.val2lin=ka,this.lin2val=da},setOptions:function(a){this.options=y(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions, -this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],y(N[this.isXAxis?"xAxis":"yAxis"],a))},update:function(a,b){var c=this.chart,a=c.options[this.xOrY+"Axis"][this.options.index]=y(this.userOptions,a);this.destroy();this._addedPlotLB=!1;this.init(c,a);c.isDirtyBox=!0;o(b,!0)&&c.redraw()},remove:function(a){var b=this.chart,c=this.xOrY+"Axis";n(this.series,function(a){a.remove(!1)});ga(b.axes,this);ga(b[c],this);b.options[c].splice(this.options.index, -1);this.destroy();b.isDirtyBox=!0;o(a,!0)&&b.redraw()},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat,e=N.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=Ea(h,this);else if(c)g=b;else if(d)g=Ua(d,b);else if(f&&a>=1E3)for(;f--&&g===x;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=Na(b/c,-1)+e[f]);g===x&&(g=b>=1E3?Na(b,0):Na(b,-1));return g},getSeriesExtremes:function(){var a=this,b=a.chart,c=a.stacks, -d=[],e=[],f=a._stacksTouched+=1,g,h;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=null;n(a.series,function(g){if(g.visible||!b.options.chart.ignoreHiddenSeries){var j=g.options,k,m,l,p,s,n,B,w,G,t=j.threshold,v,u=[],y=0;a.hasVisibleSeries=!0;if(a.isLog&&t<=0)t=j.threshold=null;if(a.isXAxis){if(j=g.xData,j.length)a.dataMin=K(o(a.dataMin,j[0]),Fa(j)),a.dataMax=q(o(a.dataMax,j[0]),pa(j))}else{var P,H,C,A=g.cropped,z=g.xAxis.getExtremes(),E=!!g.modifyValue;k=j.stacking;a.usePercentage=k==="percent";if(k)s= -j.stack,p=g.type+o(s,""),n="-"+p,g.stackKey=p,m=d[p]||[],d[p]=m,l=e[n]||[],e[n]=l;if(a.usePercentage)a.dataMin=0,a.dataMax=99;j=g.processedXData;B=g.processedYData;v=B.length;for(h=0;h0))if(E&&(G=g.modifyValue(G)),g.getExtremesFromAll||A||(j[h+1]||w)>= -z.min&&(j[h-1]||w)<=z.max)if(w=G.length)for(;w--;)G[w]!==null&&(u[y++]=G[w]);else u[y++]=G}if(!a.usePercentage&&u.length)g.dataMin=k=Fa(u),g.dataMax=g=pa(u),a.dataMin=K(o(a.dataMin,k),k),a.dataMax=q(o(a.dataMax,g),g);if(r(t))if(a.dataMin>=t)a.dataMin=t,a.ignoreMinPadding=!0;else if(a.dataMaxf+this.width)l=!0}else if(c=f,i=m-this.right,hg+this.height)l=!0;return l&&!d?null:e.renderer.crispLine(["M",c,h,"L",i,j],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4],c[5],c[1],c[2]): -d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=ia(T(b/a)*a),c=ia(ja(c/a)*a),e=[];b<=c;){e.push(b);b=ia(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null;if(a>=0.5)a=t(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=T(b),h,i,j,k,m,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d||k<=c)&&g.push(k), -k>c&&(m=!0),k=j}else if(b=da(b),c=da(c),a=e[d?"minorTickInterval":"tickInterval"],a=o(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=ib(a,null,I.pow(10,T(I.log(a)/I.LN10))),g=Ka(this.getLinearTickPositions(a,b,c),ka),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length;for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===x&&!this.isLog)r(a.min)||r(a.max)?this.minRange=null:(n(this.series,function(a){i= -a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===x||hb&&(g=0);c=q(c,g);e=q(e,h?0:g/2);f=q(f,h==="on"?0:g);!a.noSharedTooltip&&r(m)&&(d=r(d)?K(d,m):m)}),g=this.ordinalSlope?this.ordinalSlope/d:1,this.minPointOffset=e*=g,this.pointRangePadding=f*=g,this.pointRange=K(c,b),this.closestPointRange=d;if(a)this.oldTransA=h;this.translationSlope=this.transA=h=this.len/(b+f||1);this.transB=this.horiz?this.left:this.bottom;this.minPixelPadding=h*e},setTickPositions:function(a){var b= -this,c=b.chart,d=b.options,e=b.isLog,f=b.isDatetimeAxis,g=b.isXAxis,h=b.isLinked,i=b.options.tickPositioner,j=d.maxPadding,k=d.minPadding,m=d.tickInterval,l=d.minTickInterval,p=d.tickPixelInterval,s=b.categories;h?(b.linkedParent=c[g?"xAxis":"yAxis"][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=o(c.min,c.dataMin),b.max=o(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&qa(11,1)):(b.min=o(b.userMin,d.min,b.dataMin),b.max=o(b.userMax,d.max,b.dataMax));if(e)!a&&K(b.min,o(b.dataMin,b.min))<= -0&&qa(10,1),b.min=ia(ka(b.min)),b.max=ia(ka(b.max));if(b.range&&(b.userMin=b.min=q(b.min,b.max-b.range),b.userMax=b.max,a))b.range=null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!s&&!b.usePercentage&&!h&&r(b.min)&&r(b.max)&&(c=b.max-b.min)){if(!r(d.min)&&!r(b.userMin)&&k&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*k;if(!r(d.max)&&!r(b.userMax)&&j&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*j}b.tickInterval=b.min===b.max||b.min===void 0||b.max===void 0?1:h&&!m&&p===b.linkedParent.options.tickPixelInterval? -b.linkedParent.tickInterval:o(m,s?1:(b.max-b.min)*p/(b.len||1));g&&!a&&n(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)});b.setAxisTranslation(!0);b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(!m&&b.tickIntervale&&i.shift(),d.endOnTick? -b.max=f:b.max+h(b[d]||0)&&this.options.alignTicks!==!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&&!this.categories&&!this.isLinked&&this.options.alignTicks!== -!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=this.dataMax&&(b=x));this.displayBtn=a!==x||b!==x;this.setExtremes(a,b,!1,x,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=b.offsetRight||0,e=this.horiz,f,g;this.left=g=o(b.left,a.plotLeft+c);this.top=f=o(b.top,a.plotTop);this.width=c=o(b.width,a.plotWidth-c+d);this.height=b=o(b.height,a.plotHeight);this.bottom=a.chartHeight-b-f;this.right=a.chartWidth-c-g; -this.len=q(e?c:b,0);this.pos=e?g:f},getExtremes:function(){var a=this.isLog;return{min:a?ia(da(this.min)):this.min,max:a?ia(da(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?da(this.min):this.min,b=b?da(this.max):this.max;c>a||a===null?a=c:b=a.min&&b<=a.max)j[b]||(j[b]=new Ia(a,b)),w&&j[b].isNew&&j[b].render(c,!0),j[b].render(c,!1,1)}),s&&a.min===0&&(j[-1]||(j[-1]=new Ia(a,-1,null,!0)),j[-1].render(-1))),p&&n(g,function(b,c){if(c%2===0&&b1||Q(b-f.y)>1))clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(){var a=this,b;if(!this.isHidden)b= -this.chart.hoverPoints,this.hideTimer=setTimeout(function(){a.label.fadeOut();a.isHidden=!0},o(this.options.hideDelay,500)),b&&n(b,function(a){a.setState()}),this.chart.hoverPoints=null},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e=d.inverted,f=d.plotTop,g=0,h=0,i,a=ha(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===x&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(n(a,function(a){i=a.series.yAxis;g+= -a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g:h]);return Ka(c,t)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i=o(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,m;d<7&&(d=e+q(j,0)+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,m=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k= -q(f,f+h-b-i));return{x:d,y:k}},defaultFormatter:function(a){var b=this.points||ha(this),c=b[0].series,d;d=[c.tooltipHeaderFormatter(b[0])];n(b,function(a){c=a.series;d.push(c.tooltipFormatter&&c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat||"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h,i={},j,k=[];j=e.formatter||this.defaultFormatter;var i=c.hoverPoints,m,l=e.crosshairs;h=this.shared; -clearTimeout(this.hideTimer);this.followPointer=ha(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];h&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,i&&n(i,function(a){a.setState()}),n(a,function(a){a.setState("hover");k.push(a.getLabelConfig())}),i={x:a[0].category,y:a[0].y},i.points=k,a=a[0]):i=a.getLabelConfig();j=j.call(i,this);i=a.series;h=h||!i.isCartesian||i.tooltipOutsidePlot||c.isInsidePlot(f,g);j===!1||!h?this.hide():(this.isHidden&&(Ta(d),d.attr("opacity", -1).show()),d.attr({text:j}),m=e.borderColor||a.color||i.color||"#606060",d.attr({stroke:m}),this.updatePosition({plotX:f,plotY:g}),this.isHidden=!1);if(l){l=ha(l);for(d=l.length;d--;)if(e=a.series[d?"yAxis":"xAxis"],l[d]&&e)if(h=d?o(a.stackY,a.y):a.x,e.isLog&&(h=ka(h)),e=e.getPlotLinePath(h,1),this.crosshairs[d])this.crosshairs[d].attr({d:e,visibility:"visible"});else{h={"stroke-width":l[d].width||1,stroke:l[d].color||"#C0C0C0",zIndex:l[d].zIndex||2};if(l[d].dashStyle)h.dashstyle=l[d].dashStyle;this.crosshairs[d]= -c.renderer.path(e).attr(h).add()}}D(c,"tooltipRefresh",{text:j,x:f+c.plotLeft,y:g+c.plotTop,borderColor:m})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(t(c.x),t(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)}};qb.prototype={init:function(a,b){var c=$?"":b.chart.zoomType,d=a.inverted,e;this.options=b;this.chart=a;this.zoomX=e=/x/.test(c);this.zoomY=c=/y/.test(c);this.zoomHor=e&&!d||c&&d;this.zoomVert=c&& -!d||e&&d;this.pinchDown=[];this.lastValidTouch={};if(b.tooltip.enabled)a.tooltip=new pb(a,b.tooltip);this.setDOMEvents()},normalize:function(a){var b,c,d,a=a||O.event;if(!a.target)a.target=a.srcElement;a=Pb(a);d=a.touches?a.touches.item(0):a;this.chartPosition=b=Tb(this.chart.container);d.pageX===x?(c=a.x,b=a.y):(c=d.pageX-b.left,b=d.pageY-b.top);return v(a,{chartX:t(c),chartY:t(b)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};n(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c, -value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=this.getIndex(a);if(d&&this.options.tooltip.shared&&(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;ij&&e.splice(h,1);if(e.length&&e[0].clientX!==this.hoverX)d.refresh(e,a),this.hoverX=e[0].clientX}if(g&&g.tracker){if((b=g.tooltipPoints[k])&&b!==f)b.onMouseOver(a)}else d&&d.followPointer&&!d.isHidden&&(a=d.getAnchor([{}],a),d.updatePosition({plotX:a[0],plotY:a[1]}))},reset:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.tooltip,b=e&&e.shared?b.hoverPoints:d;(a=a&&e&&b)&& -ha(b)[0].plotX===x&&(a=!1);if(a)e.refresh(b);else{if(d)d.onMouseOut();if(c)c.onMouseOut();e&&(e.hide(),e.hideCrosshairs());this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart;n(c.series,function(d){d.xAxis.zoomEnabled&&(d.group.attr(a),d.markerGroup&&(d.markerGroup.attr(a),d.markerGroup.clip(b?c.clipRect:null)),d.dataLabelsGroup&&d.dataLabelsGroup.attr(a))});c.clipRect.attr(b||c.clipBox)},pinchTranslateDirection:function(a,b,c,d,e,f,g){var h=this.chart,i=a?"x":"y",j=a?"X":"Y",k="chart"+ -j,m=a?"width":"height",l=h["plot"+(a?"Left":"Top")],p,s,o=1,n=h.inverted,w=h.bounds[a?"h":"v"],q=b.length===1,t=b[0][k],r=c[0][k],v=!q&&b[1][k],u=!q&&c[1][k],x,c=function(){!q&&Q(t-v)>20&&(o=Q(r-u)/Q(t-v));s=(l-r)/o+t;p=h["plot"+(a?"Width":"Height")]/o};c();b=s;bw.max&&(b=w.max-p,x=!0);x?(r-=0.8*(r-g[i][0]),q||(u-=0.8*(u-g[i][1])),c()):g[i]=[r,u];n||(f[i]=s-l,f[m]=p);f=n?1/o:o;e[m]=p;e[i]=b;d[n?a?"scaleY":"scaleX":"scale"+j]=o;d["translate"+j]=f*l+(r-f*t)},pinch:function(a){var b= -this,c=b.chart,d=b.pinchDown,e=c.tooltip.options.followTouchMove,f=a.touches,g=f.length,h=b.lastValidTouch,i=b.zoomHor||b.pinchHor,j=b.zoomVert||b.pinchVert,k=i||j,m=b.selectionMarker,l={},p={};a.type==="touchstart"&&e&&(b.inClass(a.target,"highcharts-tracker")?(!c.runTrackerClick||g>1)&&a.preventDefault():(!c.runChartClick||g>1)&&a.preventDefault());Ka(f,function(a){return b.normalize(a)});if(a.type==="touchstart")n(f,function(a,b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&& -d[1].chartX],h.y=[d[0].chartY,d[1]&&d[1].chartY],n(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(a.dataMin),f=a.toPixels(a.dataMax),g=K(e,f),e=q(e,f);b.min=K(a.pos,g-d);b.max=q(a.pos+a.len,e+d)}});else if(d.length){if(!m)b.selectionMarker=m=v({destroy:ta},c.plotBox);i&&b.pinchTranslateDirection(!0,d,f,l,m,p,h);j&&b.pinchTranslateDirection(!1,d,f,l,m,p,h);b.hasPinched=k;b.scaleGroups(l,p);!k&&e&&g===1&&this.runPointActions(b.normalize(a))}},dragStart:function(a){var b= -this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,a=a.chartY,e=this.zoomHor,f=this.zoomVert,g=b.plotLeft,h=b.plotTop,i=b.plotWidth,j=b.plotHeight,k,m=this.mouseDownX,l=this.mouseDownY;dg+i&&(d=g+i);ah+j&&(a=h+j);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(l-a,2));if(this.hasDragged>10){k=b.isInsidePlot(m-g,l-h);if(b.hasCartesianSeries&&(this.zoomX|| -this.zoomY)&&k&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(g,h,e?1:i,f?1:j,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&e&&(e=d-m,this.selectionMarker.attr({width:Q(e),x:(e>0?0:e)+m}));this.selectionMarker&&f&&(e=a-l,this.selectionMarker.attr({height:Q(e),y:(e>0?0:e)+l}));k&&!this.selectionMarker&&c.panning&&b.pan(d)}},drop:function(a){var b=this.chart,c=this.hasPinched;if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent|| -a},e=this.selectionMarker,f=e.x,g=e.y,h;if(this.hasDragged||c)n(b.axes,function(a){if(a.zoomEnabled){var b=a.horiz,c=a.minPixelPadding,m=a.toValue((b?f:g)+c),b=a.toValue((b?f+e.width:g+e.height)-c);!isNaN(m)&&!isNaN(b)&&(d[a.xOrY+"Axis"].push({axis:a,min:K(m,b),max:q(m,b)}),h=!0)}}),h&&D(b,"selection",d,function(a){b.zoom(v(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups({translateX:b.plotLeft,translateY:b.plotTop,scaleX:1,scaleY:1})}if(b)L(b.container, -{cursor:b._cursor}),b.cancelClick=this.hasDragged,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){this.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=Pb(a);c&&d&&d.isCartesian&&!b.isInsidePlot(a.pageX-c.left-b.plotLeft,a.pageY-c.top-b.plotTop)&&this.reset()},onContainerMouseLeave:function(){this.reset(); -this.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart,a=this.normalize(a);a.returnValue=!1;b.mouseIsDown==="mousedown"&&this.drag(a);b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=A(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!==-1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b=this.chart.hoverSeries;if(b&&!b.options.stickyTracking&&!this.inClass(a.toElement|| -a.relatedTarget,"highcharts-tooltip"))b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,f=b.inverted,g,h,i,a=this.normalize(a);a.cancelBubble=!0;if(!b.cancelClick)c&&this.inClass(a.target,"highcharts-tracker")?(g=this.chartPosition,h=c.plotX,i=c.plotY,v(c,{pageX:g.left+d+(f?b.plotWidth-i:h),pageY:g.top+e+(f?b.plotHeight-h:i)}),D(c.series,"click",v(a,{point:c})),c.firePointEvent("click",a)):(v(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY- -e)&&D(b,"click",a))},onContainerTouchStart:function(a){var b=this.chart;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&(this.runPointActions(a),this.pinch(a))):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length===1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){this.drop(a)},setDOMEvents:function(){var a=this,b=a.chart.container,c;this._events=c=[[b,"onmousedown","onContainerMouseDown"], -[b,"onmousemove","onContainerMouseMove"],[b,"onclick","onContainerClick"],[b,"mouseleave","onContainerMouseLeave"],[z,"mousemove","onDocumentMouseMove"],[z,"mouseup","onDocumentMouseUp"]];fb&&c.push([b,"ontouchstart","onContainerTouchStart"],[b,"ontouchmove","onContainerTouchMove"],[z,"touchend","onDocumentTouchEnd"]);n(c,function(b){a["_"+b[2]]=function(c){a[b[2]](c)};b[1].indexOf("on")===0?b[0][b[1]]=a["_"+b[2]]:J(b[0],b[1],a["_"+b[2]])})},destroy:function(){var a=this;n(a._events,function(b){b[1].indexOf("on")=== -0?b[0][b[1]]=null:ba(b[0],b[1],a["_"+b[2]])});delete a._events;clearInterval(a.tooltipTimeout)}};rb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=o(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.baseline=u(d.fontSize)+3+f,c.itemStyle=d,c.itemHiddenStyle=y(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight=0,c.lastLineHeight=0,c.render(),J(c.chart,"endResize",function(){c.positionCheckboxes()})}, -colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.color:g,g=a.options&&a.options.marker,i={stroke:h,fill:h},j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g)for(j in g=a.convertAttribs(g),g)d=g[j],d!==x&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl,d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth- -e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&a[b].destroy()});b&&Ra(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c=b.translateY,n(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,L(f,{left:b.translateX+e.legendItemWidth+ -f.x-20+"px",top:g+"px",display:g>c-6&&g(l||c.chartWidth-2*k-o))b.itemX=o,b.itemY+=s+b.lastLineHeight+p,b.lastLineHeight=0;b.maxItemWidth=q(b.maxItemWidth,e);b.lastItemY=s+b.itemY+p;b.lastLineHeight=q(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=s+g+p,b.lastLineHeight=g);b.offsetWidth=l||q(f?b.itemX-o:e,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer, -d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,m=j.borderWidth,l=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup),a.clipRect=c.clipRect(0,0,9999,b.chartHeight),a.contentGroup.clip(a.clipRect);a.renderTitle();e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&!r(b.linkedTo)&&(e=e.concat(a.legendItems||(b.legendType=== -"point"?a.data:a)))});Gb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(m||l){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,m||0).attr({stroke:j.borderColor,"stroke-width":m|| -0,fill:l||S}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});f&&d.align(v({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=o(i.animation,!0),k=i.arrowSize||12,m=this.nav;e.layout==="horizontal"&& -(f/=2);g&&(f=K(f,g));if(a>f&&!e.useHTML){this.clipHeight=c=f-20-this.titleHeight;this.pageCount=ja(a/c);this.currentPage=o(this.currentPage,1);this.fullHeight=a;h.attr({height:c});if(!m)this.nav=m=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(m),this.pager=d.text("",15,10).css(i.style).add(m),this.down=d.symbol("triangle-down",0,0,k,k).on("click",function(){b.scroll(1,j)}).add(m);b.scroll(0);a=f}else if(m)h.attr({height:c.chartHeight}), -m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor,h=f.inactiveColor,f=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==x&&Ha(b,this.chart),this.nav.attr({translateX:i,translateY:e+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:d===1?h:g}).css({cursor:d===1?"default":"pointer"}),f.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width, -fill:d===c?h:g}).css({cursor:d===c?"default":"pointer"}),e=-K(e*(d-1),this.fullHeight-e+i)+1,this.scrollGroup.animate({translateY:e}),f.attr({text:d+"/"+c}),this.currentPage=d,this.positionCheckboxes(e)}};sb.prototype={init:function(a,b){var c,d=a.series;a.series=null;c=y(N,a);c.series=a.series=d;var d=c.chart,e=d.margin,e=V(e)?e:[e,e,e,e];this.optionsMarginTop=o(d.marginTop,e[0]);this.optionsMarginRight=o(d.marginRight,e[1]);this.optionsMarginBottom=o(d.marginBottom,e[2]);this.optionsMarginLeft= -o(d.marginLeft,e[3]);this.runChartClick=(e=d.events)&&!!e.click;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=za.length;za.push(f);d.reflow!==!1&&J(f,"load",function(){f.initReflow()});if(e)for(g in e)J(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=$?!1:o(d.animation,!0);f.pointCount=0;f.counters=new Fb;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=aa[a.type||b.type|| -b.defaultSeriesType])||qa(17,!0);b=new b;b.init(this,a);return b},addSeries:function(a,b,c){var d,e=this;a&&(b=o(b,!0),D(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var b=b?"xAxis":"yAxis",e=this.options;new ab(this,y(a,{index:this[b].length}));e[b]=ha(e[b]||{});e[b].push(a);o(c,!0)&&this.redraw(d)},isInsidePlot:function(a,b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!== -!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h=this.isDirtyBox,i=c.length,j=i,k=this.renderer,m=k.isHidden(),l=[];Ha(a,this);for(m&&this.cloneRenderTo();j--;)if(a=c[j],a.isDirty&&a.options.stacking){g=!0;break}if(g)for(j=i;j--;)if(a=c[j],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend= -!1;if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});this.adjustTickAmounts();this.getMargins();n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,l.push(function(){D(a,"afterSetExtremes",a.getExtremes())});if(a.isDirty||h||g)a.redraw(),h=!0})}h&&this.drawChartBox();n(c,function(a){a.isDirty&&a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset&&d.reset(!0);k.draw();D(this,"redraw");m&&this.cloneRenderTo(!0);n(l,function(a){a.call()})}, -showLoading:function(a){var b=this.options,c=this.loadingDiv,d=b.loading;if(!c)this.loadingDiv=c=U(wa,{className:"highcharts-loading"},v(d.style,{zIndex:10,display:S}),this.container),this.loadingSpan=U("span",null,d.labelStyle,c);this.loadingSpan.innerHTML=a||b.lang.loading;if(!this.loadingShown)L(c,{opacity:0,display:"",left:this.plotLeft+"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px"}),vb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown= -!0},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&vb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){L(b,{display:S})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dK(e.dataMin,e.min)&&c< -q(e.dataMax,e.max)&&b.setExtremes(f,c,!0,!1,{trigger:"pan"});this.mouseDownX=a;L(this.container,{cursor:"move"})},setTitle:function(a,b){var f;var c=this,d=c.options,e;e=d.title=y(d.title,a);f=d.subtitle=y(d.subtitle,b),d=f;n([["title",a,e],["subtitle",b,d]],function(a){var b=a[0],d=c[b],e=a[1],a=a[2];d&&e&&(c[b]=d=d.destroy());a&&a.text&&!d&&(c[b]=c.renderer.text(a.text,0,0,a.useHTML).attr({align:a.align,"class":"highcharts-"+b,zIndex:a.zIndex||4}).css(a.style).add().align(a,!1,"spacingBox"))})}, -getChartSize:function(){var a=this.options.chart,b=this.renderToClone||this.renderTo;this.containerWidth=gb(b,"width");this.containerHeight=gb(b,"height");this.chartWidth=q(0,a.width||this.containerWidth||600);this.chartHeight=q(0,o(a.height,this.containerHeight>19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Ra(b),delete this.renderToClone):(c&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0), -L(b,{position:"absolute",top:"-9999px",display:"block"}),z.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+tb++;if(fa(a))this.renderTo=a=z.getElementById(a);a||qa(13,!0);c=u(A(a,"data-highcharts-chart"));!isNaN(c)&&za[c]&&za[c].destroy();A(a,"data-highcharts-chart",this.index);a.innerHTML="";a.offsetWidth||this.cloneRenderTo();this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=U(wa, -{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},v({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new Aa(a,c,d,!0):new Sa(a,c,d);$&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.options.chart,b=a.spacingTop,c=a.spacingRight,d=a.spacingBottom,a=a.spacingLeft,e,f=this.legend,g=this.optionsMarginTop,h=this.optionsMarginLeft, -i=this.optionsMarginRight,j=this.optionsMarginBottom,k=this.options.title,m=this.options.subtitle,l=this.options.legend,p=o(l.margin,10),s=l.x,t=l.y,B=l.align,w=l.verticalAlign;this.resetMargins();e=this.axisOffset;if((this.title||this.subtitle)&&!r(this.optionsMarginTop))if(m=q(this.title&&!k.floating&&!k.verticalAlign&&k.y||0,this.subtitle&&!m.floating&&!m.verticalAlign&&m.y||0))this.plotTop=q(this.plotTop,m+o(k.margin,15)+b);if(f.display&&!l.floating)if(B==="right"){if(!r(i))this.marginRight=q(this.marginRight, -f.legendWidth-s+p+c)}else if(B==="left"){if(!r(h))this.plotLeft=q(this.plotLeft,f.legendWidth+s+p+a)}else if(w==="top"){if(!r(g))this.plotTop=q(this.plotTop,f.legendHeight+t+p+b)}else if(w==="bottom"&&!r(j))this.marginBottom=q(this.marginBottom,f.legendHeight-t+p+d);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+=this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});r(h)||(this.plotLeft+=e[3]);r(g)||(this.plotTop+= -e[0]);r(j)||(this.marginBottom+=e[2]);r(i)||(this.marginRight+=e[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||gb(d,"width"),h=c.height||gb(d,"height"),a=a?a.target:O;if(!b.hasUserSize&&g&&h&&(a===O||a===z)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),b.reflowTimeout=e=setTimeout(function(){if(b.container)b.setSize(g,h,!1),b.hasUserSize=null},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;J(O,"resize",a);J(b, -"destroy",function(){ba(O,"resize",a)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&D(d,"endResize",null,function(){d.isResizing-=1})};Ha(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(r(a))d.chartWidth=e=q(0,t(a)),d.hasUserSize=!!e;if(r(b))d.chartHeight=f=q(0,t(b));L(d.container,{width:e+"px",height:f+"px"});d.setChartSize(!0);d.renderer.setSize(e,f,c);d.maxTicks=null;n(d.axes,function(a){a.isDirty=!0;a.setScale()});n(d.series,function(a){a.isDirty= -!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.getMargins();d.redraw(c);d.oldChartHeight=null;D(d,"resize");xa===!1?g():setTimeout(g,xa&&xa.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=f.spacingTop,h=f.spacingRight,i=f.spacingBottom,j=f.spacingLeft,k=this.clipOffset,m,l,p,o;this.plotLeft=m=t(this.plotLeft);this.plotTop=l=t(this.plotTop);this.plotWidth=p=q(0,t(d-m-this.marginRight));this.plotHeight=o=q(0,t(e-l-this.marginBottom)); -this.plotSizeX=b?o:p;this.plotSizeY=b?p:o;this.plotBorderWidth=b=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:j,y:g,width:d-j-h,height:e-g-i};this.plotBox=c.plotBox={x:m,y:l,width:p,height:o};c=ja(q(b,k[3])/2);d=ja(q(b,k[0])/2);this.clipBox={x:c,y:d,width:T(this.plotSizeX-q(b,k[1])/2-c),height:T(this.plotSizeY-q(b,k[2])/2-d)};a||n(this.axes,function(a){a.setAxisSize();a.setAxisTranslation()})},resetMargins:function(){var a=this.options.chart,b=a.spacingRight,c=a.spacingBottom,d=a.spacingLeft; -this.plotTop=o(this.optionsMarginTop,a.spacingTop);this.marginRight=o(this.optionsMarginRight,b);this.marginBottom=o(this.optionsMarginBottom,c);this.plotLeft=o(this.optionsMarginLeft,d);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground,g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,m=a.plotBackgroundImage, -l=a.plotBorderWidth||0,p,o=this.plotLeft,n=this.plotTop,t=this.plotWidth,q=this.plotHeight,r=this.plotBox,v=this.clipRect,u=this.clipBox;p=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-p,d-p));else{e={fill:j||S};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(p/2,p/2,c-p,d-p,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f?f.animate(r):this.plotBackground=b.rect(o,n,t,q,0).attr({fill:k}).add().shadow(a.plotShadow);if(m)h?h.animate(r):this.plotBGImage= -b.image(m,o,n,t,q).add();v?v.animate({width:u.width,height:u.height}):this.clipRect=b.clipRect(u);if(l)g?g.animate(g.crisp(null,o,n,t,q)):this.plotBorder=b.rect(o,n,t,q,0,l).attr({stroke:a.plotBorderColor,"stroke-width":l,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"],function(g){c=aa[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=aa[d[e].type])&&c.prototype[g]&& -(f=!0);a[g]=f})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options,e=d.labels,f=d.credits,g;a.setTitle();a.legend=new rb(a,d.legend);n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()});if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints(); -a.render()});e.items&&n(e.items,function(b){var d=v(e.style,b.style),f=u(d.left)+a.plotLeft,g=u(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position);a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;D(a,"destroy");za[a.index]= -x;a.renderTo.removeAttribute("data-highcharts-chart");ba(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())});if(d)d.innerHTML="",ba(d),f&&Ra(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!Z&&O==O.top&&z.readyState!== -"complete"||$&&!O.canvg?($?Qb.push(function(){a.firstRender()},a.options.global.canvasToolsURL):z.attachEvent("onreadystatechange",function(){z.detachEvent("onreadystatechange",a.firstRender);z.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender())a.getContainer(),D(a,"init"),a.resetMargins(),a.setChartSize(),a.propFromSeries(),a.getAxes(),n(b.series||[],function(b){a.initSeries(b)}),D(a,"beforeRender"),a.pointer=new qb(a, -b),a.render(),a.renderer.draw(),c&&c.apply(a,[a]),n(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),D(a,"load")}};sb.prototype.callbacks=[];var Ma=function(){};Ma.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++],a.colorCounter===b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d= -c.pointValKey,a=Ma.prototype.optionsToObject.call(this,a);v(this,a);this.options=this.options?v(this.options,a):a;if(d)this.y=this[d];if(this.x===x&&c)this.x=b===x?c.autoIncrement():b;return this},optionsToObject:function(a){var b,c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b={y:a};else if(Ba(a)){b={};if(a.length>e){c=typeof a[0];if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions,d=c[this.type];this.userOptions=a;a=y(d,c.series,a);this.tooltipOptions=y(b.tooltip,a.tooltip);d.marker===null&&delete a.marker; -return a},getColor:function(){var a=this.options,b=this.userOptions,c=this.chart.options.colors,d=this.chart.counters,e;e=a.color||X[this.type].color;if(!e&&!a.colorByPoint)r(b._colorIndex)?a=b._colorIndex:(b._colorIndex=d.color,a=d.color++),e=c[a];this.color=e;d.wrapColor(c.length)},getSymbol:function(){var a=this.userOptions,b=this.options.marker,c=this.chart,d=c.options.symbols,c=c.counters;this.symbol=b.symbol;if(!this.symbol)r(a._symbolIndex)?a=a._symbolIndex:(a._symbolIndex=c.symbol,a=c.symbol++), -this.symbol=d[a];if(/^url/.test(this.symbol))b.radius=0;c.wrapSymbol(d.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline,g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a-4,"L",d,a-4]).attr(g).add(f)}if(c&&c.enabled)b=c.radius,this.legendSymbol=e.symbol(this.symbol,d/2-b,a-4-b,2*b,2*b).add(f)},addPoint:function(a,b,c,d){var e=this.options, -f=this.data,g=this.graph,h=this.area,i=this.chart,j=this.xData,k=this.yData,m=this.zData,l=this.names,p=g&&g.shift||0,n=e.data;Ha(d,i);if(g&&c)g.shift=p+1;if(h){if(c)h.shift=p+1;h.isArea=!0}b=o(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);j.push(d.x);k.push(this.toYData?this.toYData(d):d.y);m.push(d.z);if(l)l[d.x]=d.name;n.push(a);e.legendType==="point"&&this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),j.shift(),k.shift(),m.shift(),n.shift()));this.getAttribs(); -this.isDirtyData=this.isDirty=!0;b&&i.redraw()},setData:function(a,b){var c=this.points,d=this.options,e=this.chart,f=null,g=this.xAxis,h=g&&g.categories&&!g.categories.length?[]:null,i;this.xIncrement=null;this.pointRange=g&&g.categories?1:d.pointRange;this.colorCounter=0;var j=[],k=[],m=[],l=a?a.length:[],p=(i=this.pointArrayMap)&&i.length,n=!!this.toYData;if(l>(d.turboThreshold||1E3)){for(i=0;f===null&&i1&&j[1]k||this.forceCrop))if(a=i.getExtremes(),i=a.min,k=a.max,b[d-1]k)b=[],c=[];else if(b[0]k){for(a=0;a=i){e=q(0,a-1);break}for(;ak){f=a+1;break}b=b.slice(e,f);c=c.slice(e,f);g=!0}for(a=b.length-1;a>0;a--)if(d=b[a]-b[a-1],d>0&&(h===x||d=0&&c<=d;)h[c++]=f}this.tooltipPoints=h}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat,d=this.xAxis,e=d&&d.options.type==="datetime",f=b.headerFormat,g;if(e&&!c)for(g in E)if(E[g]>=d.closestPointRange){c= -b.dateTimeLabelFormats[g];break}e&&c&&Ca(a.key)&&(f=f.replace("{point.key}","{point.key:"+c+"}"));return Ea(f,{point:a,series:this})},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&&D(this,"mouseOver");this.setState("hover");a.hoverSeries=this},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&D(this,"mouseOut");c&&!a.stickyTracking&&(!c.shared||this.noSharedTooltip)&& -c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=this,c=b.chart,d=c.renderer,e;e=b.options.animation;var f=c.clipBox,g=c.inverted,h;if(e&&!V(e))e=X[b.type].animation;h="_sharedClip"+e.duration+e.easing;if(a)a=c[h],e=c[h+"m"],a||(c[h]=a=d.clipRect(v(f,{width:0})),c[h+"m"]=e=d.clipRect(-99,g?-c.plotLeft:-c.plotTop,99,g?c.chartWidth:c.chartHeight)),b.group.clip(a),b.markerGroup.clip(e),b.sharedClipKey=h;else{if(a=c[h])a.animate({width:c.plotSizeX},e),c[h+"m"].animate({width:c.plotSizeX+ -99},e);b.animate=null;b.animationTimeout=setTimeout(function(){b.afterAnimate()},e.duration)}},afterAnimate:function(){var a=this.chart,b=this.sharedClipKey,c=this.group;c&&this.options.clip!==!1&&(c.clip(a.clipRect),this.markerGroup.clip());setTimeout(function(){b&&a[b]&&(a[b]=a[b].destroy(),a[b+"m"]=a[b+"m"].destroy())},100)},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,j,k,m=this.options.marker,l,n=this.markerGroup;if(m.enabled||this._hasPointMarkers)for(f=b.length;f--;)if(g= -b[f],d=g.plotX,e=g.plotY,k=g.graphic,i=g.marker||{},a=m.enabled&&i.enabled===x||i.enabled,l=c.isInsidePlot(d,e,c.inverted),a&&e!==x&&!isNaN(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=o(i.symbol,this.symbol),j=i.indexOf("url")===0,k)k.attr({visibility:l?Z?"inherit":"visible":"hidden"}).animate(v({x:d-h,y:e-h},k.symbolName?{width:2*h,height:2*h}:{}));else{if(l&&(h>0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(n)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a, -b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=o(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=X[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color,h={stroke:g,fill:g},i=a.points||[],j=[],k,m=a.pointAttrToOptions,l=b.negativeColor,p;b.marker?(e.radius=e.radius||c.radius+2,e.lineWidth=e.lineWidth||c.lineWidth+1):e.color=e.color||ma(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,h);n(["hover", -"select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;for(g=i.length;g--;){h=i[g];if((c=h.options&&h.options.marker||h.options)&&c.enabled===!1)c.radius=0;if(h.negative&&l)h.color=h.fillColor=l;f=b.colorByPoint||h.color;if(h.options)for(p in m)r(c[m[p]])&&(f=!0);if(f){c=c||{};k=[];d=c.states||{};f=d.hover=d.hover||{};if(!b.marker)f.color=ma(f.color||h.color).brighten(f.brightness||e.brightness).get();k[""]=a.convertAttribs(v({color:h.color},c),j[""]);k.hover=a.convertAttribs(d.hover, -j.hover,k[""]);k.select=a.convertAttribs(d.select,j.select,k[""]);if(h.negative&&b.marker)k[""].fill=k.hover.fill=k.select.fill=a.convertAttribs({fillColor:l}).fill}else k=j;h.pointAttr=k}},update:function(a,b){var c=this.chart,d=this.type,a=y(this.userOptions,{animation:!1,index:this.index,pointStart:this.xData[0]},a);this.remove(!1);v(this,aa[a.type||d].prototype);this.init(c,a);o(b,!0)&&c.redraw(!1)},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(ya),d,e,f=a.data||[],g,h,i;D(a, -"destroy");ba(a);n(["xAxis","yAxis"],function(b){if(i=a[b])ga(i.series,a),i.isDirty=i.forceRedraw=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(e=f.length;e--;)(g=f[e])&&g.destroy&&g.destroy();a.points=null;clearTimeout(a.animationTimeout);n("area,graph,dataLabelsGroup,group,markerGroup,tracker,graphNeg,areaNeg,posClip,negClip".split(","),function(b){a[b]&&(d=c&&b==="group"?"hide":"destroy",a[b][d]())});if(b.hoverSeries===a)b.hoverSeries=null;ga(b.series,a);for(h in a)delete a[h]},drawDataLabels:function(){var a= -this,b=a.options.dataLabels,c=a.points,d,e,f,g;if(b.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(b),g=a.plotGroup("dataLabelsGroup","data-labels",a.visible?"visible":"hidden",b.zIndex||6),e=b,n(c,function(c){var i,j=c.dataLabel,k,m,l=c.connector,n=!0;d=c.options&&c.options.dataLabels;i=e.enabled||d&&d.enabled;if(j&&!i)c.dataLabel=j.destroy();else if(i){i=b.rotation;b=y(e,d);k=c.getLabelConfig();f=b.format?Ea(b.format,k):b.formatter.call(k,b);b.style.color=o(b.color,b.style.color, -a.color,"black");if(j)if(r(f))j.attr({text:f}),n=!1;else{if(c.dataLabel=j=j.destroy(),l)c.connector=l.destroy()}else if(r(f)){j={fill:b.backgroundColor,stroke:b.borderColor,"stroke-width":b.borderWidth,r:b.borderRadius||0,rotation:i,padding:b.padding,zIndex:1};for(m in j)j[m]===x&&delete j[m];j=c.dataLabel=a.chart.renderer[i?"text":"label"](f,0,-999,null,null,null,b.useHTML).attr(j).css(b.style).add(g).shadow(b.shadow)}j&&a.alignDataLabel(c,j,b,null,n)}})},alignDataLabel:function(a,b,c,d,e){var f= -this.chart,g=f.inverted,h=o(a.plotX,-999),a=o(a.plotY,-999),i=b.getBBox(),d=v({x:g?f.plotWidth-a:h,y:t(g?f.plotHeight-h:a),width:0,height:0},d);v(c,{width:i.width,height:i.height});c.rotation?(d={align:c.align,x:d.x+c.x+d.width/2,y:d.y+c.y+d.height/2},b[e?"attr":"animate"](d)):b.align(c,null,d);b.attr({visibility:c.crop===!1||f.isInsidePlot(h,a,g)?f.renderer.isSVG?"inherit":"visible":"hidden"})},getSegmentPath:function(a){var b=this,c=[],d=b.options.step;n(a,function(e,f){var g=e.plotX,h=e.plotY, -i;b.getPointSpline?c.push.apply(c,b.getPointSpline(a,e,f)):(c.push(f?"L":"M"),d&&f&&(i=a[f-1],d==="right"?c.push(i.plotX,h):d==="center"?c.push((i.plotX+g)/2,i.plotY,(i.plotX+g)/2,h):c.push(g,i.plotY)),c.push(e.plotX,e.plotY))});return c},getGraphPath:function(){var a=this,b=[],c,d=[];n(a.segments,function(e){c=a.getSegmentPath(e);e.length>1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth, -e=b.dashStyle,f=this.getGraphPath(),g=b.negativeColor;g&&c.push(["graphNeg",g]);n(c,function(c,g){var j=c[0],k=a[j];if(k)Ta(k),k.animate({d:f});else if(d&&f.length){k={stroke:c[1],"stroke-width":d,zIndex:1};if(e)k.dashstyle=e;a[j]=a.chart.renderer.path(f).attr(k).add(a.group).shadow(!g&&b.shadow)}})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor,e,f=this.posClip,g=this.negClip;e=b.chartWidth;var h=b.chartHeight,i=q(e,h);if(d&&this.graph)d=ja(this.yAxis.len-this.yAxis.translate(a.threshold|| -0)),a={x:0,y:0,width:i,height:d},i={x:0,y:d,width:i,height:i-d},b.inverted&&c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:h},i={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d,height:e}),this.yAxis.reversed?(b=i,e=a):(b=a,e=i),f?(f.animate(b),g.animate(e)):(this.posClip=f=c.clipRect(b),this.graph.clip(f),this.negClip=g=c.clipRect(e),this.graphNeg.clip(g),this.area&&(this.area.clip(f),this.areaNeg.clip(g)))},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};n(["group", -"markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;J(c,"resize",a);J(b,"destroy",function(){ba(c,"resize",a)});a();b.invertGroups=a},plotGroup:function(a,b,c,d,e){var f=this[a],g=!f,h=this.chart,i=this.xAxis,j=this.yAxis;g&&(this[a]=f=h.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"]({translateX:i?i.left:h.plotLeft,translateY:j?j.top:h.plotTop,scaleX:1,scaleY:1});return f},render:function(){var a=this.chart,b,c=this.options,d=c.animation&& -!!this.animate&&a.renderer.isSVG,e=this.visible?"visible":"hidden",f=c.zIndex,g=this.hasRendered,h=a.seriesGroup;b=this.plotGroup("group","series",e,f,h);this.markerGroup=this.plotGroup("markerGroup","markers",e,f,h);d&&this.animate(!0);this.getAttribs();b.inverted=a.inverted;this.drawGraph&&(this.drawGraph(),this.clipNeg());this.drawDataLabels();this.drawPoints();this.options.enableMouseTracking!==!1&&this.drawTracker();a.inverted&&this.invertGroups();c.clip!==!1&&!this.sharedClipKey&&!g&&b.clip(a.clipRect); -d?this.animate():g||this.afterAnimate();this.isDirty=this.isDirtyData=!1;this.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis,e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:o(d&&d.left,a.plotLeft),translateY:o(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints(!0);this.render();b&&D(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=this.graphNeg,e=b.states,b=b.lineWidth, -a=a||"";if(this.state!==a)this.state=a,e[a]&&e[a].enabled===!1||(a&&(b=e[a].lineWidth||b+1),c&&!c.dashstyle&&(a={"stroke-width":b},c.attr(a),d&&d.attr(a)))},setVisible:function(a,b){var c=this,d=c.chart,e=c.legendItem,f,g=d.options.chart.ignoreHiddenSeries,h=c.visible;f=(c.visible=a=c.userOptions.visible=a===x?!h:a)?"show":"hide";n(["group","dataLabelsGroup","markerGroup","tracker"],function(a){if(c[a])c[a][f]()});if(d.hoverSeries===c)c.onMouseOut();e&&d.legend.colorizeItem(c,a);c.isDirty=!0;c.options.stacking&& -n(d.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});n(c.linkedSeries,function(b){b.setVisible(a,!1)});if(g)d.isDirtyBox=!0;b!==!1&&d.redraw();D(c,f)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===x?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;D(this,a?"select":"unselect")},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer, -h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,k=k&&{cursor:k},m=a.singlePoints,l,n=function(){if(f.hoverSeries!==a)a.onMouseOver()};if(e&&!c)for(l=e+1;l--;)d[l]==="M"&&d.splice(l+1,0,d[l+1]-i,d[l+2],"L"),(l&&d[l]==="M"||l===e)&&d.splice(l,0,"L",d[l-2]+i,d[l-1]);for(l=0;l=0;d--)da&&i>e?(i=q(a,e),k=2*e-i):ig&&k>e?(k=q(g,e),i=2*e-k):kh?n-h:g-(f.translate(c.y,0,1,0,1)<=g?h:0));c.barX=s;c.pointWidth=i;c.shapeType="rect";c.shapeArgs=c=b.renderer.Element.prototype.crisp.call(0,e,s,t,j,l);e%2&&(c.y-=1,c.height+=1)})},getSymbol:ta,drawLegendSymbol:M.prototype.drawLegendSymbol, -drawGraph:ta,drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d;n(a.points,function(e){var f=e.plotY,g=e.graphic;if(f!==x&&!isNaN(f)&&e.y!==null)d=e.shapeArgs,g?(Ta(g),g.animate(y(d))):e.graphic=c[e.shapeType](d).attr(e.pointAttr[e.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius);else if(g)e.graphic=g.destroy()})},drawTracker:function(){var a=this,b=a.chart.pointer,c=a.options.cursor,d=c&&{cursor:c},e=function(b){var c=b.target,d;for(a.onMouseOver();c&& -!d;)d=c.point,c=c.parentNode;if(d!==x)d.onMouseOver(b)};n(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});a._hasTracking?a._hasTracking=!0:n(a.trackerGroups,function(c){if(a[c]&&(a[c].addClass("highcharts-tracker").on("mouseover",e).on("mouseout",function(a){b.onTrackerMouseOut(a)}).css(d),fb))a[c].on("touchstart",e)})},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=a.dlBox||a.shapeArgs,i=a.below||a.plotY>o(this.translatedThreshold, -f.plotSizeY),j=o(c.inside,!!this.options.stacking);if(h&&(d=y(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height:0,d.height=0);c.align=o(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=o(c.verticalAlign,g||j?"middle":i?"top":"bottom");R.prototype.alignDataLabel.call(this,a,b,c,d,e)},animate:function(a){var b=this.yAxis,c=this.options,d=this.chart.inverted,e={};if(Z)a?(e.scaleY=0.001,a=K(b.pos+ -b.len,q(b.pos,b.toPixels(c.threshold))),d?e.translateX=a-b.len:e.translateY=a,this.group.attr(e)):(e.scaleY=1,e[d?"translateX":"translateY"]=b.pos,this.group.animate(e,this.options.animation),this.animate=null)},remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});R.prototype.remove.apply(a,arguments)}});aa.column=F;X.bar=y(X.column);na=ea(F,{type:"bar",inverted:!0});aa.bar=na;X.scatter=y(W,{lineWidth:0,tooltip:{headerFormat:'{series.name}
', -pointFormat:"x: {point.x}
y: {point.y}
",followPointer:!0},stickyTracking:!1});na=ea(R,{type:"scatter",sorted:!1,requireSorting:!1,noSharedTooltip:!0,trackerGroups:["markerGroup"],drawTracker:F.prototype.drawTracker,setTooltipPoints:ta});aa.scatter=na;X.pie=y(W,{borderColor:"#FFFFFF",borderWidth:1,center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null, -showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});W={type:"pie",isCartesian:!1,pointClass:ea(Ma,{init:function(){Ma.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;v(a,{visible:a.visible!==!1,name:o(a.name,"Slice")});b=function(){a.slice()};J(a,"select",b);J(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart,e;b.visible=b.options.visible=a=a===x?!b.visible:a;c.options.data[la(b, -c.data)]=b.options;e=a?"show":"hide";n(["graphic","dataLabel","connector","shadowGroup"],function(a){if(b[a])b[a][e]()});b.legendItem&&d.legend.colorizeItem(b,a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;Ha(c,d.chart);o(b,!0);this.sliced=this.options.sliced=a=r(a)?a:!this.sliced;d.options.data[la(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}), -requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth",fill:"color"},getColor:ta,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b){R.prototype.setData.call(this,a,!1);this.processData();this.generatePoints(); -o(b,!0)&&this.chart.redraw()},getCenter:function(){var a=this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight-2*c,b=a.center,a=[o(b[0],"50%"),o(b[1],"50%"),a.size||"100%",a.innerSize||0],g=K(e,f),h;return Ka(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*u(a)/100:a)+(d?c:0)})},translate:function(a){this.generatePoints();var b=0,c=0,d=this.options,e=d.slicedOffset,f=e+d.borderWidth,g,h,i,j=this.startAngleRad=Ja/180*((d.startAngle||0)%360-90),k= -this.points,m=2*Ja,l=d.dataLabels.distance,n=d.ignoreHiddenPoint,o,q=k.length,r;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){i=I.asin((b-a[1])/(a[2]/2+l));return a[0]+(c?-1:1)*Y(i)*(a[2]/2+l)};for(o=0;o0.75*m&&(i-=2*Ja);r.slicedTranslation={translateX:t(Y(i)* -e),translateY:t(ca(i)*e)};g=Y(i)*a[2]/2;h=ca(i)*a[2]/2;r.tooltipPos=[a[0]+g*0.7,a[1]+h*0.7];r.half=i0,v,w,u,x,y=[[],[]],A,z,E,H,C,D=[0,0,0,0],K=function(a,b){return b.y-a.y},M=function(a,b){a.sort(function(a,c){return a.angle!==void 0&&(c.angle-a.angle)*b})};if(e.enabled||a._hasPointLabels){R.prototype.drawDataLabels.apply(a);n(b,function(a){a.dataLabel&&y[a.half].push(a)});for(H=0;!x&&b[H];)x=b[H]&&b[H].dataLabel&&(b[H].dataLabel.getBBox().height||21),H++;for(H=2;H--;){var b=[],L=[],I=y[H],J=I.length,F;M(I,H- -0.5);if(m>0){for(C=s-p-m;C<=s+p+m;C+=x)b.push(C);w=b.length;if(J>w){c=[].concat(I);c.sort(K);for(C=J;C--;)c[C].rank=C;for(C=J;C--;)I[C].rank>=w&&I.splice(C,1);J=I.length}for(C=0;C0){if(w=L.pop(), -F=w.i,z=w.y,c>z&&b[F+1]!==null||ch-f&&(D[1]=q(t(A+w-h+f),D[1])),z-x/2<0?D[0]=q(t(-z+x/2),D[0]):z+x/2>d&&(D[2]=q(t(z+x/2-d),D[2]))}}if(pa(D)===0||this.verifyDataLabelOverflow(D))this.placeDataLabels(),r&&g&&n(this.points,function(b){i= -b.connector;u=b.labelPos;if((v=b.dataLabel)&&v._pos)A=v.connX,z=v.connY,j=k?["M",A+(u[6]==="left"?5:-5),z,"C",A,z,2*u[2]-u[4],2*u[3]-u[5],u[2],u[3],"L",u[4],u[5]]:["M",A+(u[6]==="left"?5:-5),z,"L",u[2],u[3],"L",u[4],u[5]],i?(i.animate({d:j}),i.attr("visibility",E)):b.connector=i=a.chart.renderer.path(j).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:E}).add(a.group);else if(i)b.connector=i.destroy()})}},verifyDataLabelOverflow:function(a){var b=this.center,c=this.options, -d=c.center,e=c=c.minSize||80,f;d[0]!==null?e=q(b[2]-q(a[1],a[3]),c):(e=q(b[2]-a[1]-a[3],c),b[0]+=(a[3]-a[1])/2);d[1]!==null?e=q(K(e,b[2]-q(a[0],a[2])),c):(e=q(K(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);e3?c.length%3:0;return e+(g?c.substr(0,g)+d:"")+c.substr(g).replace(/(\d{3})(?=\d)/g,"$1"+d)+(f?b+U(a-c).toFixed(f).slice(2):"")}function Ka(a,b){return Array((b||2)+1-String(a).length).join(0)+a}function sa(a,b,c){var d=a[b];a[b]=function(){var a=Array.prototype.slice.call(arguments); -a.unshift(d);return c.apply(this,a)}}function La(a,b){for(var c="{",d=!1,e,f,g,h,i,j=[];(c=a.indexOf(c))!==-1;){e=a.slice(0,c);if(d){f=e.split(":");g=f.shift().split(".");i=g.length;e=b;for(h=0;h-1?h.thousandsSep:"")):e=ya(f,e)}j.push(e);a=a.slice(c+1);c=(d=!d)?"}":"{"}j.push(a);return j.join("")}function yb(a,b,c,d){var e,c=p(c,1);e=a/c;b||(b=[1, -2,2.5,5,10],d&&d.allowDecimals===!1&&(c===1?b=[1,2,5,10]:c<=0.1&&(b=[1/c])));for(d=0;d=I[eb]&&(i.setMilliseconds(0),i.setSeconds(j>=I[Ya]?0:k*W(i.getSeconds()/k)));if(j>=I[Ya])i[Nb](j>=I[za]?0:k*W(i[Ab]()/k));if(j>=I[za])i[Ob](j>=I[ga]?0:k*W(i[Bb]()/k));if(j>=I[ga])i[Cb](j>=I[Na]?1:k*W(i[Oa]()/k));j>=I[Na]&&(i[Pb](j>=I[ta]?0:k*W(i[lb]()/k)),h=i[mb]());j>=I[ta]&&(h-=h%k,i[Qb](h));if(j===I[Ma])i[Cb](i[Oa]()- -i[Db]()+p(d,1));b=1;h=i[mb]();for(var d=i.getTime(),m=i[lb](),l=i[Oa](),o=g?0:(864E5+i.getTimezoneOffset()*6E4)%864E5;dc&&(c=a[b]);return c}function Aa(a,b){for(var c in a)a[c]&&a[c]!==b&&a[c].destroy&&a[c].destroy(),delete a[c]}function Za(a){ob||(ob=aa(Qa));a&&ob.appendChild(a);ob.innerHTML=""}function Ba(a,b){var c="Highcharts error #"+a+": www.highcharts.com/errors/"+a;if(b)throw c;else Y.console&&console.log(c)}function oa(a){return parseFloat(a.toPrecision(14))} -function $a(a,b){Ra=p(a,b.animation)}function Tb(){var a=M.global.useUTC,b=a?"getUTC":"get",c=a?"setUTC":"set";nb=a?Date.UTC:function(a,b,c,g,h,i){return(new Date(a,b,p(c,1),p(g,0),p(h,0),p(i,0))).getTime()};Ab=b+"Minutes";Bb=b+"Hours";Db=b+"Day";Oa=b+"Date";lb=b+"Month";mb=b+"FullYear";Nb=c+"Minutes";Ob=c+"Hours";Cb=c+"Date";Pb=c+"Month";Qb=c+"FullYear"}function Ca(){}function ab(a,b,c,d){this.axis=a;this.pos=b;this.type=c||"";this.isNew=!0;!c&&!d&&this.addLabel()}function Fb(a,b){this.axis=a;if(b)this.options= -b,this.id=b.id}function Ub(a,b,c,d,e,f){var g=a.chart.inverted;this.axis=a;this.isNegative=c;this.options=b;this.x=d;this.stack=e;this.percent=f==="percent";this.alignOptions={align:b.align||(g?c?"left":"right":"center"),verticalAlign:b.verticalAlign||(g?"middle":c?"bottom":"top"),y:p(b.y,g?4:c?14:-6),x:p(b.x,g?c?-6:6:0)};this.textAlign=b.textAlign||(g?c?"right":"left":"center")}function Da(){this.init.apply(this,arguments)}function Gb(){this.init.apply(this,arguments)}function pb(a,b){this.init(a, -b)}function Hb(a,b){this.init(a,b)}function Sa(){this.init.apply(this,arguments)}function Ib(a){var b=a.options,c=b.navigator,d=c.enabled,b=b.scrollbar,e=b.enabled,f=d?c.height:0,g=e?b.height:0,h=c.baseSeries;this.baseSeries=a.series[h]||typeof h==="string"&&a.get(h)||a.series[0];this.handles=[];this.scrollbarButtons=[];this.elementsToDestroy=[];this.chart=a;this.height=f;this.scrollbarHeight=g;this.scrollbarEnabled=e;this.navigatorEnabled=d;this.navigatorOptions=c;this.scrollbarOptions=b;this.outlineHeight= -f+g;this.init()}function Jb(a){this.init(a)}var w,H=document,Y=window,P=Math,r=P.round,W=P.floor,pa=P.ceil,t=P.max,A=P.min,U=P.abs,ha=P.cos,ka=P.sin,bb=P.PI,qb=bb*2/360,Ta=navigator.userAgent,Vb=Y.opera,Xa=/msie/i.test(Ta)&&!Vb,rb=H.documentMode===8,sb=/AppleWebKit/.test(Ta),tb=/Firefox/.test(Ta),ub=/(Mobile|Android|Windows Phone)/.test(Ta),Ea="http://www.w3.org/2000/svg",ca=!!H.createElementNS&&!!H.createElementNS(Ea,"svg").createSVGRect,cc=tb&&parseInt(Ta.split("Firefox/")[1],10)<4,ia=!ca&&!Xa&& -!!H.createElement("canvas").getContext,cb,gb=H.documentElement.ontouchstart!==w,Wb={},Kb=0,ob,M,ya,Ra,Lb,I,qa=function(){},Ua=[],Qa="div",ba="none",Xb="rgba(192,192,192,"+(ca?1.0E-4:0.002)+")",kb="millisecond",eb="second",Ya="minute",za="hour",ga="day",Ma="week",Na="month",ta="year",Yb="stroke-width",nb,Ab,Bb,Db,Oa,lb,mb,Nb,Ob,Cb,Pb,Qb,Q={};Y.Highcharts=Y.Highcharts?Ba(16,!0):{};ya=function(a,b,c){if(!v(b)||isNaN(b))return"Invalid date";var a=p(a,"%Y-%m-%d %H:%M:%S"),d=new Date(b),e,f=d[Bb](),g=d[Db](), -h=d[Oa](),i=d[lb](),j=d[mb](),k=M.lang,m=k.weekdays,d=y({a:m[g].substr(0,3),A:m[g],d:Ka(h),e:h,b:k.shortMonths[i],B:k.months[i],m:Ka(i+1),y:j.toString().substr(2,2),Y:j,H:Ka(f),I:Ka(f%12||12),l:f%12||12,M:Ka(d[Ab]()),p:f<12?"AM":"PM",P:f<12?"am":"pm",S:Ka(d.getSeconds()),L:Ka(r(b%1E3),3)},Highcharts.dateFormats);for(e in d)for(;a.indexOf("%"+e)!==-1;)a=a.replace("%"+e,typeof d[e]==="function"?d[e](b):d[e]);return c?a.substr(0,1).toUpperCase()+a.substr(1):a};Rb.prototype={wrapColor:function(a){if(this.color>= -a)this.color=0},wrapSymbol:function(a){if(this.symbol>=a)this.symbol=0}};I=jb(kb,1,eb,1E3,Ya,6E4,za,36E5,ga,864E5,Ma,6048E5,Na,26784E5,ta,31556952E3);Lb={init:function(a,b,c){var b=b||"",d=a.shift,e=b.indexOf("C")>-1,f=e?7:3,g,b=b.split(" "),c=[].concat(c),h,i,j=function(a){for(g=a.length;g--;)a[g]==="M"&&a.splice(g+1,0,a[g+1],a[g+2],a[g+1],a[g+2])};e&&(j(b),j(c));a.isArea&&(h=b.splice(b.length-6,6),i=c.splice(c.length-6,6));if(d<=c.length/f)for(;d--;)c=[].concat(c).splice(0,f).concat(c);a.shift= -0;if(b.length)for(a=c.length;b.length{point.key}

',pointFormat:'{series.name}: {point.y}
',shadow:!0,snap:ub?25:10,style:{color:"#333333",cursor:"default",fontSize:"12px",padding:"8px",whiteSpace:"nowrap"}},credits:{enabled:!0,text:"Highcharts.com",href:"http://www.highcharts.com",position:{align:"right",x:-10,verticalAlign:"bottom",y:-5},style:{cursor:"pointer", -color:"#909090",fontSize:"9px"}}};var S=M.plotOptions,R=S.line;Tb();var wa=function(a){var b=[],c,d;(function(a){a&&a.stops?d=Fa(a.stops,function(a){return wa(a[1])}):(c=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:\.[0-9]+)?)\s*\)/.exec(a))?b=[C(c[1]),C(c[2]),C(c[3]),parseFloat(c[4],10)]:(c=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(a))?b=[C(c[1],16),C(c[2],16),C(c[3],16),1]:(c=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(a))&& -(b=[C(c[1]),C(c[2]),C(c[3]),1])})(a);return{get:function(c){var f;d?(f=z(a),f.stops=[].concat(f.stops),n(d,function(a,b){f.stops[b]=[f.stops[b][0],a.get(c)]})):f=b&&!isNaN(b[0])?c==="rgb"?"rgb("+b[0]+","+b[1]+","+b[2]+")":c==="a"?b[3]:"rgba("+b.join(",")+")":a;return f},brighten:function(a){if(d)n(d,function(b){b.brighten(a)});else if(Ja(a)&&a!==0){var c;for(c=0;c<3;c++)b[c]+=C(a*255),b[c]<0&&(b[c]=0),b[c]>255&&(b[c]=255)}return this},rgba:b,setOpacity:function(a){b[3]=a;return this}}};Ca.prototype= -{init:function(a,b){this.element=b==="span"?aa(b):H.createElementNS(Ea,b);this.renderer=a;this.attrSetters={}},opacity:1,animate:function(a,b,c){b=p(b,Ra,!0);hb(this);if(b){b=z(b);if(c)b.complete=c;Mb(this,a,b)}else this.attr(a),c&&c()},attr:function(a,b){var c,d,e,f,g=this.element,h=g.nodeName.toLowerCase(),i=this.renderer,j,k=this.attrSetters,m=this.shadows,l,o,q=this;ma(a)&&v(b)&&(c=a,a={},a[c]=b);if(ma(a))c=a,h==="circle"?c={x:"cx",y:"cy"}[c]||c:c==="strokeWidth"&&(c="stroke-width"),q=G(g,c)|| -this[c]||0,c!=="d"&&c!=="visibility"&&(q=parseFloat(q));else{for(c in a)if(j=!1,d=a[c],e=k[c]&&k[c].call(this,d,c),e!==!1){e!==w&&(d=e);if(c==="d")d&&d.join&&(d=d.join(" ")),/(NaN| {2}|^$)/.test(d)&&(d="M 0 0");else if(c==="x"&&h==="text")for(e=0;el&&/[ \-]/.test(b.textContent||b.innerText))L(b,{width:l+"px",display:"block",whiteSpace:"normal"}),k=l;l=a.fontMetrics(b.style.fontSize).b;u=q<0&&-k;s=o<0&&-m;O=q*o<0;u+=o*l*(O?1-h:h);s-=q*l*(j? -O?h:1-h:1);i&&(u-=k*h*(q<0?-1:1),j&&(s-=m*h*(o<0?-1:1)),L(b,{textAlign:g}));this.xCorr=u;this.yCorr=s}L(b,{left:e+u+"px",top:f+s+"px"});if(sb)m=b.offsetHeight;this.cTT=x}}else this.alignOnAdd=!0},updateTransform:function(){var a=this.translateX||0,b=this.translateY||0,c=this.scaleX,d=this.scaleY,e=this.inverted,f=this.rotation;e&&(a+=this.attr("width"),b+=this.attr("height"));a=["translate("+a+","+b+")"];e?a.push("rotate(90) scale(-1,1)"):f&&a.push("rotate("+f+" "+(this.x||0)+" "+(this.y||0)+")"); -(v(c)||v(d))&&a.push("scale("+p(c,1)+" "+p(d,1)+")");a.length&&G(this.element,"transform",a.join(" "))},toFront:function(){var a=this.element;a.parentNode.appendChild(a);return this},align:function(a,b,c){var d,e,f,g,h={};e=this.renderer;f=e.alignedObjects;if(a){if(this.alignOptions=a,this.alignByTranslate=b,!c||ma(c))this.alignTo=d=c||"renderer",na(f,this),f.push(this),c=null}else a=this.alignOptions,b=this.alignByTranslate,d=this.alignTo;c=p(c,e[d],e);d=a.align;e=a.verticalAlign;f=(c.x||0)+(a.x|| -0);g=(c.y||0)+(a.y||0);if(d==="right"||d==="center")f+=(c.width-(a.width||0))/{right:1,center:2}[d];h[b?"translateX":"x"]=r(f);if(e==="bottom"||e==="middle")g+=(c.height-(a.height||0))/({bottom:1,middle:2}[e]||1);h[b?"translateY":"y"]=r(g);this[this.placed?"animate":"attr"](h);this.placed=!0;this.alignAttr=h;return this},getBBox:function(){var a=this.bBox,b=this.renderer,c,d=this.rotation;c=this.element;var e=this.styles,f=d*qb;if(!a){if(c.namespaceURI===Ea||b.forExport){try{a=c.getBBox?y({},c.getBBox()): -{width:c.offsetWidth,height:c.offsetHeight}}catch(g){}if(!a||a.width<0)a={width:0,height:0}}else a=this.htmlGetBBox();if(b.isSVG){b=a.width;c=a.height;if(Xa&&e&&e.fontSize==="11px"&&c.toPrecision(3)==="22.7")a.height=c=14;if(d)a.width=U(c*ka(f))+U(b*ha(f)),a.height=U(c*ha(f))+U(b*ka(f))}this.bBox=a}return a},show:function(){return this.attr({visibility:"visible"})},hide:function(){return this.attr({visibility:"hidden"})},fadeOut:function(a){var b=this;b.animate({opacity:0},{duration:a||150,complete:function(){b.hide()}})}, -add:function(a){var b=this.renderer,c=a||b,d=c.element||b.box,e=d.childNodes,f=this.element,g=G(f,"zIndex"),h;if(a)this.parentGroup=a;this.parentInverted=a&&a.inverted;this.textStr!==void 0&&b.buildText(this);if(g)c.handleZ=!0,g=C(g);if(c.handleZ)for(c=0;cg||!v(g)&&v(b))){d.insertBefore(f,a);h=!0;break}h||d.appendChild(f);this.added=!0;K(this,"add");return this},safeRemoveChild:function(a){var b=a.parentNode;b&&b.removeChild(a)},destroy:function(){var a= -this,b=a.element||{},c=a.shadows,d,e;b.onclick=b.onmouseout=b.onmouseover=b.onmousemove=b.point=null;hb(a);if(a.clipPath)a.clipPath=a.clipPath.destroy();if(a.stops){for(e=0;e/g,'').replace(/<(i|em)>/g,'').replace(/
/g,"").split(//g),f=b.childNodes,g=/style="([^"]+)"/,h=/href="([^"]+)"/,i=G(b,"x"),j=a.styles,k=j&&j.width&&C(j.width),m=j&&j.lineHeight,l=f.length;l--;)b.removeChild(f[l]);k&&!a.added&&this.box.appendChild(b); -e[e.length-1]===""&&e.pop();n(e,function(e,f){var l,u=0,e=e.replace(//g,"|||");l=e.split("|||");n(l,function(e){if(e!==""||l.length===1){var o={},n=H.createElementNS(Ea,"tspan"),p;g.test(e)&&(p=e.match(g)[1].replace(/(;| |^)color([ :])/,"$1fill$2"),G(n,"style",p));h.test(e)&&!d&&(G(n,"onclick",'location.href="'+e.match(h)[1]+'"'),L(n,{cursor:"pointer"}));e=(e.replace(/<(.|\n)*?>/g,"")||" ").replace(/</g,"<").replace(/>/g,">");n.appendChild(H.createTextNode(e)); -u?o.dx=0:o.x=i;G(n,o);!u&&f&&(!ca&&d&&L(n,{display:"block"}),G(n,"dy",m||c.fontMetrics(/px$/.test(n.style.fontSize)?n.style.fontSize:j.fontSize).h,sb&&n.offsetHeight));b.appendChild(n);u++;if(k)for(var e=e.replace(/([^\^])-/g,"$1- ").split(" "),E,B=[];e.length||B.length;)delete a.bBox,E=a.getBBox().width,o=E>k,!o||e.length===1?(e=B,B=[],e.length&&(n=H.createElementNS(Ea,"tspan"),G(n,{dy:m||16,x:i}),p&&G(n,"style",p),b.appendChild(n),E>k&&(k=E))):(n.removeChild(n.firstChild),B.unshift(e.pop())),e.length&& -n.appendChild(H.createTextNode(e.join(" ").replace(/- /g,"-")))}})})},button:function(a,b,c,d,e,f,g){var h=this.label(a,b,c,null,null,null,null,null,"button"),i=0,j,k,m,l,o,a={x1:0,y1:0,x2:0,y2:1},e=z({"stroke-width":1,stroke:"#CCCCCC",fill:{linearGradient:a,stops:[[0,"#FEFEFE"],[1,"#F6F6F6"]]},r:2,padding:5,style:{color:"black"}},e);m=e.style;delete e.style;f=z(e,{stroke:"#68A",fill:{linearGradient:a,stops:[[0,"#FFF"],[1,"#ACF"]]}},f);l=f.style;delete f.style;g=z(e,{stroke:"#68A",fill:{linearGradient:a, -stops:[[0,"#9BD"],[1,"#CDF"]]}},g);o=g.style;delete g.style;F(h.element,"mouseenter",function(){h.attr(f).css(l)});F(h.element,"mouseleave",function(){j=[e,f,g][i];k=[m,l,o][i];h.attr(j).css(k)});h.setState=function(a){(i=a)?a===2&&h.attr(g).css(o):h.attr(e).css(m)};return h.on("click",function(){d.call(h)}).attr(e).css(y({cursor:"default"},m))},crispLine:function(a,b){a[1]===a[4]&&(a[1]=a[4]=r(a[1])-b%2/2);a[2]===a[5]&&(a[2]=a[5]=r(a[2])+b%2/2);return a},path:function(a){var b={fill:ba};Wa(a)?b.d= -a:da(a)&&y(b,a);return this.createElement("path").attr(b)},circle:function(a,b,c){a=da(a)?a:{x:a,y:b,r:c};return this.createElement("circle").attr(a)},arc:function(a,b,c,d,e,f){if(da(a))b=a.y,c=a.r,d=a.innerR,e=a.start,f=a.end,a=a.x;return this.symbol("arc",a||0,b||0,c||0,c||0,{innerR:d||0,start:e||0,end:f||0})},rect:function(a,b,c,d,e,f){e=da(a)?a.r:e;e=this.createElement("rect").attr({rx:e,ry:e,fill:ba});return e.attr(da(a)?a:e.crisp(f,a,b,t(c,0),t(d,0)))},setSize:function(a,b,c){var d=this.alignedObjects, -e=d.length;this.width=a;this.height=b;for(this.boxWrapper[p(c,!0)?"animate":"attr"]({width:a,height:b});e--;)d[e].align()},g:function(a){var b=this.createElement("g");return v(a)?b.attr({"class":"highcharts-"+a}):b},image:function(a,b,c,d,e){var f={preserveAspectRatio:ba};arguments.length>1&&y(f,{x:b,y:c,width:d,height:e});f=this.createElement("image").attr(f);f.element.setAttributeNS?f.element.setAttributeNS("http://www.w3.org/1999/xlink","href",a):f.element.setAttribute("hc-svg-href",a);return f}, -symbol:function(a,b,c,d,e,f){var g,h=this.symbols[a],h=h&&h(r(b),r(c),d,e,f),i=/^url\((.*?)\)$/,j,k;if(h)g=this.path(h),y(g,{symbolName:a,x:b,y:c,width:d,height:e}),f&&y(g,f);else if(i.test(a))k=function(a,b){a.element&&(a.attr({width:b[0],height:b[1]}),a.alignByTranslate||a.translate(r((d-b[0])/2),r((e-b[1])/2)))},j=a.match(i)[1],a=Wb[j],g=this.image(j).attr({x:b,y:c}),g.isImg=!0,a?k(g,a):(g.attr({width:0,height:0}),aa("img",{onload:function(){k(g,Wb[j]=[this.width,this.height])},src:j}));return g}, -symbols:{circle:function(a,b,c,d){var e=0.166*c;return["M",a+c/2,b,"C",a+c+e,b,a+c+e,b+d,a+c/2,b+d,"C",a-e,b+d,a-e,b,a+c/2,b,"Z"]},square:function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c,b+d,a,b+d,"Z"]},triangle:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d,a,b+d,"Z"]},"triangle-down":function(a,b,c,d){return["M",a,b,"L",a+c,b,a+c/2,b+d,"Z"]},diamond:function(a,b,c,d){return["M",a+c/2,b,"L",a+c,b+d/2,a+c/2,b+d,a,b+d/2,"Z"]},arc:function(a,b,c,d,e){var f=e.start,c=e.r||c||d,g=e.end-0.001,d=e.innerR, -h=e.open,i=ha(f),j=ka(f),k=ha(g),g=ka(g),e=e.end-f');if(b)c=e||b==="span"||b==="img"?c.join(""):a.prepVML(c),this.element=aa(c);this.renderer=a;this.attrSetters={}},add:function(a){var b=this.renderer, -c=this.element,d=b.box,d=a?a.element||a:d;a&&a.inverted&&b.invertChild(c,d);d.appendChild(c);this.added=!0;this.alignOnAdd&&!this.deferUpdateTransform&&this.updateTransform();K(this,"add");return this},updateTransform:Ca.prototype.htmlUpdateTransform,attr:function(a,b){var c,d,e,f=this.element||{},g=f.style,h=f.nodeName,i=this.renderer,j=this.symbolName,k,m=this.shadows,l,o=this.attrSetters,q=this;ma(a)&&v(b)&&(c=a,a={},a[c]=b);if(ma(a))c=a,q=c==="strokeWidth"||c==="stroke-width"?this.strokeweight: -this[c];else for(c in a)if(d=a[c],l=!1,e=o[c]&&o[c].call(this,d,c),e!==!1&&d!==null){e!==w&&(d=e);if(j&&/^(x|y|r|start|end|width|height|innerR|anchorX|anchorY)/.test(c))k||(this.symbolAttr(a),k=!0),l=!0;else if(c==="d"){d=d||[];this.d=d.join(" ");e=d.length;l=[];for(var n;e--;)if(Ja(d[e]))l[e]=r(d[e]*10)-5;else if(d[e]==="Z")l[e]="x";else if(l[e]=d[e],d.isArc&&(d[e]==="wa"||d[e]==="at"))n=d[e]==="wa"?1:-1,l[e+5]===l[e+7]&&(l[e+7]-=n),l[e+6]===l[e+8]&&(l[e+8]-=n);d=l.join(" ")||"x";f.path=d;if(m)for(e= -m.length;e--;)m[e].path=m[e].cutOff?this.cutOffPath(d,m[e].cutOff):d;l=!0}else if(c==="visibility"){if(m)for(e=m.length;e--;)m[e].style[c]=d;h==="DIV"&&(d=d==="hidden"?"-999em":0,rb||(g[c]=d?"visible":"hidden"),c="top");g[c]=d;l=!0}else if(c==="zIndex")d&&(g[c]=d),l=!0;else if(va(c,["x","y","width","height"])!==-1)this[c]=d,c==="x"||c==="y"?c={x:"left",y:"top"}[c]:d=t(0,d),this.updateClipping?(this[c]=d,this.updateClipping()):g[c]=d,l=!0;else if(c==="class"&&h==="DIV")f.className=d;else if(c==="stroke")d= -i.color(d,f,c),c="strokecolor";else if(c==="stroke-width"||c==="strokeWidth")f.stroked=d?!0:!1,c="strokeweight",this[c]=d,Ja(d)&&(d+="px");else if(c==="dashstyle")(f.getElementsByTagName("stroke")[0]||aa(i.prepVML([""]),null,null,f))[c]=d||"solid",this.dashstyle=d,l=!0;else if(c==="fill")if(h==="SPAN")g.color=d;else{if(h!=="IMG")f.filled=d!==ba?!0:!1,d=i.color(d,f,c,this),c="fillcolor"}else if(c==="opacity")l=!0;else if(h==="shape"&&c==="rotation")this[c]=d,f.style.left=-r(ka(d*qb)+1)+"px", -f.style.top=r(ha(d*qb))+"px";else if(c==="translateX"||c==="translateY"||c==="rotation")this[c]=d,this.updateTransform(),l=!0;else if(c==="text")this.bBox=null,f.innerHTML=d,l=!0;l||(rb?f[c]=d:G(f,c,d))}return q},clip:function(a){var b=this,c;a?(c=a.members,na(c,b),c.push(b),b.destroyClip=function(){na(c,b)},a=a.getCSS(b)):(b.destroyClip&&b.destroyClip(),a={clip:rb?"inherit":"rect(auto)"});return b.css(a)},css:Ca.prototype.htmlCss,safeRemoveChild:function(a){a.parentNode&&Za(a)},destroy:function(){this.destroyClip&& -this.destroyClip();return Ca.prototype.destroy.apply(this)},on:function(a,b){this.element["on"+a]=function(){var a=Y.event;a.target=a.srcElement;b(a)};return this},cutOffPath:function(a,b){var c,a=a.split(/[ ,]/);c=a.length;if(c===9||c===11)a[c-4]=a[c-2]=C(a[c-2])-10*b;return a.join(" ")},shadow:function(a,b,c){var d=[],e,f=this.element,g=this.renderer,h,i=f.style,j,k=f.path,m,l,o,q;k&&typeof k.value!=="string"&&(k="x");l=k;if(a){o=p(a.width,3);q=(a.opacity||0.15)/o;for(e=1;e<=3;e++){m=o*2+1-2*e; -c&&(l=this.cutOffPath(k.value,m+0.5));j=[''];h=aa(g.prepVML(j),null,{left:C(i.left)+p(a.offsetX,1),top:C(i.top)+p(a.offsetY,1)});if(c)h.cutOff=m+1;j=[''];aa(g.prepVML(j),null,null,h);b?b.element.appendChild(h):f.parentNode.insertBefore(h,f);d.push(h)}this.shadows=d}return this}},Z=ea(Ca,Z),Z={Element:Z,isIE8:Ta.indexOf("MSIE 8.0")> --1,init:function(a,b,c){var d,e;this.alignedObjects=[];d=this.createElement(Qa);e=d.element;e.style.position="relative";a.appendChild(d.element);this.isVML=!0;this.box=e;this.boxWrapper=d;this.setSize(b,c,!1);if(!H.namespaces.hcv)H.namespaces.add("hcv","urn:schemas-microsoft-com:vml"),H.createStyleSheet().cssText="hcv\\:fill, hcv\\:path, hcv\\:shape, hcv\\:stroke{ behavior:url(#default#VML); display: inline-block; } "},isHidden:function(){return!this.box.offsetWidth},clipRect:function(a,b,c,d){var e= -this.createElement(),f=da(a);return y(e,{members:[],left:f?a.x:a,top:f?a.y:b,width:f?a.width:c,height:f?a.height:d,getCSS:function(a){var b=a.element,c=b.nodeName,a=a.inverted,d=this.top-(c==="shape"?b.offsetTop:0),e=this.left,b=e+this.width,f=d+this.height,d={clip:"rect("+r(a?e:d)+"px,"+r(a?f:b)+"px,"+r(a?b:f)+"px,"+r(a?d:e)+"px)"};!a&&rb&&c==="DIV"&&y(d,{width:b+"px",height:f+"px"});return d},updateClipping:function(){n(e.members,function(a){a.css(e.getCSS(a))})}})},color:function(a,b,c,d){var e= -this,f,g=/^rgba/,h,i,j=ba;a&&a.linearGradient?i="gradient":a&&a.radialGradient&&(i="pattern");if(i){var k,m,l=a.linearGradient||a.radialGradient,o,q,p,u,s,x="",a=a.stops,N,fa=[],E=function(){h=[''];aa(e.prepVML(h),null,null,b)};o=a[0];N=a[a.length-1];o[0]>0&&a.unshift([0,o[1]]);N[0]<1&&a.push([1,N[1]]);n(a,function(a,b){g.test(a[1])?(f=wa(a[1]),k=f.get("rgb"),m=f.get("a")):(k=a[1],m=1); -fa.push(a[0]*100+"% "+k);b?(p=m,u=k):(q=m,s=k)});if(c==="fill")if(i==="gradient")c=l.x1||l[0]||0,a=l.y1||l[1]||0,o=l.x2||l[2]||0,l=l.y2||l[3]||0,x='angle="'+(90-P.atan((l-a)/(o-c))*180/bb)+'"',E();else{var j=l.r,B=j*2,T=j*2,t=l.cx,w=l.cy,v=b.radialReference,r,j=function(){v&&(r=d.getBBox(),t+=(v[0]-r.x)/r.width-0.5,w+=(v[1]-r.y)/r.height-0.5,B*=v[2]/r.width,T*=v[2]/r.height);x='src="'+M.global.VMLRadialGradientURL+'" size="'+B+","+T+'" origin="0.5,0.5" position="'+t+","+w+'" color2="'+s+'" ';E()}; -d.added?j():F(d,"add",j);j=u}else j=k}else if(g.test(a)&&b.tagName!=="IMG")f=wa(a),h=["<",c,' opacity="',f.get("a"),'"/>'],aa(this.prepVML(h),null,null,b),j=f.get("rgb");else{j=b.getElementsByTagName(c);if(j.length)j[0].opacity=1,j[0].type="solid";j=a}return j},prepVML:function(a){var b=this.isIE8,a=a.join("");b?(a=a.replace("/>",' xmlns="urn:schemas-microsoft-com:vml" />'),a=a.indexOf('style="')===-1?a.replace("/>",' style="display:inline-block;behavior:url(#default#VML);" />'):a.replace('style="', -'style="display:inline-block;behavior:url(#default#VML);')):a=a.replace("<","1&&f.attr({x:b,y:c,width:d,height:e});return f},rect:function(a,b,c,d,e,f){if(da(a))b=a.y,c=a.width,d=a.height,f=a.strokeWidth,a=a.x;var g=this.symbol("rect");g.r=e;return g.attr(g.crisp(f,a,b,t(c,0),t(d,0)))},invertChild:function(a,b){var c=b.style;L(a,{flip:"x",left:C(c.width)-1,top:C(c.height)-1,rotation:-90})},symbols:{arc:function(a,b,c,d,e){var f=e.start,g=e.end,h=e.r||c||d,c=e.innerR,d=ha(f),i=ka(f),j=ha(g),k=ka(g);if(g- -f===0)return["x"];f=["wa",a-h,b-h,a+h,b+h,a+h*d,b+h*i,a+h*j,b+h*k];e.open&&!c&&f.push("e","M",a,b);f.push("at",a-c,b-c,a+c,b+c,a+c*j,b+c*k,a+c*d,b+c*i,"x","e");f.isArc=!0;return f},circle:function(a,b,c,d,e){e&&e.isCircle&&(a-=c/2,b-=d/2);return["wa",a,b,a+c,b+d,a+c,b+d/2,a+c,b+d/2,"e"]},rect:function(a,b,c,d,e){var f=a+c,g=b+d,h;!v(e)||!e.r?f=Ga.prototype.symbols.square.apply(0,arguments):(h=A(e.r,c,d),f=["M",a+h,b,"L",f-h,b,"wa",f-2*h,b,f,b+2*h,f-h,b,f,b+h,"L",f,g-h,"wa",f-2*h,g-2*h,f,g,f,g-h,f- -h,g,"L",a+h,g,"wa",a,g-2*h,a+2*h,g,a+h,g,a,g-h,"L",a,b+h,"wa",a,b,a+2*h,b+2*h,a,b+h,a+h,b,"x","e"]);return f}}},Highcharts.VMLRenderer=ib=function(){this.init.apply(this,arguments)},ib.prototype=z(Ga.prototype,Z),cb=ib;var $b;if(ia)Highcharts.CanVGRenderer=Z=function(){Ea="http://www.w3.org/1999/xhtml"},Z.prototype.symbols={},$b=function(){function a(){var a=b.length,d;for(d=0;dj&&(c=!1)):h+k>l&&(h=l-k,d&&h+m0&&b.height>0){f=z({align:c&&k&&"center",x:c?!k&&4:10,verticalAlign:!c&&k&&"middle",y:c?k?16:10:k?6:-4,rotation:c&&!k&&90},f);if(!g)a.label=g=N.text(f.text,0,0).attr({align:f.textAlign||f.align,rotation:f.rotation,zIndex:s}).css(f.style).add();b=[q[1],q[4],p(q[6],q[1])];q=[q[2],q[5],p(q[7],q[2])];c=Pa(b);k=Pa(q);g.align(f,!1,{x:c,y:k,width:ua(b)-c,height:ua(q)-k});g.show()}else g&&g.hide(); -return a},destroy:function(){na(this.axis.plotLinesAndBands,this);Aa(this,this.axis)}};Ub.prototype={destroy:function(){Aa(this,this.axis)},setTotal:function(a){this.cum=this.total=a},render:function(a){var b=this.options,c=b.format,c=c?La(c,this):b.formatter.call(this);this.label?this.label.attr({text:c,visibility:"hidden"}):this.label=this.axis.chart.renderer.text(c,0,0,b.useHTML).css(b.style).attr({align:this.textAlign,rotation:b.rotation,visibility:"hidden"}).add(a)},setOffset:function(a,b){var c= -this.axis,d=c.chart,e=d.inverted,f=this.isNegative,g=c.translate(this.percent?100:this.total,0,0,0,1),c=c.translate(0),c=U(g-c),h=d.xAxis[0].translate(this.x)+a,i=d.plotHeight,f={x:e?f?g:g-c:h,y:e?i-h-b:f?i-g-c:i-g,width:e?c:b,height:e?b:c};if(e=this.label)e.align(this.alignOptions,null,f),f=e.alignAttr,e.attr({visibility:this.options.crop===!1||d.isInsidePlot(f.x,f.y)?ca?"inherit":"visible":"hidden"})}};Da.prototype={defaultOptions:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S", -minute:"%H:%M",hour:"%H:%M",day:"%e. %b",week:"%e. %b",month:"%b '%y",year:"%Y"},endOnTick:!1,gridLineColor:"#C0C0C0",labels:D,lineColor:"#C0D0E0",lineWidth:1,minPadding:0.01,maxPadding:0.01,minorGridLineColor:"#E0E0E0",minorGridLineWidth:1,minorTickColor:"#A0A0A0",minorTickLength:2,minorTickPosition:"outside",startOfWeek:1,startOnTick:!1,tickColor:"#C0D0E0",tickLength:5,tickmarkPlacement:"between",tickPixelInterval:100,tickPosition:"outside",tickWidth:1,title:{align:"middle",style:{color:"#4d759e", -fontWeight:"bold"}},type:"linear"},defaultYAxisOptions:{endOnTick:!0,gridLineWidth:1,tickPixelInterval:72,showLastLabel:!0,labels:{align:"right",x:-8,y:3},lineWidth:0,maxPadding:0.05,minPadding:0.05,startOnTick:!0,tickWidth:0,title:{rotation:270,text:"Values"},stackLabels:{enabled:!1,formatter:function(){return xa(this.total,-1)},style:D.style}},defaultLeftAxisOptions:{labels:{align:"right",x:-8,y:null},title:{rotation:270}},defaultRightAxisOptions:{labels:{align:"left",x:8,y:null},title:{rotation:90}}, -defaultBottomAxisOptions:{labels:{align:"center",x:0,y:14},title:{rotation:0}},defaultTopAxisOptions:{labels:{align:"center",x:0,y:-5},title:{rotation:0}},init:function(a,b){var c=b.isX;this.horiz=a.inverted?!c:c;this.xOrY=(this.isXAxis=c)?"x":"y";this.opposite=b.opposite;this.side=this.horiz?this.opposite?0:2:this.opposite?1:3;this.setOptions(b);var d=this.options,e=d.type;this.labelFormatter=d.labels.formatter||this.defaultLabelFormatter;this.staggerLines=this.horiz&&d.labels.staggerLines;this.userOptions= -b;this.minPixelPadding=0;this.chart=a;this.reversed=d.reversed;this.zoomEnabled=d.zoomEnabled!==!1;this.categories=d.categories||e==="category";this.isLog=e==="logarithmic";this.isDatetimeAxis=e==="datetime";this.isLinked=v(d.linkedTo);this.tickmarkOffset=this.categories&&d.tickmarkPlacement==="between"?0.5:0;this.ticks={};this.minorTicks={};this.plotLinesAndBands=[];this.alternateBands={};this.len=0;this.minRange=this.userMinRange=d.minRange||d.maxZoom;this.range=d.range;this.offset=d.offset||0; -this.stacks={};this._stacksTouched=0;this.min=this.max=null;var f,d=this.options.events;va(this,a.axes)===-1&&(a.axes.push(this),a[c?"xAxis":"yAxis"].push(this));this.series=this.series||[];if(a.inverted&&c&&this.reversed===w)this.reversed=!0;this.removePlotLine=this.removePlotBand=this.removePlotBandOrLine;for(f in d)F(this,f,d[f]);if(this.isLog)this.val2lin=ra,this.lin2val=la},setOptions:function(a){this.options=z(this.defaultOptions,this.isXAxis?{}:this.defaultYAxisOptions,[this.defaultTopAxisOptions, -this.defaultRightAxisOptions,this.defaultBottomAxisOptions,this.defaultLeftAxisOptions][this.side],z(M[this.isXAxis?"xAxis":"yAxis"],a))},update:function(a,b){var c=this.chart,a=c.options[this.xOrY+"Axis"][this.options.index]=z(this.userOptions,a);this.destroy();this._addedPlotLB=!1;this.init(c,a);c.isDirtyBox=!0;p(b,!0)&&c.redraw()},remove:function(a){var b=this.chart,c=this.xOrY+"Axis";n(this.series,function(a){a.remove(!1)});na(b.axes,this);na(b[c],this);b.options[c].splice(this.options.index, -1);n(b[c],function(a,b){a.options.index=b});this.destroy();b.isDirtyBox=!0;p(a,!0)&&b.redraw()},defaultLabelFormatter:function(){var a=this.axis,b=this.value,c=a.categories,d=this.dateTimeLabelFormat,e=M.lang.numericSymbols,f=e&&e.length,g,h=a.options.labels.format,a=a.isLog?b:a.tickInterval;if(h)g=La(h,this);else if(c)g=b;else if(d)g=ya(d,b);else if(f&&a>=1E3)for(;f--&&g===w;)c=Math.pow(1E3,f+1),a>=c&&e[f]!==null&&(g=xa(b/c,-1)+e[f]);g===w&&(g=b>=1E3?xa(b,0):xa(b,-1));return g},getSeriesExtremes:function(){var a= -this,b=a.chart,c=a.stacks,d=[],e=[],f=a._stacksTouched+=1,g,h;a.hasVisibleSeries=!1;a.dataMin=a.dataMax=null;n(a.series,function(g){if(g.visible||!b.options.chart.ignoreHiddenSeries){var j=g.options,k,m,l,o,q,n,u,s,x,N=j.threshold,fa,E=[],B=0;a.hasVisibleSeries=!0;if(a.isLog&&N<=0)N=j.threshold=null;if(a.isXAxis){if(j=g.xData,j.length)a.dataMin=A(p(a.dataMin,j[0]),Pa(j)),a.dataMax=t(p(a.dataMax,j[0]),ua(j))}else{var T,r,J,z=g.cropped,y=g.xAxis.getExtremes(),C=!!g.modifyValue;k=j.stacking;a.usePercentage= -k==="percent";if(k)q=j.stack,o=g.type+p(q,""),n="-"+o,g.stackKey=o,m=d[o]||[],d[o]=m,l=e[n]||[],e[n]=l;if(a.usePercentage)a.dataMin=0,a.dataMax=99;j=g.processedXData;u=g.processedYData;fa=u.length;for(h=0;h0))if(C&&(x=g.modifyValue(x)),g.getExtremesFromAll|| -z||(j[h+1]||s)>=y.min&&(j[h-1]||s)<=y.max)if(s=x.length)for(;s--;)x[s]!==null&&(E[B++]=x[s]);else E[B++]=x}if(!a.usePercentage&&E.length)g.dataMin=k=Pa(E),g.dataMax=g=ua(E),a.dataMin=A(p(a.dataMin,k),k),a.dataMax=t(p(a.dataMax,g),g);if(v(N))if(a.dataMin>=N)a.dataMin=N,a.ignoreMinPadding=!0;else if(a.dataMaxf+this.width)l=!0}else if(c=f,i=m-this.right,hg+this.height)l=!0;return l&&!d?null:e.renderer.crispLine(["M",c,h,"L",i,j],b||0)},getPlotBandPath:function(a,b){var c=this.getPlotLinePath(b),d=this.getPlotLinePath(a);d&&c?d.push(c[4], -c[5],c[1],c[2]):d=null;return d},getLinearTickPositions:function(a,b,c){for(var d,b=oa(W(b/a)*a),c=oa(pa(c/a)*a),e=[];b<=c;){e.push(b);b=oa(b+a);if(b===d)break;d=b}return e},getLogTickPositions:function(a,b,c,d){var e=this.options,f=this.len,g=[];if(!d)this._minorAutoInterval=null;if(a>=0.5)a=r(a),g=this.getLinearTickPositions(a,b,c);else if(a>=0.08)for(var f=W(b),h,i,j,k,m,e=a>0.3?[1,2,4]:a>0.15?[1,2,4,6,8]:[1,2,3,4,5,6,7,8,9];fb&&(!d|| -k<=c)&&g.push(k),k>c&&(m=!0),k=j}else if(b=la(b),c=la(c),a=e[d?"minorTickInterval":"tickInterval"],a=p(a==="auto"?null:a,this._minorAutoInterval,(c-b)*(e.tickPixelInterval/(d?5:1))/((d?f/this.tickPositions.length:f)||1)),a=yb(a,null,P.pow(10,W(P.log(a)/P.LN10))),g=Fa(this.getLinearTickPositions(a,b,c),ra),!d)this._minorAutoInterval=a/5;if(!d)this.tickInterval=a;return g},getMinorTickPositions:function(){var a=this.options,b=this.tickPositions,c=this.minorTickInterval,d=[],e;if(this.isLog){e=b.length; -for(a=1;a=this.minRange,f,g,h,i,j;if(this.isXAxis&&this.minRange===w&&!this.isLog)v(a.min)||v(a.max)?this.minRange=null:(n(this.series, -function(a){i=a.xData;for(g=j=a.xIncrement?1:i.length-1;g>0;g--)if(h=i[g]-i[g-1],f===w||hb&&(g=0);c=t(c,g);e=t(e,h?0:g/2);f=t(f,h==="on"?0:g);!a.noSharedTooltip&&v(m)&&(d=v(d)?A(d,m):m)}),g=this.ordinalSlope&&d?this.ordinalSlope/d:1,this.minPointOffset=e*=g,this.pointRangePadding=f*=g,this.pointRange=A(c,b),this.closestPointRange=d;if(a)this.oldTransA=h;this.translationSlope=this.transA=h=this.len/(b+f||1);this.transB=this.horiz?this.left:this.bottom;this.minPixelPadding= -h*e},setTickPositions:function(a){var b=this,c=b.chart,d=b.options,e=b.isLog,f=b.isDatetimeAxis,g=b.isXAxis,h=b.isLinked,i=b.options.tickPositioner,j=d.maxPadding,k=d.minPadding,m=d.tickInterval,l=d.minTickInterval,o=d.tickPixelInterval,q=b.categories;h?(b.linkedParent=c[g?"xAxis":"yAxis"][d.linkedTo],c=b.linkedParent.getExtremes(),b.min=p(c.min,c.dataMin),b.max=p(c.max,c.dataMax),d.type!==b.linkedParent.options.type&&Ba(11,1)):(b.min=p(b.userMin,d.min,b.dataMin),b.max=p(b.userMax,d.max,b.dataMax)); -if(e)!a&&A(b.min,p(b.dataMin,b.min))<=0&&Ba(10,1),b.min=oa(ra(b.min)),b.max=oa(ra(b.max));if(b.range&&(b.userMin=b.min=t(b.min,b.max-b.range),b.userMax=b.max,a))b.range=null;b.beforePadding&&b.beforePadding();b.adjustForMinRange();if(!q&&!b.usePercentage&&!h&&v(b.min)&&v(b.max)&&(c=b.max-b.min)){if(!v(d.min)&&!v(b.userMin)&&k&&(b.dataMin<0||!b.ignoreMinPadding))b.min-=c*k;if(!v(d.max)&&!v(b.userMax)&&j&&(b.dataMax>0||!b.ignoreMaxPadding))b.max+=c*j}b.tickInterval=b.min===b.max||b.min===void 0||b.max=== -void 0?1:h&&!m&&o===b.linkedParent.options.tickPixelInterval?b.linkedParent.tickInterval:p(m,q?1:(b.max-b.min)*o/(b.len||1));g&&!a&&n(b.series,function(a){a.processData(b.min!==b.oldMin||b.max!==b.oldMax)});b.setAxisTranslation(!0);b.beforeSetTickPositions&&b.beforeSetTickPositions();if(b.postProcessTickInterval)b.tickInterval=b.postProcessTickInterval(b.tickInterval);if(!m&&b.tickIntervale&&i.shift(),d.endOnTick?b.max=f:b.max+h(b[d]||0)&&this.options.alignTicks!==!1)b[d]=c.length;a.maxTicks=b},adjustTickAmount:function(){var a=this._maxTicksKey,b=this.tickPositions,c=this.chart.maxTicks;if(c&&c[a]&&!this.isDatetimeAxis&& -!this.categories&&!this.isLinked&&this.options.alignTicks!==!1){var d=this.tickAmount,e=b.length;this.tickAmount=a=c[a];if(e=this.dataMax&&(b=w));this.displayBtn=a!==w||b!==w;this.setExtremes(a,b,!1,w,{trigger:"zoom"});return!0},setAxisSize:function(){var a=this.chart,b=this.options,c=b.offsetLeft||0,d=b.offsetRight||0,e=this.horiz,f,g;this.left=g=p(b.left,a.plotLeft+c);this.top=f=p(b.top,a.plotTop);this.width=c=p(b.width,a.plotWidth-c+d);this.height=b=p(b.height,a.plotHeight); -this.bottom=a.chartHeight-b-f;this.right=a.chartWidth-c-g;this.len=t(e?c:b,0);this.pos=e?g:f},getExtremes:function(){var a=this.isLog;return{min:a?oa(la(this.min)):this.min,max:a?oa(la(this.max)):this.max,dataMin:this.dataMin,dataMax:this.dataMax,userMin:this.userMin,userMax:this.userMax}},getThreshold:function(a){var b=this.isLog,c=b?la(this.min):this.min,b=b?la(this.max):this.max;c>a||a===null?a=c:b=a.min&&b<=a.max)j[b]||(j[b]=new ab(a,b)),s&&j[b].isNew&&j[b].render(c,!0),j[b].render(c,!1,1)}),q&&a.min===0&&(j[-1]||(j[-1]=new ab(a,-1,null,!0)),j[-1].render(-1))), -o&&n(g,function(b,c){if(c%2===0&&b1||U(b-f.y)>1))clearTimeout(this.tooltipTimeout),this.tooltipTimeout=setTimeout(function(){e&&e.move(a,b,c,d)},32)},hide:function(){var a=this,b;clearTimeout(this.hideTimer);if(!this.isHidden)b=this.chart.hoverPoints,this.hideTimer=setTimeout(function(){a.label.fadeOut();a.isHidden=!0},p(this.options.hideDelay,500)),b&&n(b,function(a){a.setState()}),this.chart.hoverPoints=null},hideCrosshairs:function(){n(this.crosshairs,function(a){a&&a.hide()})},getAnchor:function(a,b){var c,d=this.chart,e= -d.inverted,f=d.plotTop,g=0,h=0,i,a=ja(a);c=a[0].tooltipPos;this.followPointer&&b&&(b.chartX===w&&(b=d.pointer.normalize(b)),c=[b.chartX-d.plotLeft,b.chartY-f]);c||(n(a,function(a){i=a.series.yAxis;g+=a.plotX;h+=(a.plotLow?(a.plotLow+a.plotHigh)/2:a.plotY)+(!e&&i?i.top-f:0)}),g/=a.length,h/=a.length,c=[e?d.plotWidth-h:g,this.shared&&!e&&a.length>1&&b?b.chartY-f:e?d.plotHeight-g:h]);return Fa(c,r)},getPosition:function(a,b,c){var d=this.chart,e=d.plotLeft,f=d.plotTop,g=d.plotWidth,h=d.plotHeight,i= -p(this.options.distance,12),j=c.plotX,c=c.plotY,d=j+e+(d.inverted?i:-a-i),k=c-b+f+15,m;d<7&&(d=e+t(j,0)+i);d+a>e+g&&(d-=d+a-(e+g),k=c-b+f-i,m=!0);k=k&&c<=k+b&&(k=c+f+i));k+b>f+h&&(k=t(f,f+h-b-i));return{x:d,y:k}},defaultFormatter:function(a){var b=this.points||ja(this),c=b[0].series,d;d=[c.tooltipHeaderFormatter(b[0])];n(b,function(a){c=a.series;d.push(c.tooltipFormatter&&c.tooltipFormatter(a)||a.point.tooltipFormatter(c.tooltipOptions.pointFormat))});d.push(a.options.footerFormat|| -"");return d.join("")},refresh:function(a,b){var c=this.chart,d=this.label,e=this.options,f,g,h,i={},j,k=[];j=e.formatter||this.defaultFormatter;var i=c.hoverPoints,m,l=e.crosshairs;h=this.shared;clearTimeout(this.hideTimer);this.followPointer=ja(a)[0].series.tooltipOptions.followPointer;g=this.getAnchor(a,b);f=g[0];g=g[1];h&&(!a.series||!a.series.noSharedTooltip)?(c.hoverPoints=a,i&&n(i,function(a){a.setState()}),n(a,function(a){a.setState("hover");k.push(a.getLabelConfig())}),i={x:a[0].category, -y:a[0].y},i.points=k,a=a[0]):i=a.getLabelConfig();j=j.call(i,this);i=a.series;h=h||!i.isCartesian||i.tooltipOutsidePlot||c.isInsidePlot(f,g);j===!1||!h?this.hide():(this.isHidden&&(hb(d),d.attr("opacity",1).show()),d.attr({text:j}),m=e.borderColor||a.color||i.color||"#606060",d.attr({stroke:m}),this.updatePosition({plotX:f,plotY:g}),this.isHidden=!1);if(l){l=ja(l);for(d=l.length;d--;)if(e=a.series[d?"yAxis":"xAxis"],l[d]&&e)if(h=d?p(a.stackY,a.y):a.x,e.isLog&&(h=ra(h)),e=e.getPlotLinePath(h,1),this.crosshairs[d])this.crosshairs[d].attr({d:e, -visibility:"visible"});else{h={"stroke-width":l[d].width||1,stroke:l[d].color||"#C0C0C0",zIndex:l[d].zIndex||2};if(l[d].dashStyle)h.dashstyle=l[d].dashStyle;this.crosshairs[d]=c.renderer.path(e).attr(h).add()}}K(c,"tooltipRefresh",{text:j,x:f+c.plotLeft,y:g+c.plotTop,borderColor:m})},updatePosition:function(a){var b=this.chart,c=this.label,c=(this.options.positioner||this.getPosition).call(this,c.width,c.height,a);this.move(r(c.x),r(c.y),a.plotX+b.plotLeft,a.plotY+b.plotTop)}};pb.prototype={init:function(a, -b){var c=ia?"":b.chart.zoomType,d=a.inverted,e;this.options=b;this.chart=a;this.zoomX=e=/x/.test(c);this.zoomY=c=/y/.test(c);this.zoomHor=e&&!d||c&&d;this.zoomVert=c&&!d||e&&d;this.pinchDown=[];this.lastValidTouch={};if(b.tooltip.enabled)a.tooltip=new Gb(a,b.tooltip);this.setDOMEvents()},normalize:function(a){var b,c,d,a=a||Y.event;if(!a.target)a.target=a.srcElement;a=Zb(a);d=a.touches?a.touches.item(0):a;this.chartPosition=b=ec(this.chart.container);d.pageX===w?(c=a.x,b=a.y):(c=d.pageX-b.left,b= -d.pageY-b.top);return y(a,{chartX:r(c),chartY:r(b)})},getCoordinates:function(a){var b={xAxis:[],yAxis:[]};n(this.chart.axes,function(c){b[c.isXAxis?"xAxis":"yAxis"].push({axis:c,value:c.toValue(a[c.horiz?"chartX":"chartY"])})});return b},getIndex:function(a){var b=this.chart;return b.inverted?b.plotHeight+b.plotTop-a.chartY:a.chartX-b.plotLeft},runPointActions:function(a){var b=this.chart,c=b.series,d=b.tooltip,e,f=b.hoverPoint,g=b.hoverSeries,h,i,j=b.chartWidth,k=this.getIndex(a);if(d&&this.options.tooltip.shared&& -(!g||!g.noSharedTooltip)){e=[];h=c.length;for(i=0;ij&&e.splice(h,1);if(e.length&&e[0].clientX!==this.hoverX)d.refresh(e,a),this.hoverX=e[0].clientX}if(g&&g.tracker){if((b=g.tooltipPoints[k])&&b!==f)b.onMouseOver(a)}else d&&d.followPointer&&!d.isHidden&&(a=d.getAnchor([{}],a), -d.updatePosition({plotX:a[0],plotY:a[1]}))},reset:function(a){var b=this.chart,c=b.hoverSeries,d=b.hoverPoint,e=b.tooltip,b=e&&e.shared?b.hoverPoints:d;(a=a&&e&&b)&&ja(b)[0].plotX===w&&(a=!1);if(a)e.refresh(b);else{if(d)d.onMouseOut();if(c)c.onMouseOut();e&&(e.hide(),e.hideCrosshairs());this.hoverX=null}},scaleGroups:function(a,b){var c=this.chart;n(c.series,function(d){d.xAxis&&d.xAxis.zoomEnabled&&(d.group.attr(a),d.markerGroup&&(d.markerGroup.attr(a),d.markerGroup.clip(b?c.clipRect:null)),d.dataLabelsGroup&& -d.dataLabelsGroup.attr(a))});c.clipRect.attr(b||c.clipBox)},pinchTranslateDirection:function(a,b,c,d,e,f,g){var h=this.chart,i=a?"x":"y",j=a?"X":"Y",k="chart"+j,m=a?"width":"height",l=h["plot"+(a?"Left":"Top")],o,q,n=1,p=h.inverted,s=h.bounds[a?"h":"v"],x=b.length===1,r=b[0][k],t=c[0][k],E=!x&&b[1][k],B=!x&&c[1][k],v,c=function(){!x&&U(r-E)>20&&(n=U(t-B)/U(r-E));q=(l-t)/n+r;o=h["plot"+(a?"Width":"Height")]/n};c();b=q;bs.max&&(b=s.max-o,v=!0);v?(t-=0.8*(t-g[i][0]),x||(B-= -0.8*(B-g[i][1])),c()):g[i]=[t,B];p||(f[i]=q-l,f[m]=o);f=p?1/n:n;e[m]=o;e[i]=b;d[p?a?"scaleY":"scaleX":"scale"+j]=n;d["translate"+j]=f*l+(t-f*r)},pinch:function(a){var b=this,c=b.chart,d=b.pinchDown,e=c.tooltip&&c.tooltip.options.followTouchMove,f=a.touches,g=f.length,h=b.lastValidTouch,i=b.zoomHor||b.pinchHor,j=b.zoomVert||b.pinchVert,k=i||j,m=b.selectionMarker,l={},o={};a.type==="touchstart"&&(e||k)&&a.preventDefault();Fa(f,function(a){return b.normalize(a)});if(a.type==="touchstart")n(f,function(a, -b){d[b]={chartX:a.chartX,chartY:a.chartY}}),h.x=[d[0].chartX,d[1]&&d[1].chartX],h.y=[d[0].chartY,d[1]&&d[1].chartY],n(c.axes,function(a){if(a.zoomEnabled){var b=c.bounds[a.horiz?"h":"v"],d=a.minPixelPadding,e=a.toPixels(a.dataMin),f=a.toPixels(a.dataMax),g=A(e,f),e=t(e,f);b.min=A(a.pos,g-d);b.max=t(a.pos+a.len,e+d)}});else if(d.length){if(!m)b.selectionMarker=m=y({destroy:qa},c.plotBox);i&&b.pinchTranslateDirection(!0,d,f,l,m,o,h);j&&b.pinchTranslateDirection(!1,d,f,l,m,o,h);b.hasPinched=k;b.scaleGroups(l, -o);!k&&e&&g===1&&this.runPointActions(b.normalize(a))}},dragStart:function(a){var b=this.chart;b.mouseIsDown=a.type;b.cancelClick=!1;b.mouseDownX=this.mouseDownX=a.chartX;this.mouseDownY=a.chartY},drag:function(a){var b=this.chart,c=b.options.chart,d=a.chartX,a=a.chartY,e=this.zoomHor,f=this.zoomVert,g=b.plotLeft,h=b.plotTop,i=b.plotWidth,j=b.plotHeight,k,m=this.mouseDownX,l=this.mouseDownY;dg+i&&(d=g+i);ah+j&&(a=h+j);this.hasDragged=Math.sqrt(Math.pow(m-d,2)+Math.pow(l-a,2));if(this.hasDragged> -10){k=b.isInsidePlot(m-g,l-h);if(b.hasCartesianSeries&&(this.zoomX||this.zoomY)&&k&&!this.selectionMarker)this.selectionMarker=b.renderer.rect(g,h,e?1:i,f?1:j,0).attr({fill:c.selectionMarkerFill||"rgba(69,114,167,0.25)",zIndex:7}).add();this.selectionMarker&&e&&(e=d-m,this.selectionMarker.attr({width:U(e),x:(e>0?0:e)+m}));this.selectionMarker&&f&&(e=a-l,this.selectionMarker.attr({height:U(e),y:(e>0?0:e)+l}));k&&!this.selectionMarker&&c.panning&&b.pan(d)}},drop:function(a){var b=this.chart,c=this.hasPinched; -if(this.selectionMarker){var d={xAxis:[],yAxis:[],originalEvent:a.originalEvent||a},e=this.selectionMarker,f=e.x,g=e.y,h;if(this.hasDragged||c)n(b.axes,function(a){if(a.zoomEnabled){var b=a.horiz,c=a.toValue(b?f:g),b=a.toValue(b?f+e.width:g+e.height);!isNaN(c)&&!isNaN(b)&&(d[a.xOrY+"Axis"].push({axis:a,min:A(c,b),max:t(c,b)}),h=!0)}}),h&&K(b,"selection",d,function(a){b.zoom(y(a,c?{animation:!1}:null))});this.selectionMarker=this.selectionMarker.destroy();c&&this.scaleGroups({translateX:b.plotLeft, -translateY:b.plotTop,scaleX:1,scaleY:1})}if(b)L(b.container,{cursor:b._cursor}),b.cancelClick=this.hasDragged>10,b.mouseIsDown=this.hasDragged=this.hasPinched=!1,this.pinchDown=[]},onContainerMouseDown:function(a){a=this.normalize(a);a.preventDefault&&a.preventDefault();this.dragStart(a)},onDocumentMouseUp:function(a){this.drop(a)},onDocumentMouseMove:function(a){var b=this.chart,c=this.chartPosition,d=b.hoverSeries,a=Zb(a);c&&d&&d.isCartesian&&!b.isInsidePlot(a.pageX-c.left-b.plotLeft,a.pageY-c.top- -b.plotTop)&&this.reset()},onContainerMouseLeave:function(){this.reset();this.chartPosition=null},onContainerMouseMove:function(a){var b=this.chart,a=this.normalize(a);a.returnValue=!1;b.mouseIsDown==="mousedown"&&this.drag(a);b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&!b.openMenu&&this.runPointActions(a)},inClass:function(a,b){for(var c;a;){if(c=G(a,"class"))if(c.indexOf(b)!==-1)return!0;else if(c.indexOf("highcharts-container")!==-1)return!1;a=a.parentNode}},onTrackerMouseOut:function(a){var b= -this.chart.hoverSeries;if(b&&!b.options.stickyTracking&&!this.inClass(a.toElement||a.relatedTarget,"highcharts-tooltip"))b.onMouseOut()},onContainerClick:function(a){var b=this.chart,c=b.hoverPoint,d=b.plotLeft,e=b.plotTop,f=b.inverted,g,h,i,a=this.normalize(a);a.cancelBubble=!0;if(!b.cancelClick)c&&this.inClass(a.target,"highcharts-tracker")?(g=this.chartPosition,h=c.plotX,i=c.plotY,y(c,{pageX:g.left+d+(f?b.plotWidth-i:h),pageY:g.top+e+(f?b.plotHeight-h:i)}),K(c.series,"click",y(a,{point:c})),b.hoverPoint&& -c.firePointEvent("click",a)):(y(a,this.getCoordinates(a)),b.isInsidePlot(a.chartX-d,a.chartY-e)&&K(b,"click",a))},onContainerTouchStart:function(a){var b=this.chart;a.touches.length===1?(a=this.normalize(a),b.isInsidePlot(a.chartX-b.plotLeft,a.chartY-b.plotTop)&&(this.runPointActions(a),this.pinch(a))):a.touches.length===2&&this.pinch(a)},onContainerTouchMove:function(a){(a.touches.length===1||a.touches.length===2)&&this.pinch(a)},onDocumentTouchEnd:function(a){this.drop(a)},setDOMEvents:function(){var a= -this,b=a.chart.container,c;this._events=c=[[b,"onmousedown","onContainerMouseDown"],[b,"onmousemove","onContainerMouseMove"],[b,"onclick","onContainerClick"],[b,"mouseleave","onContainerMouseLeave"],[H,"mousemove","onDocumentMouseMove"],[H,"mouseup","onDocumentMouseUp"]];gb&&c.push([b,"ontouchstart","onContainerTouchStart"],[b,"ontouchmove","onContainerTouchMove"],[H,"touchend","onDocumentTouchEnd"]);n(c,function(b){a["_"+b[2]]=function(c){a[b[2]](c)};b[1].indexOf("on")===0?b[0][b[1]]=a["_"+b[2]]: -F(b[0],b[1],a["_"+b[2]])})},destroy:function(){var a=this;n(a._events,function(b){b[1].indexOf("on")===0?b[0][b[1]]=null:X(b[0],b[1],a["_"+b[2]])});delete a._events;clearInterval(a.tooltipTimeout)}};Hb.prototype={init:function(a,b){var c=this,d=b.itemStyle,e=p(b.padding,8),f=b.itemMarginTop||0;this.options=b;if(b.enabled)c.baseline=C(d.fontSize)+3+f,c.itemStyle=d,c.itemHiddenStyle=z(d,b.itemHiddenStyle),c.itemMarginTop=f,c.padding=e,c.initialItemX=e,c.initialItemY=e-5,c.maxItemWidth=0,c.chart=a,c.itemHeight= -0,c.lastLineHeight=0,c.render(),F(c.chart,"endResize",function(){c.positionCheckboxes()})},colorizeItem:function(a,b){var c=this.options,d=a.legendItem,e=a.legendLine,f=a.legendSymbol,g=this.itemHiddenStyle.color,c=b?c.itemStyle.color:g,h=b?a.color:g,g=a.options&&a.options.marker,i={stroke:h,fill:h},j;d&&d.css({fill:c,color:c});e&&e.attr({stroke:h});if(f){if(g)for(j in g=a.convertAttribs(g),g)d=g[j],d!==w&&(i[j]=d);f.attr(i)}},positionItem:function(a){var b=this.options,c=b.symbolPadding,b=!b.rtl, -d=a._legendItemPos,e=d[0],d=d[1],f=a.checkbox;a.legendGroup&&a.legendGroup.translate(b?e:this.legendWidth-e-2*c-4,d);if(f)f.x=e,f.y=d},destroyItem:function(a){var b=a.checkbox;n(["legendItem","legendLine","legendSymbol","legendGroup"],function(b){a[b]&&a[b].destroy()});b&&Za(a.checkbox)},destroy:function(){var a=this.group,b=this.box;if(b)this.box=b.destroy();if(a)this.group=a.destroy()},positionCheckboxes:function(a){var b=this.group.alignAttr,c,d=this.clipHeight||this.legendHeight;if(b)c=b.translateY, -n(this.allItems,function(e){var f=e.checkbox,g;f&&(g=c+f.y+(a||0)+3,L(f,{left:b.translateX+e.legendItemWidth+f.x-20+"px",top:g+"px",display:g>c-6&&g(l||c.chartWidth-2*k-n))b.itemX=n,b.itemY+=q+b.lastLineHeight+o,b.lastLineHeight=0;b.maxItemWidth=t(b.maxItemWidth,e);b.lastItemY=q+b.itemY+o;b.lastLineHeight=t(g,b.lastLineHeight);a._legendItemPos=[b.itemX,b.itemY];f?b.itemX+=e:(b.itemY+=q+g+o,b.lastLineHeight= -g);b.offsetWidth=l||t(f?b.itemX-n:e,b.offsetWidth)},render:function(){var a=this,b=a.chart,c=b.renderer,d=a.group,e,f,g,h,i=a.box,j=a.options,k=a.padding,m=j.borderWidth,l=j.backgroundColor;a.itemX=a.initialItemX;a.itemY=a.initialItemY;a.offsetWidth=0;a.lastItemY=0;if(!d)a.group=d=c.g("legend").attr({zIndex:7}).add(),a.contentGroup=c.g().attr({zIndex:1}).add(d),a.scrollGroup=c.g().add(a.contentGroup);a.renderTitle();e=[];n(b.series,function(a){var b=a.options;b.showInLegend&&!v(b.linkedTo)&&(e=e.concat(a.legendItems|| -(b.legendType==="point"?a.data:a)))});Sb(e,function(a,b){return(a.options&&a.options.legendIndex||0)-(b.options&&b.options.legendIndex||0)});j.reversed&&e.reverse();a.allItems=e;a.display=f=!!e.length;n(e,function(b){a.renderItem(b)});g=j.width||a.offsetWidth;h=a.lastItemY+a.lastLineHeight+a.titleHeight;h=a.handleOverflow(h);if(m||l){g+=k;h+=k;if(i){if(g>0&&h>0)i[i.isNew?"attr":"animate"](i.crisp(null,null,null,g,h)),i.isNew=!1}else a.box=i=c.rect(0,0,g,h,j.borderRadius,m||0).attr({stroke:j.borderColor, -"stroke-width":m||0,fill:l||ba}).add(d).shadow(j.shadow),i.isNew=!0;i[f?"show":"hide"]()}a.legendWidth=g;a.legendHeight=h;n(e,function(b){a.positionItem(b)});f&&d.align(y({width:g,height:h},j),!0,"spacingBox");b.isResizing||this.positionCheckboxes()},handleOverflow:function(a){var b=this,c=this.chart,d=c.renderer,e=this.options,f=e.y,f=c.spacingBox.height+(e.verticalAlign==="top"?-f:f)-this.padding,g=e.maxHeight,h=this.clipRect,i=e.navigation,j=p(i.animation,!0),k=i.arrowSize||12,m=this.nav;e.layout=== -"horizontal"&&(f/=2);g&&(f=A(f,g));if(a>f&&!e.useHTML){this.clipHeight=c=f-20-this.titleHeight;this.pageCount=pa(a/c);this.currentPage=p(this.currentPage,1);this.fullHeight=a;if(!h)h=b.clipRect=d.clipRect(0,0,9999,0),b.contentGroup.clip(h);h.attr({height:c});if(!m)this.nav=m=d.g().attr({zIndex:1}).add(this.group),this.up=d.symbol("triangle",0,0,k,k).on("click",function(){b.scroll(-1,j)}).add(m),this.pager=d.text("",15,10).css(i.style).add(m),this.down=d.symbol("triangle-down",0,0,k,k).on("click", -function(){b.scroll(1,j)}).add(m);b.scroll(0);a=f}else if(m)h.attr({height:c.chartHeight}),m.hide(),this.scrollGroup.attr({translateY:1}),this.clipHeight=0;return a},scroll:function(a,b){var c=this.pageCount,d=this.currentPage+a,e=this.clipHeight,f=this.options.navigation,g=f.activeColor,h=f.inactiveColor,f=this.pager,i=this.padding;d>c&&(d=c);if(d>0)b!==w&&$a(b,this.chart),this.nav.attr({translateX:i,translateY:e+7+this.titleHeight,visibility:"visible"}),this.up.attr({fill:d===1?h:g}).css({cursor:d=== -1?"default":"pointer"}),f.attr({text:d+"/"+this.pageCount}),this.down.attr({x:18+this.pager.getBBox().width,fill:d===c?h:g}).css({cursor:d===c?"default":"pointer"}),e=-A(e*(d-1),this.fullHeight-e+i)+1,this.scrollGroup.animate({translateY:e}),f.attr({text:d+"/"+c}),this.currentPage=d,this.positionCheckboxes(e)}};Sa.prototype={init:function(a,b){var c,d=a.series;a.series=null;c=z(M,a);c.series=a.series=d;var d=c.chart,e=d.margin,e=da(e)?e:[e,e,e,e];this.optionsMarginTop=p(d.marginTop,e[0]);this.optionsMarginRight= -p(d.marginRight,e[1]);this.optionsMarginBottom=p(d.marginBottom,e[2]);this.optionsMarginLeft=p(d.marginLeft,e[3]);this.runChartClick=(e=d.events)&&!!e.click;this.bounds={h:{},v:{}};this.callback=b;this.isResizing=0;this.options=c;this.axes=[];this.series=[];this.hasCartesianSeries=d.showAxes;var f=this,g;f.index=Ua.length;Ua.push(f);d.reflow!==!1&&F(f,"load",function(){f.initReflow()});if(e)for(g in e)F(f,g,e[g]);f.xAxis=[];f.yAxis=[];f.animation=ia?!1:p(d.animation,!0);f.pointCount=0;f.counters= -new Rb;f.firstRender()},initSeries:function(a){var b=this.options.chart;(b=Q[a.type||b.type||b.defaultSeriesType])||Ba(17,!0);b=new b;b.init(this,a);return b},addSeries:function(a,b,c){var d,e=this;a&&(b=p(b,!0),K(e,"addSeries",{options:a},function(){d=e.initSeries(a);e.isDirtyLegend=!0;b&&e.redraw(c)}));return d},addAxis:function(a,b,c,d){var b=b?"xAxis":"yAxis",e=this.options;new Da(this,z(a,{index:this[b].length}));e[b]=ja(e[b]||{});e[b].push(a);p(c,!0)&&this.redraw(d)},isInsidePlot:function(a, -b,c){var d=c?b:a,a=c?a:b;return d>=0&&d<=this.plotWidth&&a>=0&&a<=this.plotHeight},adjustTickAmounts:function(){this.options.chart.alignTicks!==!1&&n(this.axes,function(a){a.adjustTickAmount()});this.maxTicks=null},redraw:function(a){var b=this.axes,c=this.series,d=this.pointer,e=this.legend,f=this.isDirtyLegend,g,h=this.isDirtyBox,i=c.length,j=i,k=this.renderer,m=k.isHidden(),l=[];$a(a,this);for(m&&this.cloneRenderTo();j--;)if(a=c[j],a.isDirty&&a.options.stacking){g=!0;break}if(g)for(j=i;j--;)if(a= -c[j],a.options.stacking)a.isDirty=!0;n(c,function(a){a.isDirty&&a.options.legendType==="point"&&(f=!0)});if(f&&e.options.enabled)e.render(),this.isDirtyLegend=!1;if(this.hasCartesianSeries){if(!this.isResizing)this.maxTicks=null,n(b,function(a){a.setScale()});this.adjustTickAmounts();this.getMargins();n(b,function(a){if(a.isDirtyExtremes)a.isDirtyExtremes=!1,l.push(function(){K(a,"afterSetExtremes",a.getExtremes())});if(a.isDirty||h||g)a.redraw(),h=!0})}h&&this.drawChartBox();n(c,function(a){a.isDirty&& -a.visible&&(!a.isCartesian||a.xAxis)&&a.redraw()});d&&d.reset&&d.reset(!0);k.draw();K(this,"redraw");m&&this.cloneRenderTo(!0);n(l,function(a){a.call()})},showLoading:function(a){var b=this.options,c=this.loadingDiv,d=b.loading;if(!c)this.loadingDiv=c=aa(Qa,{className:"highcharts-loading"},y(d.style,{zIndex:10,display:ba}),this.container),this.loadingSpan=aa("span",null,d.labelStyle,c);this.loadingSpan.innerHTML=a||b.lang.loading;if(!this.loadingShown)L(c,{opacity:0,display:"",left:this.plotLeft+ -"px",top:this.plotTop+"px",width:this.plotWidth+"px",height:this.plotHeight+"px"}),Mb(c,{opacity:d.style.opacity},{duration:d.showDuration||0}),this.loadingShown=!0},hideLoading:function(){var a=this.options,b=this.loadingDiv;b&&Mb(b,{opacity:0},{duration:a.loading.hideDuration||100,complete:function(){L(b,{display:ba})}});this.loadingShown=!1},get:function(a){var b=this.axes,c=this.series,d,e;for(d=0;dA(e.dataMin,e.min)&&c19?this.containerHeight:400))},cloneRenderTo:function(a){var b=this.renderToClone,c=this.container;a?b&&(this.renderTo.appendChild(c),Za(b),delete this.renderToClone): -(c&&this.renderTo.removeChild(c),this.renderToClone=b=this.renderTo.cloneNode(0),L(b,{position:"absolute",top:"-9999px",display:"block"}),H.body.appendChild(b),c&&b.appendChild(c))},getContainer:function(){var a,b=this.options.chart,c,d,e;this.renderTo=a=b.renderTo;e="highcharts-"+Kb++;if(ma(a))this.renderTo=a=H.getElementById(a);a||Ba(13,!0);c=C(G(a,"data-highcharts-chart"));!isNaN(c)&&Ua[c]&&Ua[c].destroy();G(a,"data-highcharts-chart",this.index);a.innerHTML="";a.offsetWidth||this.cloneRenderTo(); -this.getChartSize();c=this.chartWidth;d=this.chartHeight;this.container=a=aa(Qa,{className:"highcharts-container"+(b.className?" "+b.className:""),id:e},y({position:"relative",overflow:"hidden",width:c+"px",height:d+"px",textAlign:"left",lineHeight:"normal",zIndex:0,"-webkit-tap-highlight-color":"rgba(0,0,0,0)"},b.style),this.renderToClone||a);this._cursor=a.style.cursor;this.renderer=b.forExport?new Ga(a,c,d,!0):new cb(a,c,d);ia&&this.renderer.create(this,a,c,d)},getMargins:function(){var a=this.options.chart, -b=a.spacingTop,c=a.spacingRight,d=a.spacingBottom,a=a.spacingLeft,e,f=this.legend,g=this.optionsMarginTop,h=this.optionsMarginLeft,i=this.optionsMarginRight,j=this.optionsMarginBottom,k=this.options.title,m=this.options.subtitle,l=this.options.legend,o=p(l.margin,10),q=l.x,O=l.y,u=l.align,s=l.verticalAlign;this.resetMargins();e=this.axisOffset;if((this.title||this.subtitle)&&!v(this.optionsMarginTop))if(m=t(this.title&&!k.floating&&!k.verticalAlign&&k.y||0,this.subtitle&&!m.floating&&!m.verticalAlign&& -m.y||0))this.plotTop=t(this.plotTop,m+p(k.margin,15)+b);if(f.display&&!l.floating)if(u==="right"){if(!v(i))this.marginRight=t(this.marginRight,f.legendWidth-q+o+c)}else if(u==="left"){if(!v(h))this.plotLeft=t(this.plotLeft,f.legendWidth+q+o+a)}else if(s==="top"){if(!v(g))this.plotTop=t(this.plotTop,f.legendHeight+O+o+b)}else if(s==="bottom"&&!v(j))this.marginBottom=t(this.marginBottom,f.legendHeight-O+o+d);this.extraBottomMargin&&(this.marginBottom+=this.extraBottomMargin);this.extraTopMargin&&(this.plotTop+= -this.extraTopMargin);this.hasCartesianSeries&&n(this.axes,function(a){a.getOffset()});v(h)||(this.plotLeft+=e[3]);v(g)||(this.plotTop+=e[0]);v(j)||(this.marginBottom+=e[2]);v(i)||(this.marginRight+=e[1]);this.setChartSize()},initReflow:function(){function a(a){var g=c.width||vb(d,"width"),h=c.height||vb(d,"height"),a=a?a.target:Y;if(!b.hasUserSize&&g&&h&&(a===Y||a===H)){if(g!==b.containerWidth||h!==b.containerHeight)clearTimeout(e),b.reflowTimeout=e=setTimeout(function(){if(b.container)b.setSize(g, -h,!1),b.hasUserSize=null},100);b.containerWidth=g;b.containerHeight=h}}var b=this,c=b.options.chart,d=b.renderTo,e;F(Y,"resize",a);F(b,"destroy",function(){X(Y,"resize",a)})},setSize:function(a,b,c){var d=this,e,f,g;d.isResizing+=1;g=function(){d&&K(d,"endResize",null,function(){d.isResizing-=1})};$a(c,d);d.oldChartHeight=d.chartHeight;d.oldChartWidth=d.chartWidth;if(v(a))d.chartWidth=e=t(0,r(a)),d.hasUserSize=!!e;if(v(b))d.chartHeight=f=t(0,r(b));L(d.container,{width:e+"px",height:f+"px"});d.setChartSize(!0); -d.renderer.setSize(e,f,c);d.maxTicks=null;n(d.axes,function(a){a.isDirty=!0;a.setScale()});n(d.series,function(a){a.isDirty=!0});d.isDirtyLegend=!0;d.isDirtyBox=!0;d.getMargins();d.redraw(c);d.oldChartHeight=null;K(d,"resize");Ra===!1?g():setTimeout(g,Ra&&Ra.duration||500)},setChartSize:function(a){var b=this.inverted,c=this.renderer,d=this.chartWidth,e=this.chartHeight,f=this.options.chart,g=f.spacingTop,h=f.spacingRight,i=f.spacingBottom,j=f.spacingLeft,k=this.clipOffset,m,l,o,q;this.plotLeft=m= -r(this.plotLeft);this.plotTop=l=r(this.plotTop);this.plotWidth=o=t(0,r(d-m-this.marginRight));this.plotHeight=q=t(0,r(e-l-this.marginBottom));this.plotSizeX=b?q:o;this.plotSizeY=b?o:q;this.plotBorderWidth=b=f.plotBorderWidth||0;this.spacingBox=c.spacingBox={x:j,y:g,width:d-j-h,height:e-g-i};this.plotBox=c.plotBox={x:m,y:l,width:o,height:q};c=pa(t(b,k[3])/2);d=pa(t(b,k[0])/2);this.clipBox={x:c,y:d,width:W(this.plotSizeX-t(b,k[1])/2-c),height:W(this.plotSizeY-t(b,k[2])/2-d)};a||n(this.axes,function(a){a.setAxisSize(); -a.setAxisTranslation()})},resetMargins:function(){var a=this.options.chart,b=a.spacingRight,c=a.spacingBottom,d=a.spacingLeft;this.plotTop=p(this.optionsMarginTop,a.spacingTop);this.marginRight=p(this.optionsMarginRight,b);this.marginBottom=p(this.optionsMarginBottom,c);this.plotLeft=p(this.optionsMarginLeft,d);this.axisOffset=[0,0,0,0];this.clipOffset=[0,0,0,0]},drawChartBox:function(){var a=this.options.chart,b=this.renderer,c=this.chartWidth,d=this.chartHeight,e=this.chartBackground,f=this.plotBackground, -g=this.plotBorder,h=this.plotBGImage,i=a.borderWidth||0,j=a.backgroundColor,k=a.plotBackgroundColor,m=a.plotBackgroundImage,l=a.plotBorderWidth||0,o,q=this.plotLeft,n=this.plotTop,p=this.plotWidth,s=this.plotHeight,x=this.plotBox,r=this.clipRect,t=this.clipBox;o=i+(a.shadow?8:0);if(i||j)if(e)e.animate(e.crisp(null,null,null,c-o,d-o));else{e={fill:j||ba};if(i)e.stroke=a.borderColor,e["stroke-width"]=i;this.chartBackground=b.rect(o/2,o/2,c-o,d-o,a.borderRadius,i).attr(e).add().shadow(a.shadow)}if(k)f? -f.animate(x):this.plotBackground=b.rect(q,n,p,s,0).attr({fill:k}).add().shadow(a.plotShadow);if(m)h?h.animate(x):this.plotBGImage=b.image(m,q,n,p,s).add();r?r.animate({width:t.width,height:t.height}):this.clipRect=b.clipRect(t);if(l)g?g.animate(g.crisp(null,q,n,p,s)):this.plotBorder=b.rect(q,n,p,s,0,l).attr({stroke:a.plotBorderColor,"stroke-width":l,zIndex:1}).add();this.isDirtyBox=!1},propFromSeries:function(){var a=this,b=a.options.chart,c,d=a.options.series,e,f;n(["inverted","angular","polar"], -function(g){c=Q[b.type||b.defaultSeriesType];f=a[g]||b[g]||c&&c.prototype[g];for(e=d&&d.length;!f&&e--;)(c=Q[d[e].type])&&c.prototype[g]&&(f=!0);a[g]=f})},render:function(){var a=this,b=a.axes,c=a.renderer,d=a.options,e=d.labels,f=d.credits,g;a.setTitle();a.legend=new Hb(a,d.legend);n(b,function(a){a.setScale()});a.getMargins();a.maxTicks=null;n(b,function(a){a.setTickPositions(!0);a.setMaxTicks()});a.adjustTickAmounts();a.getMargins();a.drawChartBox();a.hasCartesianSeries&&n(b,function(a){a.render()}); -if(!a.seriesGroup)a.seriesGroup=c.g("series-group").attr({zIndex:3}).add();n(a.series,function(a){a.translate();a.setTooltipPoints();a.render()});e.items&&n(e.items,function(b){var d=y(e.style,b.style),f=C(d.left)+a.plotLeft,g=C(d.top)+a.plotTop+12;delete d.left;delete d.top;c.text(b.html,f,g).attr({zIndex:2}).css(d).add()});if(f.enabled&&!a.credits)g=f.href,a.credits=c.text(f.text,0,0).on("click",function(){if(g)location.href=g}).attr({align:f.position.align,zIndex:8}).css(f.style).add().align(f.position); -a.hasRendered=!0},destroy:function(){var a=this,b=a.axes,c=a.series,d=a.container,e,f=d&&d.parentNode;K(a,"destroy");Ua[a.index]=w;a.renderTo.removeAttribute("data-highcharts-chart");X(a);for(e=b.length;e--;)b[e]=b[e].destroy();for(e=c.length;e--;)c[e]=c[e].destroy();n("title,subtitle,chartBackground,plotBackground,plotBGImage,plotBorder,seriesGroup,clipRect,credits,pointer,scroller,rangeSelector,legend,resetZoomButton,tooltip,renderer".split(","),function(b){var c=a[b];c&&c.destroy&&(a[b]=c.destroy())}); -if(d)d.innerHTML="",X(d),f&&Za(d);for(e in a)delete a[e]},isReadyToRender:function(){var a=this;return!ca&&Y==Y.top&&H.readyState!=="complete"||ia&&!Y.canvg?(ia?$b.push(function(){a.firstRender()},a.options.global.canvasToolsURL):H.attachEvent("onreadystatechange",function(){H.detachEvent("onreadystatechange",a.firstRender);H.readyState==="complete"&&a.firstRender()}),!1):!0},firstRender:function(){var a=this,b=a.options,c=a.callback;if(a.isReadyToRender())a.getContainer(),K(a,"init"),a.resetMargins(), -a.setChartSize(),a.propFromSeries(),a.getAxes(),n(b.series||[],function(b){a.initSeries(b)}),K(a,"beforeRender"),a.pointer=new pb(a,b),a.render(),a.renderer.draw(),c&&c.apply(a,[a]),n(a.callbacks,function(b){b.apply(a,[a])}),a.cloneRenderTo(!0),K(a,"load")}};Sa.prototype.callbacks=[];var Ha=function(){};Ha.prototype={init:function(a,b,c){this.series=a;this.applyOptions(b,c);this.pointAttr={};if(a.options.colorByPoint&&(b=a.options.colors||a.chart.options.colors,this.color=this.color||b[a.colorCounter++], -a.colorCounter===b.length))a.colorCounter=0;a.chart.pointCount++;return this},applyOptions:function(a,b){var c=this.series,d=c.pointValKey,a=Ha.prototype.optionsToObject.call(this,a);y(this,a);this.options=this.options?y(this.options,a):a;if(d)this.y=this[d];if(this.x===w&&c)this.x=b===w?c.autoIncrement():b;return this},optionsToObject:function(a){var b,c=this.series,d=c.pointArrayMap||["y"],e=d.length,f=0,g=0;if(typeof a==="number"||a===null)b={y:a};else if(Wa(a)){b={};if(a.length>e){c=typeof a[0]; -if(c==="string")b.name=a[0];else if(c==="number")b.x=a[0];f++}for(;ga+1&&b.push(d.slice(a+1,g)),a=g):g===e-1&&b.push(d.slice(a+1,g+1))});this.segments=b},setOptions:function(a){var b=this.chart.options,c=b.plotOptions, -d=c[this.type];this.userOptions=a;a=z(d,c.series,a);this.tooltipOptions=z(b.tooltip,a.tooltip);d.marker===null&&delete a.marker;return a},getColor:function(){var a=this.options,b=this.userOptions,c=this.chart.options.colors,d=this.chart.counters,e;e=a.color||S[this.type].color;if(!e&&!a.colorByPoint)v(b._colorIndex)?a=b._colorIndex:(b._colorIndex=d.color,a=d.color++),e=c[a];this.color=e;d.wrapColor(c.length)},getSymbol:function(){var a=this.userOptions,b=this.options.marker,c=this.chart,d=c.options.symbols, -c=c.counters;this.symbol=b.symbol;if(!this.symbol)v(a._symbolIndex)?a=a._symbolIndex:(a._symbolIndex=c.symbol,a=c.symbol++),this.symbol=d[a];if(/^url/.test(this.symbol))b.radius=0;c.wrapSymbol(d.length)},drawLegendSymbol:function(a){var b=this.options,c=b.marker,d=a.options.symbolWidth,e=this.chart.renderer,f=this.legendGroup,a=a.baseline,g;if(b.lineWidth){g={"stroke-width":b.lineWidth};if(b.dashStyle)g.dashstyle=b.dashStyle;this.legendLine=e.path(["M",0,a-4,"L",d,a-4]).attr(g).add(f)}if(c&&c.enabled)b= -c.radius,this.legendSymbol=e.symbol(this.symbol,d/2-b,a-4-b,2*b,2*b).add(f)},addPoint:function(a,b,c,d){var e=this.options,f=this.data,g=this.graph,h=this.area,i=this.chart,j=this.xData,k=this.yData,m=this.zData,l=this.names,o=g&&g.shift||0,q=e.data;$a(d,i);if(g&&c)g.shift=o+1;if(h){if(c)h.shift=o+1;h.isArea=!0}b=p(b,!0);d={series:this};this.pointClass.prototype.applyOptions.apply(d,[a]);j.push(d.x);k.push(this.toYData?this.toYData(d):d.y);m.push(d.z);if(l)l[d.x]=d.name;q.push(a);e.legendType==="point"&& -this.generatePoints();c&&(f[0]&&f[0].remove?f[0].remove(!1):(f.shift(),j.shift(),k.shift(),m.shift(),q.shift()));this.getAttribs();this.isDirtyData=this.isDirty=!0;b&&i.redraw()},setData:function(a,b){var c=this.points,d=this.options,e=this.chart,f=null,g=this.xAxis,h=g&&g.categories&&!g.categories.length?[]:null,i;this.xIncrement=null;this.pointRange=g&&g.categories?1:d.pointRange;this.colorCounter=0;var j=[],k=[],m=[],l=a?a.length:[],o=(i=this.pointArrayMap)&&i.length,q=!!this.toYData;if(l>(d.turboThreshold|| -1E3)){for(i=0;f===null&&i1&&j[1]k||this.forceCrop))if(a=i.getExtremes(),i=a.min,k=a.max,b[d-1]k)b=[],c=[];else if(b[0]k){for(a=0;a=i){e=t(0,a-1);break}for(;ak){f=a+1;break}b=b.slice(e,f);c=c.slice(e,f);g=!0}for(a=b.length-1;a>0;a--)if(d=b[a]-b[a-1],d>0&&(h===w||d= -0&&c<=d;)h[c++]=f}this.tooltipPoints=h}},tooltipHeaderFormatter:function(a){var b=this.tooltipOptions,c=b.xDateFormat,d=b.dateTimeLabelFormats,e=this.xAxis,f=e&&e.options.type==="datetime",b=b.headerFormat,e=e&&e.closestPointRange,g;if(f&&!c)if(e)for(g in I){if(I[g]>=e){c=d[g];break}}else c=d.day;f&&c&&Ja(a.key)&&(b=b.replace("{point.key}","{point.key:"+c+"}"));return La(b,{point:a,series:this})},onMouseOver:function(){var a=this.chart,b=a.hoverSeries;if(b&&b!==this)b.onMouseOut();this.options.events.mouseOver&& -K(this,"mouseOver");this.setState("hover");a.hoverSeries=this},onMouseOut:function(){var a=this.options,b=this.chart,c=b.tooltip,d=b.hoverPoint;if(d)d.onMouseOut();this&&a.events.mouseOut&&K(this,"mouseOut");c&&!a.stickyTracking&&(!c.shared||this.noSharedTooltip)&&c.hide();this.setState();b.hoverSeries=null},animate:function(a){var b=this,c=b.chart,d=c.renderer,e;e=b.options.animation;var f=c.clipBox,g=c.inverted,h;if(e&&!da(e))e=S[b.type].animation;h="_sharedClip"+e.duration+e.easing;if(a)a=c[h], -e=c[h+"m"],a||(c[h]=a=d.clipRect(y(f,{width:0})),c[h+"m"]=e=d.clipRect(-99,g?-c.plotLeft:-c.plotTop,99,g?c.chartWidth:c.chartHeight)),b.group.clip(a),b.markerGroup.clip(e),b.sharedClipKey=h;else{if(a=c[h])a.animate({width:c.plotSizeX},e),c[h+"m"].animate({width:c.plotSizeX+99},e);b.animate=null;b.animationTimeout=setTimeout(function(){b.afterAnimate()},e.duration)}},afterAnimate:function(){var a=this.chart,b=this.sharedClipKey,c=this.group;c&&this.options.clip!==!1&&(c.clip(a.clipRect),this.markerGroup.clip()); -setTimeout(function(){b&&a[b]&&(a[b]=a[b].destroy(),a[b+"m"]=a[b+"m"].destroy())},100)},drawPoints:function(){var a,b=this.points,c=this.chart,d,e,f,g,h,i,j,k,m=this.options.marker,l,o=this.markerGroup;if(m.enabled||this._hasPointMarkers)for(f=b.length;f--;)if(g=b[f],d=g.plotX,e=g.plotY,k=g.graphic,i=g.marker||{},a=m.enabled&&i.enabled===w||i.enabled,l=c.isInsidePlot(r(d),e,c.inverted),a&&e!==w&&!isNaN(e)&&g.y!==null)if(a=g.pointAttr[g.selected?"select":""],h=a.r,i=p(i.symbol,this.symbol),j=i.indexOf("url")=== -0,k)k.attr({visibility:l?ca?"inherit":"visible":"hidden"}).animate(y({x:d-h,y:e-h},k.symbolName?{width:2*h,height:2*h}:{}));else{if(l&&(h>0||j))g.graphic=c.renderer.symbol(i,d-h,e-h,2*h,2*h).attr(a).add(o)}else if(k)g.graphic=k.destroy()},convertAttribs:function(a,b,c,d){var e=this.pointAttrToOptions,f,g,h={},a=a||{},b=b||{},c=c||{},d=d||{};for(f in e)g=e[f],h[f]=p(a[g],b[f],c[f],d[f]);return h},getAttribs:function(){var a=this,b=a.options,c=S[a.type].marker?b.marker:b,d=c.states,e=d.hover,f,g=a.color, -h={stroke:g,fill:g},i=a.points||[],j=[],k,m=a.pointAttrToOptions,l=b.negativeColor,o;b.marker?(e.radius=e.radius||c.radius+2,e.lineWidth=e.lineWidth||c.lineWidth+1):e.color=e.color||wa(e.color||g).brighten(e.brightness).get();j[""]=a.convertAttribs(c,h);n(["hover","select"],function(b){j[b]=a.convertAttribs(d[b],j[""])});a.pointAttr=j;for(g=i.length;g--;){h=i[g];if((c=h.options&&h.options.marker||h.options)&&c.enabled===!1)c.radius=0;if(h.negative&&l)h.color=h.fillColor=l;f=b.colorByPoint||h.color; -if(h.options)for(o in m)v(c[m[o]])&&(f=!0);if(f){c=c||{};k=[];d=c.states||{};f=d.hover=d.hover||{};if(!b.marker)f.color=wa(f.color||h.color).brighten(f.brightness||e.brightness).get();k[""]=a.convertAttribs(y({color:h.color},c),j[""]);k.hover=a.convertAttribs(d.hover,j.hover,k[""]);k.select=a.convertAttribs(d.select,j.select,k[""]);if(h.negative&&b.marker&&l)k[""].fill=k.hover.fill=k.select.fill=a.convertAttribs({fillColor:l}).fill}else k=j;h.pointAttr=k}},update:function(a,b){var c=this.chart,d= -this.type,a=z(this.userOptions,{animation:!1,index:this.index,pointStart:this.xData[0]},a);this.remove(!1);y(this,Q[a.type||d].prototype);this.init(c,a);p(b,!0)&&c.redraw(!1)},destroy:function(){var a=this,b=a.chart,c=/AppleWebKit\/533/.test(Ta),d,e,f=a.data||[],g,h,i;K(a,"destroy");X(a);n(["xAxis","yAxis"],function(b){if(i=a[b])na(i.series,a),i.isDirty=i.forceRedraw=!0});a.legendItem&&a.chart.legend.destroyItem(a);for(e=f.length;e--;)(g=f[e])&&g.destroy&&g.destroy();a.points=null;clearTimeout(a.animationTimeout); -n("area,graph,dataLabelsGroup,group,markerGroup,tracker,graphNeg,areaNeg,posClip,negClip".split(","),function(b){a[b]&&(d=c&&b==="group"?"hide":"destroy",a[b][d]())});if(b.hoverSeries===a)b.hoverSeries=null;na(b.series,a);for(h in a)delete a[h]},drawDataLabels:function(){var a=this,b=a.options.dataLabels,c=a.points,d,e,f,g;if(b.enabled||a._hasPointLabels)a.dlProcessOptions&&a.dlProcessOptions(b),g=a.plotGroup("dataLabelsGroup","data-labels",a.visible?"visible":"hidden",b.zIndex||6),e=b,n(c,function(c){var i, -j=c.dataLabel,k,m,l=c.connector,o=!0;d=c.options&&c.options.dataLabels;i=e.enabled||d&&d.enabled;if(j&&!i)c.dataLabel=j.destroy();else if(i){i=b.rotation;b=z(e,d);k=c.getLabelConfig();f=b.format?La(b.format,k):b.formatter.call(k,b);b.style.color=p(b.color,b.style.color,a.color,"black");if(j)if(v(f))j.attr({text:f}),o=!1;else{if(c.dataLabel=j=j.destroy(),l)c.connector=l.destroy()}else if(v(f)){j={fill:b.backgroundColor,stroke:b.borderColor,"stroke-width":b.borderWidth,r:b.borderRadius||0,rotation:i, -padding:b.padding,zIndex:1};for(m in j)j[m]===w&&delete j[m];j=c.dataLabel=a.chart.renderer[i?"text":"label"](f,0,-999,null,null,null,b.useHTML).attr(j).css(b.style).add(g).shadow(b.shadow)}j&&a.alignDataLabel(c,j,b,null,o)}})},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=p(a.plotX,-999),a=p(a.plotY,-999),i=b.getBBox(),d=y({x:g?f.plotWidth-a:h,y:r(g?f.plotHeight-h:a),width:0,height:0},d);y(c,{width:i.width,height:i.height});c.rotation?(d={align:c.align,x:d.x+c.x+d.width/2,y:d.y+ -c.y+d.height/2},b[e?"attr":"animate"](d)):b.align(c,null,d);b.attr({visibility:c.crop===!1||f.isInsidePlot(h,a,g)?f.renderer.isSVG?"inherit":"visible":"hidden"})},getSegmentPath:function(a){var b=this,c=[],d=b.options.step;n(a,function(e,f){var g=e.plotX,h=e.plotY,i;b.getPointSpline?c.push.apply(c,b.getPointSpline(a,e,f)):(c.push(f?"L":"M"),d&&f&&(i=a[f-1],d==="right"?c.push(i.plotX,h):d==="center"?c.push((i.plotX+g)/2,i.plotY,(i.plotX+g)/2,h):c.push(g,i.plotY)),c.push(e.plotX,e.plotY))});return c}, -getGraphPath:function(){var a=this,b=[],c,d=[];n(a.segments,function(e){c=a.getSegmentPath(e);e.length>1?b=b.concat(c):d.push(e[0])});a.singlePoints=d;return a.graphPath=b},drawGraph:function(){var a=this,b=this.options,c=[["graph",b.lineColor||this.color]],d=b.lineWidth,e=b.dashStyle,f=this.getGraphPath(),g=b.negativeColor;g&&c.push(["graphNeg",g]);n(c,function(c,g){var j=c[0],k=a[j];if(k)hb(k),k.animate({d:f});else if(d&&f.length){k={stroke:c[1],"stroke-width":d,zIndex:1};if(e)k.dashstyle=e;a[j]= -a.chart.renderer.path(f).attr(k).add(a.group).shadow(!g&&b.shadow)}})},clipNeg:function(){var a=this.options,b=this.chart,c=b.renderer,d=a.negativeColor,e,f=this.graph,g=this.area,h=this.posClip,i=this.negClip;e=b.chartWidth;var j=b.chartHeight,k=t(e,j);if(d&&(f||g))d=pa(this.yAxis.len-this.yAxis.translate(a.threshold||0)),a={x:0,y:0,width:k,height:d},k={x:0,y:d,width:k,height:k-d},b.inverted&&c.isVML&&(a={x:b.plotWidth-d-b.plotLeft,y:0,width:e,height:j},k={x:d+b.plotLeft-e,y:0,width:b.plotLeft+d, -height:e}),this.yAxis.reversed?(b=k,e=a):(b=a,e=k),h?(h.animate(b),i.animate(e)):(this.posClip=h=c.clipRect(b),this.negClip=i=c.clipRect(e),f&&(f.clip(h),this.graphNeg.clip(i)),g&&(g.clip(h),this.areaNeg.clip(i)))},invertGroups:function(){function a(){var a={width:b.yAxis.len,height:b.xAxis.len};n(["group","markerGroup"],function(c){b[c]&&b[c].attr(a).invert()})}var b=this,c=b.chart;if(b.xAxis)F(c,"resize",a),F(b,"destroy",function(){X(c,"resize",a)}),a(),b.invertGroups=a},plotGroup:function(a,b, -c,d,e){var f=this[a],g=!f,h=this.chart,i=this.xAxis,j=this.yAxis;g&&(this[a]=f=h.renderer.g(b).attr({visibility:c,zIndex:d||0.1}).add(e));f[g?"attr":"animate"]({translateX:i?i.left:h.plotLeft,translateY:j?j.top:h.plotTop,scaleX:1,scaleY:1});return f},render:function(){var a=this.chart,b,c=this.options,d=c.animation&&!!this.animate&&a.renderer.isSVG,e=this.visible?"visible":"hidden",f=c.zIndex,g=this.hasRendered,h=a.seriesGroup;b=this.plotGroup("group","series",e,f,h);this.markerGroup=this.plotGroup("markerGroup", -"markers",e,f,h);d&&this.animate(!0);this.getAttribs();b.inverted=this.isCartesian?a.inverted:!1;this.drawGraph&&(this.drawGraph(),this.clipNeg());this.drawDataLabels();this.drawPoints();this.options.enableMouseTracking!==!1&&this.drawTracker();a.inverted&&this.invertGroups();c.clip!==!1&&!this.sharedClipKey&&!g&&b.clip(a.clipRect);d?this.animate():g||this.afterAnimate();this.isDirty=this.isDirtyData=!1;this.hasRendered=!0},redraw:function(){var a=this.chart,b=this.isDirtyData,c=this.group,d=this.xAxis, -e=this.yAxis;c&&(a.inverted&&c.attr({width:a.plotWidth,height:a.plotHeight}),c.animate({translateX:p(d&&d.left,a.plotLeft),translateY:p(e&&e.top,a.plotTop)}));this.translate();this.setTooltipPoints(!0);this.render();b&&K(this,"updatedData")},setState:function(a){var b=this.options,c=this.graph,d=this.graphNeg,e=b.states,b=b.lineWidth,a=a||"";if(this.state!==a)this.state=a,e[a]&&e[a].enabled===!1||(a&&(b=e[a].lineWidth||b+1),c&&!c.dashstyle&&(a={"stroke-width":b},c.attr(a),d&&d.attr(a)))},setVisible:function(a, -b){var c=this,d=c.chart,e=c.legendItem,f,g=d.options.chart.ignoreHiddenSeries,h=c.visible;f=(c.visible=a=c.userOptions.visible=a===w?!h:a)?"show":"hide";n(["group","dataLabelsGroup","markerGroup","tracker"],function(a){if(c[a])c[a][f]()});if(d.hoverSeries===c)c.onMouseOut();e&&d.legend.colorizeItem(c,a);c.isDirty=!0;c.options.stacking&&n(d.series,function(a){if(a.options.stacking&&a.visible)a.isDirty=!0});n(c.linkedSeries,function(b){b.setVisible(a,!1)});if(g)d.isDirtyBox=!0;b!==!1&&d.redraw();K(c, -f)},show:function(){this.setVisible(!0)},hide:function(){this.setVisible(!1)},select:function(a){this.selected=a=a===w?!this.selected:a;if(this.checkbox)this.checkbox.checked=a;K(this,a?"select":"unselect")},drawTracker:function(){var a=this,b=a.options,c=b.trackByArea,d=[].concat(c?a.areaPath:a.graphPath),e=d.length,f=a.chart,g=f.pointer,h=f.renderer,i=f.options.tooltip.snap,j=a.tracker,k=b.cursor,k=k&&{cursor:k},m=a.singlePoints,l,o=function(){if(f.hoverSeries!==a)a.onMouseOver()};if(e&&!c)for(l= -e+1;l--;)d[l]==="M"&&d.splice(l+1,0,d[l+1]-i,d[l+2],"L"),(l&&d[l]==="M"||l===e)&&d.splice(l,0,"L",d[l-2]+i,d[l-1]);for(l=0;l=0;d--)da&&i>e?(i=t(a,e),k=2*e-i):ig&&k>e?(k=t(g,e),i=2*e-k):kh?o-h:g-(f.translate(c.y,0,1,0,1)<=g?h:0));c.barX=q;c.pointWidth=i;c.shapeType="rect";c.shapeArgs=c=b.renderer.Element.prototype.crisp.call(0,e,q,n,j,l);e%2&&(c.y-=1,c.height+=1)})},getSymbol:qa,drawLegendSymbol:D.prototype.drawLegendSymbol,drawGraph:qa,drawPoints:function(){var a=this,b=a.options,c=a.chart.renderer,d;n(a.points,function(e){var f=e.plotY,g=e.graphic;if(f!==w&&!isNaN(f)&&e.y!==null)d=e.shapeArgs,g?(hb(g),g.animate(z(d))):e.graphic= -c[e.shapeType](d).attr(e.pointAttr[e.selected?"select":""]).add(a.group).shadow(b.shadow,null,b.stacking&&!b.borderRadius);else if(g)e.graphic=g.destroy()})},drawTracker:function(){var a=this,b=a.chart.pointer,c=a.options.cursor,d=c&&{cursor:c},e=function(b){var c=b.target,d;for(a.onMouseOver();c&&!d;)d=c.point,c=c.parentNode;if(d!==w)d.onMouseOver(b)};n(a.points,function(a){if(a.graphic)a.graphic.element.point=a;if(a.dataLabel)a.dataLabel.element.point=a});a._hasTracking?a._hasTracking=!0:n(a.trackerGroups, -function(c){if(a[c]&&(a[c].addClass("highcharts-tracker").on("mouseover",e).on("mouseout",function(a){b.onTrackerMouseOut(a)}).css(d),gb))a[c].on("touchstart",e)})},alignDataLabel:function(a,b,c,d,e){var f=this.chart,g=f.inverted,h=a.dlBox||a.shapeArgs,i=a.below||a.plotY>p(this.translatedThreshold,f.plotSizeY),j=p(c.inside,!!this.options.stacking);if(h&&(d=z(h),g&&(d={x:f.plotWidth-d.y-d.height,y:f.plotHeight-d.x-d.width,width:d.height,height:d.width}),!j))g?(d.x+=i?0:d.width,d.width=0):(d.y+=i?d.height: -0,d.height=0);c.align=p(c.align,!g||j?"center":i?"right":"left");c.verticalAlign=p(c.verticalAlign,g||j?"middle":i?"top":"bottom");$.prototype.alignDataLabel.call(this,a,b,c,d,e)},animate:function(a){var b=this.yAxis,c=this.options,d=this.chart.inverted,e={};if(ca)a?(e.scaleY=0.001,a=A(b.pos+b.len,t(b.pos,b.toPixels(c.threshold))),d?e.translateX=a-b.len:e.translateY=a,this.group.attr(e)):(e.scaleY=1,e[d?"translateX":"translateY"]=b.pos,this.group.animate(e,this.options.animation),this.animate=null)}, -remove:function(){var a=this,b=a.chart;b.hasRendered&&n(b.series,function(b){if(b.type===a.type)b.isDirty=!0});$.prototype.remove.apply(a,arguments)}});Q.column=Z;S.bar=z(S.column);Va=ea(Z,{type:"bar",inverted:!0});Q.bar=Va;S.scatter=z(R,{lineWidth:0,tooltip:{headerFormat:'{series.name}
',pointFormat:"x: {point.x}
y: {point.y}
",followPointer:!0},stickyTracking:!1});Va=ea($,{type:"scatter",sorted:!1,requireSorting:!1, -noSharedTooltip:!0,trackerGroups:["markerGroup"],drawTracker:Z.prototype.drawTracker,setTooltipPoints:qa});Q.scatter=Va;S.pie=z(R,{borderColor:"#FFFFFF",borderWidth:1,center:[null,null],clip:!1,colorByPoint:!0,dataLabels:{distance:30,enabled:!0,formatter:function(){return this.point.name}},ignoreHiddenPoint:!0,legendType:"point",marker:null,size:null,showInLegend:!1,slicedOffset:10,states:{hover:{brightness:0.1,shadow:!1}},stickyTracking:!1,tooltip:{followPointer:!0}});R={type:"pie",isCartesian:!1, -pointClass:ea(Ha,{init:function(){Ha.prototype.init.apply(this,arguments);var a=this,b;if(a.y<0)a.y=null;y(a,{visible:a.visible!==!1,name:p(a.name,"Slice")});b=function(){a.slice()};F(a,"select",b);F(a,"unselect",b);return a},setVisible:function(a){var b=this,c=b.series,d=c.chart,e;b.visible=b.options.visible=a=a===w?!b.visible:a;c.options.data[va(b,c.data)]=b.options;e=a?"show":"hide";n(["graphic","dataLabel","connector","shadowGroup"],function(a){if(b[a])b[a][e]()});b.legendItem&&d.legend.colorizeItem(b, -a);if(!c.isDirty&&c.options.ignoreHiddenPoint)c.isDirty=!0,d.redraw()},slice:function(a,b,c){var d=this.series;$a(c,d.chart);p(b,!0);this.sliced=this.options.sliced=a=v(a)?a:!this.sliced;d.options.data[va(this,d.data)]=this.options;a=a?this.slicedTranslation:{translateX:0,translateY:0};this.graphic.animate(a);this.shadowGroup&&this.shadowGroup.animate(a)}}),requireSorting:!1,noSharedTooltip:!0,trackerGroups:["group","dataLabelsGroup"],pointAttrToOptions:{stroke:"borderColor","stroke-width":"borderWidth", -fill:"color"},getColor:qa,animate:function(a){var b=this,c=b.points,d=b.startAngleRad;if(!a)n(c,function(a){var c=a.graphic,a=a.shapeArgs;c&&(c.attr({r:b.center[3]/2,start:d,end:d}),c.animate({r:a.r,start:a.start,end:a.end},b.options.animation))}),b.animate=null},setData:function(a,b){$.prototype.setData.call(this,a,!1);this.processData();this.generatePoints();p(b,!0)&&this.chart.redraw()},getCenter:function(){var a=this.options,b=this.chart,c=2*(a.slicedOffset||0),d,e=b.plotWidth-2*c,f=b.plotHeight- -2*c,b=a.center,a=[p(b[0],"50%"),p(b[1],"50%"),a.size||"100%",a.innerSize||0],g=A(e,f),h;return Fa(a,function(a,b){h=/%$/.test(a);d=b<2||b===2&&h;return(h?[e,f,g,g][b]*C(a)/100:a)+(d?c:0)})},translate:function(a){this.generatePoints();var b=0,c=0,d=this.options,e=d.slicedOffset,f=e+d.borderWidth,g,h,i,j=this.startAngleRad=bb/180*((d.startAngle||0)%360-90),k=this.points,m=2*bb,l=d.dataLabels.distance,o=d.ignoreHiddenPoint,q,n=k.length,p;if(!a)this.center=a=this.getCenter();this.getX=function(b,c){i= -P.asin((b-a[1])/(a[2]/2+l));return a[0]+(c?-1:1)*ha(i)*(a[2]/2+l)};for(q=0;q0.75*m&&(i-=2*bb);p.slicedTranslation={translateX:r(ha(i)*e),translateY:r(ka(i)*e)};g=ha(i)*a[2]/2;h=ka(i)*a[2]/2;p.tooltipPos=[a[0]+g*0.7,a[1]+h*0.7];p.half=i0,u,s,x,w,z=[[],[]],E,B,y,A,J,C=[0,0,0,0], -db=function(a,b){return b.y-a.y},F=function(a,b){a.sort(function(a,c){return a.angle!==void 0&&(c.angle-a.angle)*b})};if(e.enabled||a._hasPointLabels){$.prototype.drawDataLabels.apply(a);n(b,function(a){a.dataLabel&&z[a.half].push(a)});for(A=0;!w&&b[A];)w=b[A]&&b[A].dataLabel&&(b[A].dataLabel.getBBox().height||21),A++;for(A=2;A--;){var b=[],G=[],I=z[A],H=I.length,D;F(I,A-0.5);if(m>0){for(J=q-o-m;J<=q+o+m;J+=w)b.push(J);s=b.length;if(H>s){c=[].concat(I);c.sort(db);for(J=H;J--;)c[J].rank=J;for(J=H;J--;)I[J].rank>= -s&&I.splice(J,1);H=I.length}for(J=0;J0){if(s=G.pop(),D=s.i,B=s.y,c>B&&b[D+1]!==null||ch-f&&(C[1]=t(r(E+s-h+f),C[1])),B-w/2<0?C[0]=t(r(-B+w/2),C[0]):B+w/2>d&&(C[2]=t(r(B+w/2-d),C[2]))}}if(ua(C)===0||this.verifyDataLabelOverflow(C))this.placeDataLabels(),v&&g&&n(this.points,function(b){i=b.connector;x=b.labelPos;if((u=b.dataLabel)&&u._pos)y=u._attr.visibility,E=u.connX,B=u.connY,j=k?["M",E+(x[6]==="left"?5:-5),B, -"C",E,B,2*x[2]-x[4],2*x[3]-x[5],x[2],x[3],"L",x[4],x[5]]:["M",E+(x[6]==="left"?5:-5),B,"L",x[2],x[3],"L",x[4],x[5]],i?(i.animate({d:j}),i.attr("visibility",y)):b.connector=i=a.chart.renderer.path(j).attr({"stroke-width":g,stroke:e.connectorColor||b.color||"#606060",visibility:y}).add(a.group);else if(i)b.connector=i.destroy()})}},verifyDataLabelOverflow:function(a){var b=this.center,c=this.options,d=c.center,e=c=c.minSize||80,f;d[0]!==null?e=t(b[2]-t(a[1],a[3]),c):(e=t(b[2]-a[1]-a[3],c),b[0]+=(a[3]- -a[1])/2);d[1]!==null?e=t(A(e,b[2]-t(a[0],a[2])),c):(e=t(A(e,b[2]-a[0]-a[2]),c),b[1]+=(a[0]-a[2])/2);e=c[1]||n===i;)if(j=c.shift(), -k=d.apply(0,l),k!==w&&(g.push(j),h.push(k)),l[0]=[],l[1]=[],l[2]=[],l[3]=[],n===i)break;if(n===i)break;if(o){j=this.cropStart+n;j=e&&e[j]||this.pointClass.prototype.applyOptions.apply({series:this},[f[j]]);var p;for(k=0;kg/i||j&&c.forced){e=!0;this.points=null;a=h.getExtremes();j=a.min;k=a.max;a=h.getGroupIntervalFactor&& -h.getGroupIntervalFactor(j,k,d)||1;g=i*(k-j)/g*a;h=(h.getNonLinearTimeTicks||fb)(zb(g,c.units||bc),j,k,null,d,this.closestPointRange);f=V.groupData.apply(this,[d,f,h,c.approximation]);d=f[0];f=f[1];if(c.smoothed){a=d.length-1;for(d[a]=k;a--&&a>0;)d[a]+=g/2;d[0]=j}this.currentDataGrouping=h.info;if(b.pointRange===null)this.pointRange=h.info.totalRange;this.closestPointRange=h.info.totalRange;this.processedXData=d;this.processedYData=f}else this.currentDataGrouping=null,this.pointRange=m;this.hasGroupedData= -e}};V.destroyGroupedData=function(){var a=this.groupedData;n(a||[],function(b,c){b&&(a[c]=b.destroy?b.destroy():null)});this.groupedData=null};V.generatePoints=function(){gc.apply(this);this.destroyGroupedData();this.groupedData=this.hasGroupedData?this.points:null};V.tooltipHeaderFormatter=function(a){var b=this.tooltipOptions,c=this.options.dataGrouping,d=b.xDateFormat,e,f=this.xAxis,g,h;if(f&&f.options.type==="datetime"&&c&&Ja(a.key)){g=this.currentDataGrouping;c=c.dateTimeLabelFormats;if(g)f= -c[g.unitName],g.count===1?d=f[0]:(d=f[1],e=f[2]);else if(!d&&c)for(h in I)if(I[h]>=f.closestPointRange){d=c[h][0];break}d=ya(d,a.key);e&&(d+=ya(e,a.key+g.totalRange-1));a=b.headerFormat.replace("{point.key}",d)}else a=ic.call(this,a);return a};V.destroy=function(){for(var a=this.groupedData||[],b=a.length;b--;)a[b]&&a[b].destroy();hc.apply(this)};sa(V,"setOptions",function(a,b){var c=a.call(this,b),d=this.type,e=this.chart.options.plotOptions,f=S[d].dataGrouping;if(ac[d])f||(f=z(jc,ac[d])),c.dataGrouping= -z(f,e.series&&e.series.dataGrouping,e[d].dataGrouping,b.dataGrouping);if(this.chart.options._stock)this.requireSorting=!0;return c});S.ohlc=z(S.column,{lineWidth:1,tooltip:{pointFormat:'{series.name}
Open: {point.open}
High: {point.high}
Low: {point.low}
Close: {point.close}
'},states:{hover:{lineWidth:3}},threshold:null});R=ea(Q.column,{type:"ohlc",pointArrayMap:["open","high","low","close"],toYData:function(a){return[a.open, -a.high,a.low,a.close]},pointValKey:"high",pointAttrToOptions:{stroke:"color","stroke-width":"lineWidth"},upColorProp:"stroke",getAttribs:function(){Q.column.prototype.getAttribs.apply(this,arguments);var a=this.options,b=a.states,a=a.upColor||this.color,c=z(this.pointAttr),d=this.upColorProp;c[""][d]=a;c.hover[d]=b.hover.upColor||a;c.select[d]=b.select.upColor||a;n(this.points,function(a){if(a.open"},threshold:null,y:-30});Q.flags=ea(Q.column,{type:"flags",sorted:!1,noSharedTooltip:!0,takeOrdinalPosition:!1,forceCrop:!0,init:$.prototype.init,pointAttrToOptions:{fill:"fillColor",stroke:"color","stroke-width":"lineWidth",r:"radius"},translate:function(){Q.column.prototype.translate.apply(this);var a=this.chart,b=this.points,c=b.length-1,d,e,f=this.options.onSeries,f=(d=f&& -a.get(f))&&d.options.step,g=d&&d.points,h=g&&g.length,i=this.xAxis,j=i.getExtremes(),k,m,l;if(d&&d.visible&&h){m=g[h-1].x;for(b.sort(function(a,b){return a.x-b.x});h--&&b[c];)if(d=b[c],k=g[h],k.x<=d.x&&k.plotY!==w){if(d.x<=m)d.plotY=k.plotY,k.x=j.min&&c.x<=j.max?c.plotY=i.lineTop-a.plotTop:c.shapeArgs={};if((e=b[d-1])&&e.plotX===c.plotX){if(e.stackIndex=== -w)e.stackIndex=0;c.stackIndex=e.stackIndex+1}})},drawPoints:function(){var a,b=this.points,c=this.chart.renderer,d,e,f=this.options,g=f.y,h,i,j,k,m,l=f.lineWidth%2/2,o;for(j=b.length;j--;)if(k=b[j],d=k.plotX+l,a=k.stackIndex,h=k.options.shape||f.shape,e=k.plotY,e!==w&&(e=k.plotY+g+l-(a!==w&&a*f.stackDistance)),i=a?w:k.plotX+l,o=a?w:k.plotY,m=k.graphic,e!==w)a=k.pointAttr[k.selected?"select":""],m?m.attr({x:d,y:e,r:a.r,anchorX:i,anchorY:o}):m=k.graphic=c.label(k.options.title||f.title||"A",d,e,h,i, -o,f.useHTML).css(z(f.style,k.style)).attr(a).attr({align:h==="flag"?"left":"center",width:f.width,height:f.height}).add(this.group).shadow(f.shadow),i=m.box,a=i.getBBox(),k.shapeArgs=y(a,{x:d-(h==="flag"?0:i.attr("width")/2),y:e});else if(m)k.graphic=m.destroy()},drawTracker:function(){Q.column.prototype.drawTracker.apply(this);ca&&n(this.points,function(a){a.graphic&&F(a.graphic.element,"mouseover",function(){a.graphic.toFront()})})},animate:qa});xb.flag=function(a,b,c,d,e){var f=e&&e.anchorX||a, -e=e&&e.anchorY||b;return["M",f,e,"L",a,b+d,a,b,a+c,b,a+c,b+d,a,b+d,"M",f,e,"Z"]};n(["circle","square"],function(a){xb[a+"pin"]=function(b,c,d,e,f){var g=f&&f.anchorX,f=f&&f.anchorY,b=xb[a](b,c,d,e);g&&f&&b.push("M",g,c>f?c:c+e,"L",g,f);return b}});cb===ib&&n(["flag","circlepin","squarepin"],function(a){ib.prototype.symbols[a]=xb[a]});R=jb("linearGradient",{x1:0,y1:0,x2:0,y2:1},"stops",[[0,"#FFF"],[1,"#CCC"]]);D=[].concat(bc);D[4]=[ga,[1,2,3,4]];D[5]=[Ma,[1,2,3]];y(M,{navigator:{handles:{backgroundColor:"#FFF", -borderColor:"#666"},height:40,margin:10,maskFill:"rgba(255, 255, 255, 0.75)",outlineColor:"#444",outlineWidth:1,series:{type:"areaspline",color:"#4572A7",compare:null,fillOpacity:0.4,dataGrouping:{approximation:"average",groupPixelWidth:2,smoothed:!0,units:D},dataLabels:{enabled:!1,zIndex:2},id:"highcharts-navigator-series",lineColor:"#4572A7",lineWidth:1,marker:{enabled:!1},pointRange:0,shadow:!1},xAxis:{tickWidth:0,lineWidth:0,gridLineWidth:1,tickPixelInterval:200,labels:{align:"left",x:3,y:-4}}, -yAxis:{gridLineWidth:0,startOnTick:!1,endOnTick:!1,minPadding:0.1,maxPadding:0.1,labels:{enabled:!1},title:{text:null},tickWidth:0}},scrollbar:{height:ub?20:14,barBackgroundColor:R,barBorderRadius:2,barBorderWidth:1,barBorderColor:"#666",buttonArrowColor:"#666",buttonBackgroundColor:R,buttonBorderColor:"#666",buttonBorderRadius:2,buttonBorderWidth:1,minWidth:6,rifleColor:"#666",trackBackgroundColor:jb("linearGradient",{x1:0,y1:0,x2:0,y2:1},"stops",[[0,"#EEE"],[1,"#FFF"]]),trackBorderColor:"#CCC", -trackBorderWidth:1,liveRedraw:ca}});Ib.prototype={drawHandle:function(a,b){var c=this.chart,d=c.renderer,e=this.elementsToDestroy,f=this.handles,g=this.navigatorOptions.handles,g={fill:g.backgroundColor,stroke:g.borderColor,"stroke-width":1},h;this.rendered||(f[b]=d.g().css({cursor:"e-resize"}).attr({zIndex:4-b}).add(),h=d.rect(-4.5,0,9,16,3,1).attr(g).add(f[b]),e.push(h),h=d.path(["M",-1.5,4,"L",-1.5,12,"M",0.5,4,"L",0.5,12]).attr(g).add(f[b]),e.push(h));f[b][c.isResizing?"animate":"attr"]({translateX:this.scrollerLeft+ -this.scrollbarHeight+parseInt(a,10),translateY:this.top+this.height/2-8})},drawScrollbarButton:function(a){var b=this.chart.renderer,c=this.elementsToDestroy,d=this.scrollbarButtons,e=this.scrollbarHeight,f=this.scrollbarOptions,g;this.rendered||(d[a]=b.g().add(this.scrollbarGroup),g=b.rect(-0.5,-0.5,e+1,e+1,f.buttonBorderRadius,f.buttonBorderWidth).attr({stroke:f.buttonBorderColor,"stroke-width":f.buttonBorderWidth,fill:f.buttonBackgroundColor}).add(d[a]),c.push(g),g=b.path(["M",e/2+(a?-1:1),e/2- -3,"L",e/2+(a?-1:1),e/2+3,e/2+(a?2:-2),e/2]).attr({fill:f.buttonArrowColor}).add(d[a]),c.push(g));a&&d[a].attr({translateX:this.scrollerWidth-e})},render:function(a,b,c,d){var e=this.chart,f=e.renderer,g,h,i,j,k=this.scrollbarGroup,m=this.navigatorGroup,l=this.scrollbar,m=this.xAxis,o=this.scrollbarTrack,n=this.scrollbarHeight,v=this.scrollbarEnabled,u=this.navigatorOptions,s=this.scrollbarOptions,x=s.minWidth,w=this.height,z=this.top,E=this.navigatorEnabled,B=u.outlineWidth,y=B/2,D=0,J=this.outlineHeight, -I=s.barBorderRadius,H=s.barBorderWidth,F=z+y;if(!isNaN(a)){this.navigatorLeft=g=p(m.left,e.plotLeft+n);this.navigatorWidth=h=p(m.len,e.plotWidth-2*n);this.scrollerLeft=i=g-n;this.scrollerWidth=j=j=h+2*n;if(m.getExtremes){var G=e.xAxis[0].getExtremes(),L=G.dataMin===null,K=m.getExtremes(),M=A(G.dataMin,K.dataMin),G=t(G.dataMax,K.dataMax);!L&&(M!==K.min||G!==K.max)&&m.setExtremes(M,G,!0,!1)}c=p(c,m.translate(a));d=p(d,m.translate(b));this.zoomedMax=a=A(C(t(c,d)),h);this.zoomedMin=d=this.fixedWidth? -a-this.fixedWidth:t(C(A(c,d)),0);this.range=c=a-d;if(!this.rendered){if(E)this.navigatorGroup=m=f.g("navigator").attr({zIndex:3}).add(),this.leftShade=f.rect().attr({fill:u.maskFill}).add(m),this.rightShade=f.rect().attr({fill:u.maskFill}).add(m),this.outline=f.path().attr({"stroke-width":B,stroke:u.outlineColor}).add(m);if(v)this.scrollbarGroup=k=f.g("scrollbar").add(),l=s.trackBorderWidth,this.scrollbarTrack=o=f.rect().attr({y:-l%2/2,fill:s.trackBackgroundColor,stroke:s.trackBorderColor,"stroke-width":l, -r:s.trackBorderRadius||0,height:n}).add(k),this.scrollbar=l=f.rect().attr({y:-H%2/2,height:n,fill:s.barBackgroundColor,stroke:s.barBorderColor,"stroke-width":H,r:I}).add(k),this.scrollbarRifles=f.path().attr({stroke:s.rifleColor,"stroke-width":1}).add(k)}e=e.isResizing?"animate":"attr";E&&(this.leftShade[e]({x:g,y:z,width:d,height:w}),this.rightShade[e]({x:g+a,y:z,width:h-a,height:w}),this.outline[e]({d:["M",i,F,"L",g+d+y,F,g+d+y,F+J-n,"M",g+a-y,F+J-n,"L",g+a-y,F,i+j,F]}),this.drawHandle(d+y,0),this.drawHandle(a+ -y,1));if(v)this.drawScrollbarButton(0),this.drawScrollbarButton(1),k[e]({translateX:i,translateY:r(F+w)}),o[e]({width:j}),g=n+d,h=c-H,h12?"visible":"hidden"})[e]({d:["M",x-3,n/4,"L",x-3,2*n/3,"M",x,n/4,"L",x,2*n/3,"M",x+3,n/4,"L",x+3,2*n/3]});this.scrollbarPad=D;this.rendered=!0}},addEvents:function(){var a=this.chart.container,b=this.mouseDownHandler,c=this.mouseMoveHandler, -d=this.mouseUpHandler,e;e=[[a,"mousedown",b],[a,"mousemove",c],[document,"mouseup",d]];gb&&e.push([a,"touchstart",b],[a,"touchmove",c],[document,"touchend",d]);n(e,function(a){F.apply(null,a)});this._events=e},removeEvents:function(){n(this._events,function(a){X.apply(null,a)});this._events=w;this.navigatorEnabled&&this.baseSeries&&X(this.baseSeries,"updatedData",this.updatedDataHandler)},init:function(){var a=this,b=a.chart,c,d,e=a.scrollbarHeight,f=a.navigatorOptions,g=a.height,h=a.top,i,j,k,m= -document.body.style,l,o=a.baseSeries,n;a.mouseDownHandler=function(d){var d=b.pointer.normalize(d),e=a.zoomedMin,f=a.zoomedMax,h=a.top,i=a.scrollbarHeight,k=a.scrollerLeft,o=a.scrollerWidth,n=a.navigatorLeft,p=a.navigatorWidth,q=a.scrollbarPad,r=a.range,s=d.chartX,u=d.chartY,d=b.xAxis[0],t=ub?10:7;if(u>h&&un+e-q&&sk&&sk+o-i?e+A(10,r):sp&&(f=p-r),f!==e)){a.fixedWidth=r;if(!d.ordinalPositions)d.fixedRange=d.max-d.min;e=c.translate(f,!0);d.setExtremes(e,d.fixedRange?e+d.fixedRange:c.translate(f+r,!0),!0,!1,{trigger:"navigator"})}};a.mouseMoveHandler=function(c){var d=a.scrollbarHeight,e=a.navigatorLeft,f=a.navigatorWidth,g=a.scrollerLeft,h=a.scrollerWidth,i=a.range,l;if(c.pageX!== -0)c=b.pointer.normalize(c),l=c.chartX,lg+h-d&&(l=g+h-d),a.grabbedLeft?(k=!0,a.render(0,0,l-e,a.otherHandlePos)):a.grabbedRight?(k=!0,a.render(0,0,a.otherHandlePos,l-e)):a.grabbedCenter&&(k=!0,lf+j-i&&(l=f+j-i),a.render(0,0,l-j,l-j+i)),k&&a.scrollbarOptions.liveRedraw&&setTimeout(function(){a.mouseUpHandler(c)},0)};a.mouseUpHandler=function(d){k&&b.xAxis[0].setExtremes(c.translate(a.zoomedMin,!0),c.translate(a.zoomedMax,!0),!0,!1,{trigger:"navigator",DOMEvent:d});if(d.type!=="mousemove")a.grabbedLeft= -a.grabbedRight=a.grabbedCenter=a.fixedWidth=k=j=null,m.cursor=l||""};a.updatedDataHandler=function(){var c=o.xAxis,d=c.getExtremes(),e=d.min,f=d.max,g=d.dataMin,d=d.dataMax,h=f-e,j,k,l,m,p;j=i.xData;var r=!!c.setExtremes;k=f>=j[j.length-1];j=e<=g;if(!n)i.options.pointStart=o.xData[0],i.setData(o.options.data,!1),p=!0;j&&(m=g,l=m+h);k&&(l=d,j||(m=t(l-h,i.xData[0])));r&&(j||k)?c.setExtremes(m,l,!0,!1,{trigger:"updatedData"}):(p&&b.redraw(!1),a.render(t(e,g),A(f,d)))};var r=b.xAxis.length,u=b.yAxis.length; -b.extraBottomMargin=a.outlineHeight+f.margin;if(a.navigatorEnabled){var s=o?o.options:{},x=s.data,v=f.series;n=v.data;a.xAxis=c=new Da(b,z({ordinal:o&&o.xAxis.options.ordinal},f.xAxis,{isX:!0,type:"datetime",index:r,height:g,offset:0,offsetLeft:e,offsetRight:-e,startOnTick:!1,endOnTick:!1,minPadding:0,maxPadding:0,zoomEnabled:!1}));a.yAxis=d=new Da(b,z(f.yAxis,{alignTicks:!1,height:g,offset:0,index:u,zoomEnabled:!1}));r=z(s,v,{threshold:null,clip:!1,enableMouseTracking:!1,group:"nav",padXAxis:!1, -xAxis:r,yAxis:u,name:"Navigator",showInLegend:!1,isInternal:!0,visible:!0});r.data=n||x;i=b.initSeries(r);if(o&&f.adaptToUpdatedData!==!1)F(o,"updatedData",a.updatedDataHandler),o.userOptions.events=y(o.userOptions.event,{updatedData:a.updatedDataHandler})}else a.xAxis=c={translate:function(a,c){var d=b.xAxis[0].getExtremes(),f=b.plotWidth-2*e,g=d.dataMin,d=d.dataMax-g;return c?a*d/f+g:f*(a-g)/d}};a.series=i;sa(b,"getMargins",function(b){var e=this.legend,f=e.options;b.call(this);a.top=h=a.navigatorOptions.top|| -this.chartHeight-a.height-a.scrollbarHeight-this.options.chart.spacingBottom-(f.verticalAlign==="bottom"&&f.enabled&&!f.floating?e.legendHeight+p(f.margin,10):0);if(c&&d)c.options.top=d.options.top=h,c.setAxisSize(),d.setAxisSize()});a.addEvents()},destroy:function(){this.removeEvents();n([this.xAxis,this.yAxis,this.leftShade,this.rightShade,this.outline,this.scrollbarTrack,this.scrollbarRifles,this.scrollbarGroup,this.scrollbar],function(a){a&&a.destroy&&a.destroy()});this.xAxis=this.yAxis=this.leftShade= -this.rightShade=this.outline=this.scrollbarTrack=this.scrollbarRifles=this.scrollbarGroup=this.scrollbar=null;n([this.scrollbarButtons,this.handles,this.elementsToDestroy],function(a){Aa(a)})}};Highcharts.Scroller=Ib;sa(Da.prototype,"zoom",function(a,b,c){var d=this.chart,e=d.options,f=e.chart.zoomType,g=e.navigator,e=e.rangeSelector,h;if(this.isXAxis&&(g&&g.enabled||e&&e.enabled))if(f==="x")d.resetZoomButton="blocked";else if(f==="y")h=!1;else if(f==="xy")d=this.previousZoom,v(b)?this.previousZoom= -[this.min,this.max]:d&&(b=d[0],c=d[1],delete this.previousZoom);return h!==w?h:a.call(this,b,c)});sa(Sa.prototype,"init",function(a,b,c){F(this,"beforeRender",function(){var a=this.options;if(a.navigator.enabled||a.scrollbar.enabled)this.scroller=new Ib(this)});a.call(this,b,c)});y(M,{rangeSelector:{buttonTheme:{width:28,height:16,padding:1,r:0,stroke:"#68A",zIndex:7},inputPosition:{align:"right"},labelStyle:{color:"#666"}}});M.lang=z(M.lang,{rangeSelectorZoom:"Zoom",rangeSelectorFrom:"From",rangeSelectorTo:"To"}); -Jb.prototype={clickButton:function(a,b,c){var d=this,e=d.chart,f=d.buttons,g=e.xAxis[0],h=g&&g.getExtremes(),i=e.scroller&&e.scroller.xAxis,j=i&&i.getExtremes&&i.getExtremes(),i=j&&j.dataMin,j=j&&j.dataMax,k=h&&h.dataMin,m=h&&h.dataMax,l=(v(k)&&v(i)?A:p)(k,i),o=(v(m)&&v(j)?t:p)(m,j),q,h=g&&A(h.max,p(o,h.max)),i=new Date(h),j=b.type,k=b.count,r,u,m={millisecond:1,second:1E3,minute:6E4,hour:36E5,day:864E5,week:6048E5};if(!(l===null||o===null||a===d.selected)){if(m[j])r=m[j]*k,q=t(h-r,l);else if(j=== -"month"||j==="year")r={month:"Month",year:"FullYear"}[j],i["set"+r](i["get"+r]()-k),q=t(i.getTime(),p(l,Number.MIN_VALUE)),r={month:30,year:365}[j]*864E5*k;else if(j==="ytd")if(g){if(o===w)l=Number.MAX_VALUE,o=Number.MIN_VALUE,n(e.series,function(a){a=a.xData;l=A(a[0],l);o=t(a[a.length-1],o)}),c=!1;h=new Date(o);u=h.getFullYear();q=u=t(l||0,Date.UTC(u,0,1));h=h.getTime();h=A(o||h,h)}else{F(e,"beforeRender",function(){d.clickButton(a,b)});return}else j==="all"&&g&&(q=l,h=o);f[a]&&f[a].setState(2); -g?g.setExtremes(q,h,p(c,1),0,{trigger:"rangeSelectorButton",rangeSelectorButton:b}):(c=e.options.xAxis,c[0]=z(c[0],{range:r,min:u}));d.selected=a}},defaultButtons:[{type:"month",count:1,text:"1m"},{type:"month",count:3,text:"3m"},{type:"month",count:6,text:"6m"},{type:"ytd",text:"YTD"},{type:"year",count:1,text:"1y"},{type:"all",text:"All"}],init:function(a){var b=this,c=a.options.rangeSelector,d=c.buttons||[].concat(b.defaultButtons),e=b.buttons=[],c=c.selected,f=b.blurInputs=function(){var a=b.minInput, -c=b.maxInput;a&&a.blur();c&&c.blur()};b.chart=a;a.extraTopMargin=25;b.buttonOptions=d;F(a.container,"mousedown",f);F(a,"resize",f);c!==w&&d[c]&&this.clickButton(c,d[c],!1);F(a,"load",function(){F(a.xAxis[0],"afterSetExtremes",function(){if(this.fixedRange!==this.max-this.min)e[b.selected]&&!a.renderer.forExport&&e[b.selected].setState(0),b.selected=null;this.fixedRange=null})})},setInputValue:function(a,b){var c=this.chart.options.rangeSelector;if(b)this[a+"Input"].HCTime=b;this[a+"Input"].value= -ya(c.inputEditDateFormat||"%Y-%m-%d",this[a+"Input"].HCTime);this[a+"DateBox"].attr({text:ya(c.inputDateFormat||"%b %e, %Y",this[a+"Input"].HCTime)})},drawInput:function(a){var b=this,c=b.chart,d=c.options.chart.style,e=c.renderer,f=c.options.rangeSelector,g=b.div,h=a==="min",i,j,k,m=this.inputGroup;this[a+"Label"]=j=e.label(M.lang[h?"rangeSelectorFrom":"rangeSelectorTo"],this.inputGroup.offset).attr({padding:1}).css(z(d,f.labelStyle)).add(m);m.offset+=j.width+5;this[a+"DateBox"]=k=e.label("",m.offset).attr({padding:1, -width:90,height:16,stroke:"silver","stroke-width":1}).css(z({textAlign:"center"},d,f.inputStyle)).on("click",function(){b[a+"Input"].focus()}).add(m);m.offset+=k.width+(h?10:0);this[a+"Input"]=i=aa("input",{name:a,className:"highcharts-range-selector",type:"text"},y({position:"absolute",border:0,width:"1px",height:"1px",padding:0,textAlign:"center",fontSize:d.fontSize,fontFamily:d.fontFamily,top:c.plotTop+"px"},f.inputStyle),g);i.onfocus=function(){L(this,{left:m.translateX+k.x+"px",top:m.translateY+ -"px",width:k.width-2+"px",height:k.height-2+"px",border:"2px solid silver"})};i.onblur=function(){L(this,{border:0,width:"1px",height:"1px"});b.setInputValue(a)};i.onchange=function(){var a=i.value,d=Date.parse(a),e=c.xAxis[0].getExtremes();isNaN(d)&&(d=a.split("-"),d=Date.UTC(C(d[0]),C(d[1])-1,C(d[2])));if(!isNaN(d)&&(M.global.useUTC||(d+=(new Date).getTimezoneOffset()*6E4),h&&d>=e.dataMin&&d<=b.maxInput.HCTime||!h&&d<=e.dataMax&&d>=b.minInput.HCTime))c.xAxis[0].setExtremes(h?d:e.min,h?e.max:d,w, -w,{trigger:"rangeSelectorInput"})}},render:function(a,b){var c=this,d=c.chart,e=d.renderer,f=d.container,g=d.options,h=g.exporting&&d.options.navigation.buttonOptions,i=g.rangeSelector,j=c.buttons,k=M.lang,m=c.div,m=c.inputGroup,l=i.buttonTheme,o=i.inputEnabled!==!1,p=l&&l.states,r=d.plotLeft,u;if(!c.rendered&&(c.zoomText=e.text(k.rangeSelectorZoom,r,d.plotTop-10).css(i.labelStyle).add(),u=r+c.zoomText.getBBox().width+5,n(c.buttonOptions,function(a,b){j[b]=e.button(a.text,u,d.plotTop-25,function(){c.clickButton(b, -a);c.isActive=!0},l,p&&p.hover,p&&p.select).css({textAlign:"center"}).add();u+=j[b].width+(i.buttonSpacing||0);c.selected===b&&j[b].setState(2)}),o))c.div=m=aa("div",null,{position:"relative",height:0,zIndex:1}),f.parentNode.insertBefore(m,f),c.inputGroup=m=e.g("input-group").add(),m.offset=0,c.drawInput("min"),c.drawInput("max");o&&(f=d.plotTop-35,m.align(y({y:f,width:m.offset,x:h&&f<(h.y||0)+h.height-g.chart.spacingTop?-40:0},i.inputPosition),!0,d.spacingBox),c.setInputValue("min",a),c.setInputValue("max", -b));c.rendered=!0},destroy:function(){var a=this.minInput,b=this.maxInput,c=this.chart,d=this.blurInputs,e;X(c.container,"mousedown",d);X(c,"resize",d);Aa(this.buttons);if(a)a.onfocus=a.onblur=a.onchange=null;if(b)b.onfocus=b.onblur=b.onchange=null;for(e in this)this[e]&&e!=="chart"&&(this[e].destroy?this[e].destroy():this[e].nodeType&&Za(this[e])),this[e]=null}};sa(Sa.prototype,"init",function(a,b,c){F(this,"init",function(){if(this.options.rangeSelector.enabled)this.rangeSelector=new Jb(this)}); -a.call(this,b,c)});Highcharts.RangeSelector=Jb;Sa.prototype.callbacks.push(function(a){function b(){f=a.xAxis[0].getExtremes();g.render(t(f.min,f.dataMin),A(f.max,p(f.dataMax,Number.MAX_VALUE)))}function c(){f=a.xAxis[0].getExtremes();h.render(f.min,f.max)}function d(a){g.render(a.min,a.max)}function e(a){h.render(a.min,a.max)}var f,g=a.scroller,h=a.rangeSelector;g&&(F(a.xAxis[0],"afterSetExtremes",d),sa(a,"drawChartBox",function(a){var c=this.isDirtyBox;a.call(this);c&&b()}),b());h&&(F(a.xAxis[0], -"afterSetExtremes",e),F(a,"resize",c),c());F(a,"destroy",function(){g&&X(a.xAxis[0],"afterSetExtremes",d);h&&(X(a,"resize",c),X(a.xAxis[0],"afterSetExtremes",e))})});Highcharts.StockChart=function(a,b){var c=a.series,d,e={marker:{enabled:!1,states:{hover:{radius:5}}},states:{hover:{lineWidth:2}}},f={shadow:!1,borderWidth:0};a.xAxis=Fa(ja(a.xAxis||{}),function(a){return z({minPadding:0,maxPadding:0,ordinal:!0,title:{text:null},labels:{overflow:"justify"},showLastLabel:!0},a,{type:"datetime",categories:null})}); -a.yAxis=Fa(ja(a.yAxis||{}),function(a){d=a.opposite;return z({labels:{align:d?"right":"left",x:d?-2:2,y:-2},showLastLabel:!1,title:{text:null}},a)});a.series=null;a=z({chart:{panning:!0,pinchType:"x"},navigator:{enabled:!0},scrollbar:{enabled:!0},rangeSelector:{enabled:!0},title:{text:null},tooltip:{shared:!0,crosshairs:!0},legend:{enabled:!1},plotOptions:{line:e,spline:e,area:e,areaspline:e,arearange:e,areasplinerange:e,column:f,columnrange:f,candlestick:f,ohlc:f}},a,{_stock:!0,chart:{inverted:!1}}); -a.series=c;return new Sa(a,b)};sa(pb.prototype,"init",function(a,b,c){var d=c.chart.pinchType||"";a.call(this,b,c);this.pinchX=this.pinchHor=d.indexOf("x")!==-1;this.pinchY=this.pinchVert=d.indexOf("y")!==-1});var kc=V.init,lc=V.processData,mc=Ha.prototype.tooltipFormatter;V.init=function(){kc.apply(this,arguments);this.setCompare(this.options.compare)};V.setCompare=function(a){this.modifyValue=a==="value"||a==="percent"?function(b,c){var d=this.compareValue,b=a==="value"?b-d:b=100*(b/d)-100;if(c)c.change= -b;return b}:null;if(this.chart.hasRendered)this.isDirty=!0};V.processData=function(){lc.apply(this,arguments);if(this.options.compare)for(var a=0,b=this.processedXData,c=this.processedYData,d=c.length,e=this.xAxis.getExtremes().min;a=e){this.compareValue=c[a];break}};Da.prototype.setCompare=function(a,b){this.isXAxis||(n(this.series,function(b){b.setCompare(a)}),p(b,!0)&&this.chart.redraw())};Ha.prototype.tooltipFormatter=function(a){a=a.replace("{point.change}", -(this.change>0?"+":"")+xa(this.change,this.series.tooltipOptions.changeDecimals||2));return mc.apply(this,[a])};(function(){var a=V.init,b=V.getSegments;V.init=function(){var b,d;a.apply(this,arguments);b=this.chart;(d=this.xAxis)&&d.options.ordinal&&F(this,"updatedData",function(){delete d.ordinalIndex});if(d&&d.options.ordinal&&!d.hasOrdinalExtension){d.hasOrdinalExtension=!0;d.beforeSetTickPositions=function(){var a,b=[],c=!1,e,j=this.getExtremes(),k=j.min,j=j.max,m;if(this.options.ordinal){n(this.series, -function(c,d){if(c.visible!==!1&&c.takeOrdinalPosition!==!1&&(b=b.concat(c.processedXData),a=b.length,b.sort(function(a,b){return a-b}),a))for(d=a-1;d--;)b[d]===b[d+1]&&b.splice(d,1)});a=b.length;if(a>2){e=b[1]-b[0];for(m=a-1;m--&&!c;)b[m+1]-b[m]!==e&&(c=!0)}c?(this.ordinalPositions=b,c=d.val2lin(k,!0),e=d.val2lin(j,!0),this.ordinalSlope=j=(j-k)/(e-c),this.ordinalOffset=k-c*j):this.ordinalPositions=this.ordinalSlope=this.ordinalOffset=w}};d.val2lin=function(a,b){var c=this.ordinalPositions;if(c){var d= -c.length,e,k;for(e=d;e--;)if(c[e]===a){k=e;break}for(e=d-1;e--;)if(a>c[e]||e===0){c=(a-c[e])/(c[e+1]-c[e]);k=e+c;break}return b?k:this.ordinalSlope*(k||0)+this.ordinalOffset}else return a};d.lin2val=function(a,b){var c=this.ordinalPositions;if(c){var d=this.ordinalSlope,e=this.ordinalOffset,k=c.length-1,m,l;if(b)a<0?a=c[0]:a>k?a=c[k]:(k=W(a),l=a-k);else for(;k--;)if(m=d*k+e,a>=m){d=d*(k+1)+e;l=(a-m)/(d-m);break}return l!==w&&c[k]!==w?c[k]+(l?l*(c[k+1]-c[k]):0):a}else return a};d.getExtendedPositions= -function(){var a=d.series[0].currentDataGrouping,e=d.ordinalIndex,h=a?a.count+a.unitName:"raw",i=d.getExtremes(),j,k;if(!e)e=d.ordinalIndex={};if(!e[h])j={series:[],getExtremes:function(){return{min:i.dataMin,max:i.dataMax}},options:{ordinal:!0}},n(d.series,function(d){k={xAxis:j,xData:d.xData,chart:b,destroyGroupedData:qa};k.options={dataGrouping:a?{enabled:!0,forced:!0,approximation:"open",units:[[a.unitName,[a.count]]]}:{enabled:!1}};d.processData.apply(k);j.series.push(k)}),d.beforeSetTickPositions.apply(j), -e[h]=j.ordinalPositions;return e[h]};d.getGroupIntervalFactor=function(a,b,c){for(var d=0,e=c.length,k=[];dc;j[n]k*5||t){if(j[n]>z){for(p=fb(a,j[l],j[n],e);p.length&&p[0]<=z;)p.shift();p.length&&(z=p[p.length-1]);y=y.concat(p)}l=n+1}if(t)break}a=p.info;if(m&&a.unitRange<=I[za]){n=y.length-1;for(l=1;lc?a-1:a;for(B=void 0;m--;)l=n[m],c=B-l,B&&c -1)o&&n(o,function(a){a.setState()}),i<0?(o=r,r=d.ordinalPositions?d:r):o=d.ordinalPositions?d:r,u=r.ordinalPositions,k>u[u.length-1]&&u.push(k),o=p.apply(o,[s.apply(o,[m,!0])+i,!0]),i=p.apply(r,[s.apply(r,[l,!0])+i,!0]),o>A(j.dataMin,m)&&ia.xAxis.closestPointRange*e&&d.splice(g+1,0,b.splice(h+1,b.length-h))})}})();y(Highcharts,{Axis:Da,Chart:Sa,Color:wa,Legend:Hb,Pointer:pb,Point:Ha,Tick:ab,Tooltip:Gb,Renderer:cb,Series:$,SVGElement:Ca,SVGRenderer:Ga,arrayMin:Pa,arrayMax:ua,charts:Ua,dateFormat:ya,format:La,pathAnim:Lb,getOptions:function(){return M},hasBidiBug:cc,isTouchDevice:ub,numberFormat:xa,seriesTypes:Q,setOptions:function(a){M=z(M,a);Tb();return M},addEvent:F,removeEvent:X,createElement:aa,discardElement:Za, -css:L,each:n,extend:y,map:Fa,merge:z,pick:p,splat:ja,extendClass:ea,pInt:C,wrap:sa,svg:ca,canvas:ia,vml:!ca&&!ia,product:"Highstock",version:"1.3.2"})})(); diff --git a/menu/js/jquery-1.6.2.js b/menu/js/jquery-1.6.2.js deleted file mode 100644 index f3201aa..0000000 --- a/menu/js/jquery-1.6.2.js +++ /dev/null @@ -1,8981 +0,0 @@ -/*! - * jQuery JavaScript Library v1.6.2 - * http://jquery.com/ - * - * Copyright 2011, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * - * Date: Thu Jun 30 14:16:56 2011 -0400 - */ -(function( window, undefined ) { - -// Use the correct document accordingly with window argument (sandbox) -var document = window.document, - navigator = window.navigator, - location = window.location; -var jQuery = (function() { - -// Define a local copy of jQuery -var jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // A central reference to the root jQuery(document) - rootjQuery, - - // A simple way to check for HTML strings or ID strings - // (both of which we optimize for) - quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, - - // Check if a string has a non-whitespace character in it - rnotwhite = /\S/, - - // Used for trimming whitespace - trimLeft = /^\s+/, - trimRight = /\s+$/, - - // Check for digits - rdigit = /\d/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, - rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - - // Useragent RegExp - rwebkit = /(webkit)[ \/]([\w.]+)/, - ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, - rmsie = /(msie) ([\w.]+)/, - rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, - - // Matches dashed string for camelizing - rdashAlpha = /-([a-z])/ig, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }, - - // Keep a UserAgent string for use with jQuery.browser - userAgent = navigator.userAgent, - - // For matching the engine and version of the browser - browserMatch, - - // The deferred used on DOM ready - readyList, - - // The ready event handler - DOMContentLoaded, - - // Save a reference to some core methods - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - push = Array.prototype.push, - slice = Array.prototype.slice, - trim = String.prototype.trim, - indexOf = Array.prototype.indexOf, - - // [[Class]] -> type pairs - class2type = {}; - -jQuery.fn = jQuery.prototype = { - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem, ret, doc; - - // Handle $(""), $(null), or $(undefined) - if ( !selector ) { - return this; - } - - // Handle $(DOMElement) - if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - } - - // The body element only exists once, optimize finding it - if ( selector === "body" && !context && document.body ) { - this.context = document; - this[0] = document.body; - this.selector = selector; - this.length = 1; - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - // Are we dealing with HTML string or an ID? - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = quickExpr.exec( selector ); - } - - // Verify a match, and that no context was specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - doc = (context ? context.ownerDocument || context : document); - - // If a single string is passed in and it's a single tag - // just do a createElement and skip the rest - ret = rsingleTag.exec( selector ); - - if ( ret ) { - if ( jQuery.isPlainObject( context ) ) { - selector = [ document.createElement( ret[1] ) ]; - jQuery.fn.attr.call( selector, context, true ); - - } else { - selector = [ doc.createElement( ret[1] ) ]; - } - - } else { - ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); - selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; - } - - return jQuery.merge( this, selector ); - - // HANDLE: $("#id") - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return (context || rootjQuery).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if (selector.selector !== undefined) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The current version of jQuery being used - jquery: "1.6.2", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return slice.call( this, 0 ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems, name, selector ) { - // Build a new jQuery matched element set - var ret = this.constructor(); - - if ( jQuery.isArray( elems ) ) { - push.apply( ret, elems ); - - } else { - jQuery.merge( ret, elems ); - } - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - - ret.context = this.context; - - if ( name === "find" ) { - ret.selector = this.selector + (this.selector ? " " : "") + selector; - } else if ( name ) { - ret.selector = this.selector + "." + name + "(" + selector + ")"; - } - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Attach the listeners - jQuery.bindReady(); - - // Add the callback - readyList.done( fn ); - - return this; - }, - - eq: function( i ) { - return i === -1 ? - this.slice( i ) : - this.slice( i, +i + 1 ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - slice: function() { - return this.pushStack( slice.apply( this, arguments ), - "slice", slice.call(arguments).join(",") ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var options, name, src, copy, copyIsArray, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - // Either a released hold or an DOMready/load event and not yet ready - if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready, 1 ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger( "ready" ).unbind( "ready" ); - } - } - }, - - bindReady: function() { - if ( readyList ) { - return; - } - - readyList = jQuery._Deferred(); - - // Catch cases where $(document).ready() is called after the - // browser event has already occurred. - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - return setTimeout( jQuery.ready, 1 ); - } - - // Mozilla, Opera and webkit nightlies currently support this event - if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", jQuery.ready, false ); - - // If IE event model is used - } else if ( document.attachEvent ) { - // ensure firing before onload, - // maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", DOMContentLoaded ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", jQuery.ready ); - - // If IE and not a frame - // continually check to see if the document is ready - var toplevel = false; - - try { - toplevel = window.frameElement == null; - } catch(e) {} - - if ( document.documentElement.doScroll && toplevel ) { - doScrollCheck(); - } - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - // A crude way of determining if an object is a window - isWindow: function( obj ) { - return obj && typeof obj === "object" && "setInterval" in obj; - }, - - isNaN: function( obj ) { - return obj == null || !rdigit.test( obj ) || isNaN( obj ); - }, - - type: function( obj ) { - return obj == null ? - String( obj ) : - class2type[ toString.call(obj) ] || "object"; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - // Not own constructor property must be Object - if ( obj.constructor && - !hasOwn.call(obj, "constructor") && - !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - for ( var name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw msg; - }, - - parseJSON: function( data ) { - if ( typeof data !== "string" || !data ) { - return null; - } - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return (new Function( "return " + data ))(); - - } - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - // (xml & tmp used internally) - parseXML: function( data , xml , tmp ) { - - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - - tmp = xml.documentElement; - - if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) { - jQuery.error( "Invalid XML: " + data ); - } - - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && rnotwhite.test( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Converts a dashed string to camelCased string; - // Used by both the css and data modules - camelCase: function( string ) { - return string.replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); - }, - - // args is for internal usage only - each: function( object, callback, args ) { - var name, i = 0, - length = object.length, - isObj = length === undefined || jQuery.isFunction( object ); - - if ( args ) { - if ( isObj ) { - for ( name in object ) { - if ( callback.apply( object[ name ], args ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.apply( object[ i++ ], args ) === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isObj ) { - for ( name in object ) { - if ( callback.call( object[ name ], name, object[ name ] ) === false ) { - break; - } - } - } else { - for ( ; i < length; ) { - if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { - break; - } - } - } - } - - return object; - }, - - // Use native String.trim function wherever possible - trim: trim ? - function( text ) { - return text == null ? - "" : - trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); - }, - - // results is for internal usage only - makeArray: function( array, results ) { - var ret = results || []; - - if ( array != null ) { - // The window, strings (and functions) also have 'length' - // The extra typeof function check is to prevent crashes - // in Safari 2 (See: #3039) - // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 - var type = jQuery.type( array ); - - if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { - push.call( ret, array ); - } else { - jQuery.merge( ret, array ); - } - } - - return ret; - }, - - inArray: function( elem, array ) { - - if ( indexOf ) { - return indexOf.call( array, elem ); - } - - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } - } - - return -1; - }, - - merge: function( first, second ) { - var i = first.length, - j = 0; - - if ( typeof second.length === "number" ) { - for ( var l = second.length; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var ret = [], retVal; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( var i = 0, length = elems.length; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, key, ret = [], - i = 0, - length = elems.length, - // jquery objects are treated as arrays - isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( key in elems ) { - value = callback( elems[ key ], key, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return ret.concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - if ( typeof context === "string" ) { - var tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - var args = slice.call( arguments, 2 ), - proxy = function() { - return fn.apply( context, args.concat( slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; - - return proxy; - }, - - // Mutifunctional method to get and set values to a collection - // The value/s can optionally be executed if it's a function - access: function( elems, key, value, exec, fn, pass ) { - var length = elems.length; - - // Setting many attributes - if ( typeof key === "object" ) { - for ( var k in key ) { - jQuery.access( elems, k, key[k], exec, fn, value ); - } - return elems; - } - - // Setting one attribute - if ( value !== undefined ) { - // Optionally, function values get executed if exec is true - exec = !pass && exec && jQuery.isFunction(value); - - for ( var i = 0; i < length; i++ ) { - fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); - } - - return elems; - } - - // Getting an attribute - return length ? fn( elems[0], key ) : undefined; - }, - - now: function() { - return (new Date()).getTime(); - }, - - // Use of jQuery.browser is frowned upon. - // More details: http://docs.jquery.com/Utilities/jQuery.browser - uaMatch: function( ua ) { - ua = ua.toLowerCase(); - - var match = rwebkit.exec( ua ) || - ropera.exec( ua ) || - rmsie.exec( ua ) || - ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || - []; - - return { browser: match[1] || "", version: match[2] || "0" }; - }, - - sub: function() { - function jQuerySub( selector, context ) { - return new jQuerySub.fn.init( selector, context ); - } - jQuery.extend( true, jQuerySub, this ); - jQuerySub.superclass = this; - jQuerySub.fn = jQuerySub.prototype = this(); - jQuerySub.fn.constructor = jQuerySub; - jQuerySub.sub = this.sub; - jQuerySub.fn.init = function init( selector, context ) { - if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { - context = jQuerySub( context ); - } - - return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); - }; - jQuerySub.fn.init.prototype = jQuerySub.fn; - var rootjQuerySub = jQuerySub(document); - return jQuerySub; - }, - - browser: {} -}); - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -browserMatch = jQuery.uaMatch( userAgent ); -if ( browserMatch.browser ) { - jQuery.browser[ browserMatch.browser ] = true; - jQuery.browser.version = browserMatch.version; -} - -// Deprecated, use jQuery.browser.webkit instead -if ( jQuery.browser.webkit ) { - jQuery.browser.safari = true; -} - -// IE doesn't match non-breaking spaces with \s -if ( rnotwhite.test( "\xA0" ) ) { - trimLeft = /^[\s\xA0]+/; - trimRight = /[\s\xA0]+$/; -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); - -// Cleanup functions for the document ready method -if ( document.addEventListener ) { - DOMContentLoaded = function() { - document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); - jQuery.ready(); - }; - -} else if ( document.attachEvent ) { - DOMContentLoaded = function() { - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( document.readyState === "complete" ) { - document.detachEvent( "onreadystatechange", DOMContentLoaded ); - jQuery.ready(); - } - }; -} - -// The DOM ready check for Internet Explorer -function doScrollCheck() { - if ( jQuery.isReady ) { - return; - } - - try { - // If IE is used, use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - document.documentElement.doScroll("left"); - } catch(e) { - setTimeout( doScrollCheck, 1 ); - return; - } - - // and execute any waiting functions - jQuery.ready(); -} - -return jQuery; - -})(); - - -var // Promise methods - promiseMethods = "done fail isResolved isRejected promise then always pipe".split( " " ), - // Static reference to slice - sliceDeferred = [].slice; - -jQuery.extend({ - // Create a simple deferred (one callbacks list) - _Deferred: function() { - var // callbacks list - callbacks = [], - // stored [ context , args ] - fired, - // to avoid firing when already doing so - firing, - // flag to know if the deferred has been cancelled - cancelled, - // the deferred itself - deferred = { - - // done( f1, f2, ...) - done: function() { - if ( !cancelled ) { - var args = arguments, - i, - length, - elem, - type, - _fired; - if ( fired ) { - _fired = fired; - fired = 0; - } - for ( i = 0, length = args.length; i < length; i++ ) { - elem = args[ i ]; - type = jQuery.type( elem ); - if ( type === "array" ) { - deferred.done.apply( deferred, elem ); - } else if ( type === "function" ) { - callbacks.push( elem ); - } - } - if ( _fired ) { - deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); - } - } - return this; - }, - - // resolve with given context and args - resolveWith: function( context, args ) { - if ( !cancelled && !fired && !firing ) { - // make sure args are available (#8421) - args = args || []; - firing = 1; - try { - while( callbacks[ 0 ] ) { - callbacks.shift().apply( context, args ); - } - } - finally { - fired = [ context, args ]; - firing = 0; - } - } - return this; - }, - - // resolve with this as context and given arguments - resolve: function() { - deferred.resolveWith( this, arguments ); - return this; - }, - - // Has this deferred been resolved? - isResolved: function() { - return !!( firing || fired ); - }, - - // Cancel - cancel: function() { - cancelled = 1; - callbacks = []; - return this; - } - }; - - return deferred; - }, - - // Full fledged deferred (two callbacks list) - Deferred: function( func ) { - var deferred = jQuery._Deferred(), - failDeferred = jQuery._Deferred(), - promise; - // Add errorDeferred methods, then and promise - jQuery.extend( deferred, { - then: function( doneCallbacks, failCallbacks ) { - deferred.done( doneCallbacks ).fail( failCallbacks ); - return this; - }, - always: function() { - return deferred.done.apply( deferred, arguments ).fail.apply( this, arguments ); - }, - fail: failDeferred.done, - rejectWith: failDeferred.resolveWith, - reject: failDeferred.resolve, - isRejected: failDeferred.isResolved, - pipe: function( fnDone, fnFail ) { - return jQuery.Deferred(function( newDefer ) { - jQuery.each( { - done: [ fnDone, "resolve" ], - fail: [ fnFail, "reject" ] - }, function( handler, data ) { - var fn = data[ 0 ], - action = data[ 1 ], - returned; - if ( jQuery.isFunction( fn ) ) { - deferred[ handler ](function() { - returned = fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise().then( newDefer.resolve, newDefer.reject ); - } else { - newDefer[ action ]( returned ); - } - }); - } else { - deferred[ handler ]( newDefer[ action ] ); - } - }); - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - if ( obj == null ) { - if ( promise ) { - return promise; - } - promise = obj = {}; - } - var i = promiseMethods.length; - while( i-- ) { - obj[ promiseMethods[i] ] = deferred[ promiseMethods[i] ]; - } - return obj; - } - }); - // Make sure only one callback list will be used - deferred.done( failDeferred.cancel ).fail( deferred.cancel ); - // Unexpose cancel - delete deferred.cancel; - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - return deferred; - }, - - // Deferred helper - when: function( firstParam ) { - var args = arguments, - i = 0, - length = args.length, - count = length, - deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? - firstParam : - jQuery.Deferred(); - function resolveFunc( i ) { - return function( value ) { - args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; - if ( !( --count ) ) { - // Strange bug in FF4: - // Values changed onto the arguments object sometimes end up as undefined values - // outside the $.when method. Cloning the object into a fresh array solves the issue - deferred.resolveWith( deferred, sliceDeferred.call( args, 0 ) ); - } - }; - } - if ( length > 1 ) { - for( ; i < length; i++ ) { - if ( args[ i ] && jQuery.isFunction( args[ i ].promise ) ) { - args[ i ].promise().then( resolveFunc(i), deferred.reject ); - } else { - --count; - } - } - if ( !count ) { - deferred.resolveWith( deferred, args ); - } - } else if ( deferred !== firstParam ) { - deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); - } - return deferred.promise(); - } -}); - - - -jQuery.support = (function() { - - var div = document.createElement( "div" ), - documentElement = document.documentElement, - all, - a, - select, - opt, - input, - marginDiv, - support, - fragment, - body, - testElementParent, - testElement, - testElementStyle, - tds, - events, - eventName, - i, - isSupported; - - // Preliminary tests - div.setAttribute("className", "t"); - div.innerHTML = "
a"; - - all = div.getElementsByTagName( "*" ); - a = div.getElementsByTagName( "a" )[ 0 ]; - - // Can't get basic test support - if ( !all || !all.length || !a ) { - return {}; - } - - // First batch of supports tests - select = document.createElement( "select" ); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName( "input" )[ 0 ]; - - support = { - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: ( div.firstChild.nodeType === 3 ), - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName( "tbody" ).length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName( "link" ).length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: ( a.getAttribute( "href" ) === "/a" ), - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.55$/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Make sure that if no value is specified for a checkbox - // that it defaults to "on". - // (WebKit defaults to "" instead) - checkOn: ( input.value === "on" ), - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // Will be defined later - submitBubbles: true, - changeBubbles: true, - focusinBubbles: false, - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true - }; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Test to see if it's possible to delete an expando from an element - // Fails in Internet Explorer - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { - div.attachEvent( "onclick", function() { - // Cloning a node shouldn't copy over any - // bound event handlers (IE does this) - support.noCloneEvent = false; - }); - div.cloneNode( true ).fireEvent( "onclick" ); - } - - // Check if a radio maintains it's value - // after being appended to the DOM - input = document.createElement("input"); - input.value = "t"; - input.setAttribute("type", "radio"); - support.radioValue = input.value === "t"; - - input.setAttribute("checked", "checked"); - div.appendChild( input ); - fragment = document.createDocumentFragment(); - fragment.appendChild( div.firstChild ); - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - div.innerHTML = ""; - - // Figure out if the W3C box model works as expected - div.style.width = div.style.paddingLeft = "1px"; - - body = document.getElementsByTagName( "body" )[ 0 ]; - // We use our own, invisible, body unless the body is already present - // in which case we use a div (#9239) - testElement = document.createElement( body ? "div" : "body" ); - testElementStyle = { - visibility: "hidden", - width: 0, - height: 0, - border: 0, - margin: 0 - }; - if ( body ) { - jQuery.extend( testElementStyle, { - position: "absolute", - left: -1000, - top: -1000 - }); - } - for ( i in testElementStyle ) { - testElement.style[ i ] = testElementStyle[ i ]; - } - testElement.appendChild( div ); - testElementParent = body || documentElement; - testElementParent.insertBefore( testElement, testElementParent.firstChild ); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - support.boxModel = div.offsetWidth === 2; - - if ( "zoom" in div.style ) { - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - // (IE < 8 does this) - div.style.display = "inline"; - div.style.zoom = 1; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 2 ); - - // Check if elements with layout shrink-wrap their children - // (IE 6 does this) - div.style.display = ""; - div.innerHTML = "
"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 2 ); - } - - div.innerHTML = "
t
"; - tds = div.getElementsByTagName( "td" ); - - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - // (only IE 8 fails this test) - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Check if empty table cells still have offsetWidth/Height - // (IE < 8 fail this test) - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - div.innerHTML = ""; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. For more - // info see bug #3333 - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - if ( document.defaultView && document.defaultView.getComputedStyle ) { - marginDiv = document.createElement( "div" ); - marginDiv.style.width = "0"; - marginDiv.style.marginRight = "0"; - div.appendChild( marginDiv ); - support.reliableMarginRight = - ( parseInt( ( document.defaultView.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; - } - - // Remove the body element we added - testElement.innerHTML = ""; - testElementParent.removeChild( testElement ); - - // Technique from Juriy Zaytsev - // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ - // We only care about the case where non-standard event systems - // are used, namely in IE. Short-circuiting here helps us to - // avoid an eval call (in setAttribute) which can cause CSP - // to go haywire. See: https://developer.mozilla.org/en/Security/CSP - if ( div.attachEvent ) { - for( i in { - submit: 1, - change: 1, - focusin: 1 - } ) { - eventName = "on" + i; - isSupported = ( eventName in div ); - if ( !isSupported ) { - div.setAttribute( eventName, "return;" ); - isSupported = ( typeof div[ eventName ] === "function" ); - } - support[ i + "Bubbles" ] = isSupported; - } - } - - // Null connected elements to avoid leaks in IE - testElement = fragment = select = opt = body = marginDiv = div = input = null; - - return support; -})(); - -// Keep track of boxModel -jQuery.boxModel = jQuery.support.boxModel; - - - - -var rbrace = /^(?:\{.*\}|\[.*\])$/, - rmultiDash = /([a-z])([A-Z])/g; - -jQuery.extend({ - cache: {}, - - // Please use with caution - uuid: 0, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ jQuery.expando ] = id = ++jQuery.uuid; - } else { - id = jQuery.expando; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - - // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery - // metadata on plain JS objects when the object is serialized using - // JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); - } else { - cache[ id ] = jQuery.extend(cache[ id ], name); - } - } - - thisCache = cache[ id ]; - - // Internal jQuery data is stored in a separate object inside the object's data - // cache in order to avoid key collisions between internal data and user-defined - // data - if ( pvt ) { - if ( !thisCache[ internalKey ] ) { - thisCache[ internalKey ] = {}; - } - - thisCache = thisCache[ internalKey ]; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should - // not attempt to inspect the internal events object using jQuery.data, as this - // internal data object is undocumented and subject to change. - if ( name === "events" && !thisCache[name] ) { - return thisCache[ internalKey ] && thisCache[ internalKey ].events; - } - - return getByName ? - // Check for both converted-to-camel and non-converted data property names - thisCache[ jQuery.camelCase( name ) ] || thisCache[ name ] : - thisCache; - }, - - removeData: function( elem, name, pvt /* Internal Use Only */ ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var internalKey = jQuery.expando, isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - - // See jQuery.data for more information - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; - - if ( thisCache ) { - delete thisCache[ name ]; - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !isEmptyDataObject(thisCache) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( pvt ) { - delete cache[ id ][ internalKey ]; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject(cache[ id ]) ) { - return; - } - } - - var internalCache = cache[ id ][ internalKey ]; - - // Browsers that fail expando deletion also refuse to delete expandos on - // the window, but it will allow it on all other JS objects; other browsers - // don't care - if ( jQuery.support.deleteExpando || cache != window ) { - delete cache[ id ]; - } else { - cache[ id ] = null; - } - - // We destroyed the entire user cache at once because it's faster than - // iterating through each key, but we need to continue to persist internal - // data if it existed - if ( internalCache ) { - cache[ id ] = {}; - // TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery - // metadata on plain JS objects when the object is serialized using - // JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - - cache[ id ][ internalKey ] = internalCache; - - // Otherwise, we need to eliminate the expando on the node to avoid - // false lookups in the cache for entries that no longer exist - } else if ( isNode ) { - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( jQuery.support.deleteExpando ) { - delete elem[ jQuery.expando ]; - } else if ( elem.removeAttribute ) { - elem.removeAttribute( jQuery.expando ); - } else { - elem[ jQuery.expando ] = null; - } - } - }, - - // For internal use only. - _data: function( elem, name, data ) { - return jQuery.data( elem, name, data, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - if ( elem.nodeName ) { - var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; - - if ( match ) { - return !(match === true || elem.getAttribute("classid") !== match); - } - } - - return true; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var data = null; - - if ( typeof key === "undefined" ) { - if ( this.length ) { - data = jQuery.data( this[0] ); - - if ( this[0].nodeType === 1 ) { - var attr = this[0].attributes, name; - for ( var i = 0, l = attr.length; i < l; i++ ) { - name = attr[i].name; - - if ( name.indexOf( "data-" ) === 0 ) { - name = jQuery.camelCase( name.substring(5) ); - - dataAttr( this[0], name, data[ name ] ); - } - } - } - } - - return data; - - } else if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - var parts = key.split("."); - parts[1] = parts[1] ? "." + parts[1] : ""; - - if ( value === undefined ) { - data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); - - // Try to fetch any internally stored data first - if ( data === undefined && this.length ) { - data = jQuery.data( this[0], key ); - data = dataAttr( this[0], key, data ); - } - - return data === undefined && parts[1] ? - this.data( parts[0] ) : - data; - - } else { - return this.each(function() { - var $this = jQuery( this ), - args = [ parts[0], value ]; - - $this.triggerHandler( "setData" + parts[1] + "!", args ); - jQuery.data( this, key, value ); - $this.triggerHandler( "changeData" + parts[1] + "!", args ); - }); - } - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - var name = "data-" + key.replace( rmultiDash, "$1-$2" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - !jQuery.isNaN( data ) ? parseFloat( data ) : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// TODO: This is a hack for 1.5 ONLY to allow objects with a single toJSON -// property to be considered empty objects; this property always exists in -// order to make sure JSON.stringify does not expose internal metadata -function isEmptyDataObject( obj ) { - for ( var name in obj ) { - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} - - - - -function handleQueueMarkDefer( elem, type, src ) { - var deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - defer = jQuery.data( elem, deferDataKey, undefined, true ); - if ( defer && - ( src === "queue" || !jQuery.data( elem, queueDataKey, undefined, true ) ) && - ( src === "mark" || !jQuery.data( elem, markDataKey, undefined, true ) ) ) { - // Give room for hard-coded callbacks to fire first - // and eventually mark/queue something else on the element - setTimeout( function() { - if ( !jQuery.data( elem, queueDataKey, undefined, true ) && - !jQuery.data( elem, markDataKey, undefined, true ) ) { - jQuery.removeData( elem, deferDataKey, true ); - defer.resolve(); - } - }, 0 ); - } -} - -jQuery.extend({ - - _mark: function( elem, type ) { - if ( elem ) { - type = (type || "fx") + "mark"; - jQuery.data( elem, type, (jQuery.data(elem,type,undefined,true) || 0) + 1, true ); - } - }, - - _unmark: function( force, elem, type ) { - if ( force !== true ) { - type = elem; - elem = force; - force = false; - } - if ( elem ) { - type = type || "fx"; - var key = type + "mark", - count = force ? 0 : ( (jQuery.data( elem, key, undefined, true) || 1 ) - 1 ); - if ( count ) { - jQuery.data( elem, key, count, true ); - } else { - jQuery.removeData( elem, key, true ); - handleQueueMarkDefer( elem, type, "mark" ); - } - } - }, - - queue: function( elem, type, data ) { - if ( elem ) { - type = (type || "fx") + "queue"; - var q = jQuery.data( elem, type, undefined, true ); - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !q || jQuery.isArray(data) ) { - q = jQuery.data( elem, type, jQuery.makeArray(data), true ); - } else { - q.push( data ); - } - } - return q || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - fn = queue.shift(), - defer; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - } - - if ( fn ) { - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift("inprogress"); - } - - fn.call(elem, function() { - jQuery.dequeue(elem, type); - }); - } - - if ( !queue.length ) { - jQuery.removeData( elem, type + "queue", true ); - handleQueueMarkDefer( elem, type, "queue" ); - } - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - } - - if ( data === undefined ) { - return jQuery.queue( this[0], type ); - } - return this.each(function() { - var queue = jQuery.queue( this, type, data ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; - type = type || "fx"; - - return this.queue( type, function() { - var elem = this; - setTimeout(function() { - jQuery.dequeue( elem, type ); - }, time ); - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, object ) { - if ( typeof type !== "string" ) { - object = type; - type = undefined; - } - type = type || "fx"; - var defer = jQuery.Deferred(), - elements = this, - i = elements.length, - count = 1, - deferDataKey = type + "defer", - queueDataKey = type + "queue", - markDataKey = type + "mark", - tmp; - function resolve() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - } - while( i-- ) { - if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || - ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || - jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && - jQuery.data( elements[ i ], deferDataKey, jQuery._Deferred(), true ) )) { - count++; - tmp.done( resolve ); - } - } - resolve(); - return defer.promise(); - } -}); - - - - -var rclass = /[\n\t\r]/g, - rspace = /\s+/, - rreturn = /\r/g, - rtype = /^(?:button|input)$/i, - rfocusable = /^(?:button|input|object|select|textarea)$/i, - rclickable = /^a(?:rea)?$/i, - rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, - rinvalidChar = /\:|^on/, - formHook, boolHook; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, name, value, true, jQuery.attr ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, name, value, true, jQuery.prop ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classNames, i, l, elem, - setClass, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call(this, j, this.className) ); - }); - } - - if ( value && typeof value === "string" ) { - classNames = value.split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 ) { - if ( !elem.className && classNames.length === 1 ) { - elem.className = value; - - } else { - setClass = " " + elem.className + " "; - - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { - setClass += classNames[ c ] + " "; - } - } - elem.className = jQuery.trim( setClass ); - } - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classNames, i, l, elem, className, c, cl; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call(this, j, this.className) ); - }); - } - - if ( (value && typeof value === "string") || value === undefined ) { - classNames = (value || "").split( rspace ); - - for ( i = 0, l = this.length; i < l; i++ ) { - elem = this[ i ]; - - if ( elem.nodeType === 1 && elem.className ) { - if ( value ) { - className = (" " + elem.className + " ").replace( rclass, " " ); - for ( c = 0, cl = classNames.length; c < cl; c++ ) { - className = className.replace(" " + classNames[ c ] + " ", " "); - } - elem.className = jQuery.trim( className ); - - } else { - elem.className = ""; - } - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.split( rspace ); - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space seperated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - } else if ( type === "undefined" || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // toggle whole className - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " "; - for ( var i = 0, l = this.length; i < l; i++ ) { - if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var hooks, ret, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return undefined; - } - - var isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var self = jQuery(this), val; - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, - index = elem.selectedIndex, - values = [], - options = elem.options, - one = elem.type === "select-one"; - - // Nothing was selected - if ( index < 0 ) { - return null; - } - - // Loop through all the selected options - for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { - var option = options[ i ]; - - // Don't return options that are disabled or in a disabled optgroup - if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - // Fixes Bug #2551 -- select.val() broken in IE after form.reset() - if ( one && !values.length && options.length ) { - return jQuery( options[ index ] ).val(); - } - - return values; - }, - - set: function( elem, value ) { - var values = jQuery.makeArray( value ); - - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attrFn: { - val: true, - css: true, - html: true, - text: true, - data: true, - width: true, - height: true, - offset: true - }, - - attrFix: { - // Always normalize to ensure hook usage - tabindex: "tabIndex" - }, - - attr: function( elem, name, value, pass ) { - var nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return undefined; - } - - if ( pass && name in jQuery.attrFn ) { - return jQuery( elem )[ name ]( value ); - } - - // Fallback to prop when attributes are not supported - if ( !("getAttribute" in elem) ) { - return jQuery.prop( elem, name, value ); - } - - var ret, hooks, - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - // Normalize the name if needed - if ( notxml ) { - name = jQuery.attrFix[ name ] || name; - - hooks = jQuery.attrHooks[ name ]; - - if ( !hooks ) { - // Use boolHook for boolean attributes - if ( rboolean.test( name ) ) { - - hooks = boolHook; - - // Use formHook for forms and if the name contains certain characters - } else if ( formHook && name !== "className" && - (jQuery.nodeName( elem, "form" ) || rinvalidChar.test( name )) ) { - - hooks = formHook; - } - } - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - return undefined; - - } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, "" + value ); - return value; - } - - } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - ret = elem.getAttribute( name ); - - // Non-existent attributes return null, we normalize to undefined - return ret === null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, name ) { - var propName; - if ( elem.nodeType === 1 ) { - name = jQuery.attrFix[ name ] || name; - - if ( jQuery.support.getSetAttribute ) { - // Use removeAttribute in browsers that support it - elem.removeAttribute( name ); - } else { - jQuery.attr( elem, name, "" ); - elem.removeAttributeNode( elem.getAttributeNode( name ) ); - } - - // Set corresponding property to false for boolean attributes - if ( rboolean.test( name ) && (propName = jQuery.propFix[ name ] || name) in elem ) { - elem[ propName ] = false; - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - // We can't allow the type property to be changed (since it causes problems in IE) - if ( rtype.test( elem.nodeName ) && elem.parentNode ) { - jQuery.error( "type property can't be changed" ); - } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to it's default in case type is set after value - // This is for element creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - }, - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabIndex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - }, - // Use the value property for back compat - // Use the formHook for button elements in IE6/7 (#1954) - value: { - get: function( elem, name ) { - if ( formHook && jQuery.nodeName( elem, "button" ) ) { - return formHook.get( elem, name ); - } - return name in elem ? - elem.value : - null; - }, - set: function( elem, value, name ) { - if ( formHook && jQuery.nodeName( elem, "button" ) ) { - return formHook.set( elem, value, name ); - } - // Does not return so that setAttribute is also used - elem.value = value; - } - } - }, - - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, - - prop: function( elem, name, value ) { - var nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return undefined; - } - - var ret, hooks, - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - return (elem[ name ] = value); - } - - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== undefined ) { - return ret; - - } else { - return elem[ name ]; - } - } - }, - - propHooks: {} -}); - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - // Align boolean attributes with corresponding properties - return jQuery.prop( elem, name ) ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - var propName; - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else { - // value is true since we know at this point it's type boolean and not false - // Set boolean attributes to the same name and set the DOM property - propName = jQuery.propFix[ name ] || name; - if ( propName in elem ) { - // Only set the IDL specifically if it already exists on the element - elem[ propName ] = true; - } - - elem.setAttribute( name, name.toLowerCase() ); - } - return name; - } -}; - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !jQuery.support.getSetAttribute ) { - - // propFix is more comprehensive and contains all fixes - jQuery.attrFix = jQuery.propFix; - - // Use this for any attribute on a form in IE6/7 - formHook = jQuery.attrHooks.name = jQuery.attrHooks.title = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret; - ret = elem.getAttributeNode( name ); - // Return undefined if nodeValue is empty string - return ret && ret.nodeValue !== "" ? - ret.nodeValue : - undefined; - }, - set: function( elem, value, name ) { - // Check form objects in IE (multiple bugs related) - // Only use nodeValue if the attribute node exists on the form - var ret = elem.getAttributeNode( name ); - if ( ret ) { - ret.nodeValue = value; - return value; - } - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }); - }); -} - - -// Some attributes require a special call on IE -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret === null ? undefined : ret; - } - }); - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Normalize to lowercase since IE uppercases css property names - return elem.style.cssText.toLowerCase() || undefined; - }, - set: function( elem, value ) { - return (elem.style.cssText = "" + value); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - } - }); -} - -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return (elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0); - } - } - }); -}); - - - - -var rnamespaces = /\.(.*)$/, - rformElems = /^(?:textarea|input|select)$/i, - rperiod = /\./g, - rspaces = / /g, - rescape = /[^\w\s.|`]/g, - fcleanup = function( nm ) { - return nm.replace(rescape, "\\$&"); - }; - -/* - * A number of helper functions used for managing events. - * Many of the ideas behind this code originated from - * Dean Edwards' addEvent library. - */ -jQuery.event = { - - // Bind an event to an element - // Original by Dean Edwards - add: function( elem, types, handler, data ) { - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - if ( handler === false ) { - handler = returnFalse; - } else if ( !handler ) { - // Fixes bug #7229. Fix recommended by jdalton - return; - } - - var handleObjIn, handleObj; - - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - } - - // Make sure that the function being executed has a unique ID - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure - var elemData = jQuery._data( elem ); - - // If no elemData is found then we must be trying to bind to one of the - // banned noData elements - if ( !elemData ) { - return; - } - - var events = elemData.events, - eventHandle = elemData.handle; - - if ( !events ) { - elemData.events = events = {}; - } - - if ( !eventHandle ) { - elemData.handle = eventHandle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.handle.apply( eventHandle.elem, arguments ) : - undefined; - }; - } - - // Add elem as a property of the handle function - // This is to prevent a memory leak with non-native events in IE. - eventHandle.elem = elem; - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = types.split(" "); - - var type, i = 0, namespaces; - - while ( (type = types[ i++ ]) ) { - handleObj = handleObjIn ? - jQuery.extend({}, handleObjIn) : - { handler: handler, data: data }; - - // Namespaced event handlers - if ( type.indexOf(".") > -1 ) { - namespaces = type.split("."); - type = namespaces.shift(); - handleObj.namespace = namespaces.slice(0).sort().join("."); - - } else { - namespaces = []; - handleObj.namespace = ""; - } - - handleObj.type = type; - if ( !handleObj.guid ) { - handleObj.guid = handler.guid; - } - - // Get the current list of functions bound to this event - var handlers = events[ type ], - special = jQuery.event.special[ type ] || {}; - - // Init the event handler queue - if ( !handlers ) { - handlers = events[ type ] = []; - - // Check for a special event handler - // Only use addEventListener/attachEvent if the special - // events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add the function to the element's handler list - handlers.push( handleObj ); - - // Keep track of which events have been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - global: {}, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, pos ) { - // don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - if ( handler === false ) { - handler = returnFalse; - } - - var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ), - events = elemData && elemData.events; - - if ( !elemData || !events ) { - return; - } - - // types is actually an event object here - if ( types && types.type ) { - handler = types.handler; - types = types.type; - } - - // Unbind all events for the element - if ( !types || typeof types === "string" && types.charAt(0) === "." ) { - types = types || ""; - - for ( type in events ) { - jQuery.event.remove( elem, type + types ); - } - - return; - } - - // Handle multiple events separated by a space - // jQuery(...).unbind("mouseover mouseout", fn); - types = types.split(" "); - - while ( (type = types[ i++ ]) ) { - origType = type; - handleObj = null; - all = type.indexOf(".") < 0; - namespaces = []; - - if ( !all ) { - // Namespaced event handlers - namespaces = type.split("."); - type = namespaces.shift(); - - namespace = new RegExp("(^|\\.)" + - jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - eventType = events[ type ]; - - if ( !eventType ) { - continue; - } - - if ( !handler ) { - for ( j = 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( all || namespace.test( handleObj.namespace ) ) { - jQuery.event.remove( elem, origType, handleObj.handler, j ); - eventType.splice( j--, 1 ); - } - } - - continue; - } - - special = jQuery.event.special[ type ] || {}; - - for ( j = pos || 0; j < eventType.length; j++ ) { - handleObj = eventType[ j ]; - - if ( handler.guid === handleObj.guid ) { - // remove the given handler for the given type - if ( all || namespace.test( handleObj.namespace ) ) { - if ( pos == null ) { - eventType.splice( j--, 1 ); - } - - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - - if ( pos != null ) { - break; - } - } - } - - // remove generic event handler if no more handlers exist - if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { - if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - ret = null; - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - var handle = elemData.handle; - if ( handle ) { - handle.elem = null; - } - - delete elemData.events; - delete elemData.handle; - - if ( jQuery.isEmptyObject( elemData ) ) { - jQuery.removeData( elem, undefined, true ); - } - } - }, - - // Events that are safe to short-circuit if no handlers are attached. - // Native DOM events should not be added, they may have inline handlers. - customEvent: { - "getData": true, - "setData": true, - "changeData": true - }, - - trigger: function( event, data, elem, onlyHandlers ) { - // Event object or event type - var type = event.type || event, - namespaces = [], - exclusive; - - if ( type.indexOf("!") >= 0 ) { - // Exclusive events trigger only for the exact event (no namespaces) - type = type.slice(0, -1); - exclusive = true; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - - if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { - // No jQuery handlers for this event type, and it can't have inline handlers - return; - } - - // Caller can pass in an Event, Object, or just an event type string - event = typeof event === "object" ? - // jQuery.Event object - event[ jQuery.expando ] ? event : - // Object literal - new jQuery.Event( type, event ) : - // Just the event type (string) - new jQuery.Event( type ); - - event.type = type; - event.exclusive = exclusive; - event.namespace = namespaces.join("."); - event.namespace_re = new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)"); - - // triggerHandler() and global events don't bubble or run the default action - if ( onlyHandlers || !elem ) { - event.preventDefault(); - event.stopPropagation(); - } - - // Handle a global trigger - if ( !elem ) { - // TODO: Stop taunting the data cache; remove global events and always attach to document - jQuery.each( jQuery.cache, function() { - // internalKey variable is just used to make it easier to find - // and potentially change this stuff later; currently it just - // points to jQuery.expando - var internalKey = jQuery.expando, - internalCache = this[ internalKey ]; - if ( internalCache && internalCache.events && internalCache.events[ type ] ) { - jQuery.event.trigger( event, data, internalCache.handle.elem ); - } - }); - return; - } - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // Clean up the event in case it is being reused - event.result = undefined; - event.target = elem; - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data != null ? jQuery.makeArray( data ) : []; - data.unshift( event ); - - var cur = elem, - // IE doesn't like method names with a colon (#3533, #8272) - ontype = type.indexOf(":") < 0 ? "on" + type : ""; - - // Fire event on the current element, then bubble up the DOM tree - do { - var handle = jQuery._data( cur, "handle" ); - - event.currentTarget = cur; - if ( handle ) { - handle.apply( cur, data ); - } - - // Trigger an inline bound script - if ( ontype && jQuery.acceptData( cur ) && cur[ ontype ] && cur[ ontype ].apply( cur, data ) === false ) { - event.result = false; - event.preventDefault(); - } - - // Bubble up to document, then to window - cur = cur.parentNode || cur.ownerDocument || cur === event.target.ownerDocument && window; - } while ( cur && !event.isPropagationStopped() ); - - // If nobody prevented the default action, do it now - if ( !event.isDefaultPrevented() ) { - var old, - special = jQuery.event.special[ type ] || {}; - - if ( (!special._default || special._default.call( elem.ownerDocument, event ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction)() check here because IE6/7 fails that test. - // IE<9 dies on focus to hidden element (#1486), may want to revisit a try/catch. - try { - if ( ontype && elem[ type ] ) { - // Don't re-trigger an onFOO event when we call its FOO() method - old = elem[ ontype ]; - - if ( old ) { - elem[ ontype ] = null; - } - - jQuery.event.triggered = type; - elem[ type ](); - } - } catch ( ieError ) {} - - if ( old ) { - elem[ ontype ] = old; - } - - jQuery.event.triggered = undefined; - } - } - - return event.result; - }, - - handle: function( event ) { - event = jQuery.event.fix( event || window.event ); - // Snapshot the handlers list since a called handler may add/remove events. - var handlers = ((jQuery._data( this, "events" ) || {})[ event.type ] || []).slice(0), - run_all = !event.exclusive && !event.namespace, - args = Array.prototype.slice.call( arguments, 0 ); - - // Use the fix-ed Event rather than the (read-only) native event - args[0] = event; - event.currentTarget = this; - - for ( var j = 0, l = handlers.length; j < l; j++ ) { - var handleObj = handlers[ j ]; - - // Triggered event must 1) be non-exclusive and have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event. - if ( run_all || event.namespace_re.test( handleObj.namespace ) ) { - // Pass in a reference to the handler function itself - // So that we can later remove it - event.handler = handleObj.handler; - event.data = handleObj.data; - event.handleObj = handleObj; - - var ret = handleObj.handler.apply( this, args ); - - if ( ret !== undefined ) { - event.result = ret; - if ( ret === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - return event.result; - }, - - props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // store a copy of the original event object - // and "clone" to set read-only properties - var originalEvent = event; - event = jQuery.Event( originalEvent ); - - for ( var i = this.props.length, prop; i; ) { - prop = this.props[ --i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Fix target property, if necessary - if ( !event.target ) { - // Fixes #1925 where srcElement might not be defined either - event.target = event.srcElement || document; - } - - // check if target is a textnode (safari) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && event.fromElement ) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && event.clientX != null ) { - var eventDocument = event.target.ownerDocument || document, - doc = eventDocument.documentElement, - body = eventDocument.body; - - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add which for key events - if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { - event.which = event.charCode != null ? event.charCode : event.keyCode; - } - - // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) - if ( !event.metaKey && event.ctrlKey ) { - event.metaKey = event.ctrlKey; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && event.button !== undefined ) { - event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); - } - - return event; - }, - - // Deprecated, use jQuery.guid instead - guid: 1E8, - - // Deprecated, use jQuery.proxy instead - proxy: jQuery.proxy, - - special: { - ready: { - // Make sure the ready event is setup - setup: jQuery.bindReady, - teardown: jQuery.noop - }, - - live: { - add: function( handleObj ) { - jQuery.event.add( this, - liveConvert( handleObj.origType, handleObj.selector ), - jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); - }, - - remove: function( handleObj ) { - jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); - } - }, - - beforeunload: { - setup: function( data, namespaces, eventHandle ) { - // We only want to do this special case on windows - if ( jQuery.isWindow( this ) ) { - this.onbeforeunload = eventHandle; - } - }, - - teardown: function( namespaces, eventHandle ) { - if ( this.onbeforeunload === eventHandle ) { - this.onbeforeunload = null; - } - } - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - if ( elem.detachEvent ) { - elem.detachEvent( "on" + type, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !this.preventDefault ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // timeStamp is buggy for some events on Firefox(#3843) - // So we won't rely on the native value - this.timeStamp = jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -function returnFalse() { - return false; -} -function returnTrue() { - return true; -} - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - preventDefault: function() { - this.isDefaultPrevented = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - - // if preventDefault exists run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // otherwise set the returnValue property of the original event to false (IE) - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - this.isPropagationStopped = returnTrue; - - var e = this.originalEvent; - if ( !e ) { - return; - } - // if stopPropagation exists run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - // otherwise set the cancelBubble property of the original event to true (IE) - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - }, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse -}; - -// Checks if an event happened on an element within another element -// Used in jQuery.event.special.mouseenter and mouseleave handlers -var withinElement = function( event ) { - - // Check if mouse(over|out) are still within the same parent element - var related = event.relatedTarget, - inside = false, - eventType = event.type; - - event.type = event.data; - - if ( related !== this ) { - - if ( related ) { - inside = jQuery.contains( this, related ); - } - - if ( !inside ) { - - jQuery.event.handle.apply( this, arguments ); - - event.type = eventType; - } - } -}, - -// In case of event delegation, we only need to rename the event.type, -// liveHandler will take care of the rest. -delegate = function( event ) { - event.type = event.data; - jQuery.event.handle.apply( this, arguments ); -}; - -// Create mouseenter and mouseleave events -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - setup: function( data ) { - jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); - }, - teardown: function( data ) { - jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); - } - }; -}); - -// submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function( data, namespaces ) { - if ( !jQuery.nodeName( this, "form" ) ) { - jQuery.event.add(this, "click.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { - trigger( "submit", this, arguments ); - } - }); - - jQuery.event.add(this, "keypress.specialSubmit", function( e ) { - var elem = e.target, - type = elem.type; - - if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { - trigger( "submit", this, arguments ); - } - }); - - } else { - return false; - } - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialSubmit" ); - } - }; - -} - -// change delegation, happens here so we have bind. -if ( !jQuery.support.changeBubbles ) { - - var changeFilters, - - getVal = function( elem ) { - var type = elem.type, val = elem.value; - - if ( type === "radio" || type === "checkbox" ) { - val = elem.checked; - - } else if ( type === "select-multiple" ) { - val = elem.selectedIndex > -1 ? - jQuery.map( elem.options, function( elem ) { - return elem.selected; - }).join("-") : - ""; - - } else if ( jQuery.nodeName( elem, "select" ) ) { - val = elem.selectedIndex; - } - - return val; - }, - - testChange = function testChange( e ) { - var elem = e.target, data, val; - - if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { - return; - } - - data = jQuery._data( elem, "_change_data" ); - val = getVal(elem); - - // the current data will be also retrieved by beforeactivate - if ( e.type !== "focusout" || elem.type !== "radio" ) { - jQuery._data( elem, "_change_data", val ); - } - - if ( data === undefined || val === data ) { - return; - } - - if ( data != null || val ) { - e.type = "change"; - e.liveFired = undefined; - jQuery.event.trigger( e, arguments[1], elem ); - } - }; - - jQuery.event.special.change = { - filters: { - focusout: testChange, - - beforedeactivate: testChange, - - click: function( e ) { - var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; - - if ( type === "radio" || type === "checkbox" || jQuery.nodeName( elem, "select" ) ) { - testChange.call( this, e ); - } - }, - - // Change has to be called before submit - // Keydown will be called before keypress, which is used in submit-event delegation - keydown: function( e ) { - var elem = e.target, type = jQuery.nodeName( elem, "input" ) ? elem.type : ""; - - if ( (e.keyCode === 13 && !jQuery.nodeName( elem, "textarea" ) ) || - (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || - type === "select-multiple" ) { - testChange.call( this, e ); - } - }, - - // Beforeactivate happens also before the previous element is blurred - // with this event you can't trigger a change event, but you can store - // information - beforeactivate: function( e ) { - var elem = e.target; - jQuery._data( elem, "_change_data", getVal(elem) ); - } - }, - - setup: function( data, namespaces ) { - if ( this.type === "file" ) { - return false; - } - - for ( var type in changeFilters ) { - jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); - } - - return rformElems.test( this.nodeName ); - }, - - teardown: function( namespaces ) { - jQuery.event.remove( this, ".specialChange" ); - - return rformElems.test( this.nodeName ); - } - }; - - changeFilters = jQuery.event.special.change.filters; - - // Handle when the input is .focus()'d - changeFilters.focus = changeFilters.beforeactivate; -} - -function trigger( type, elem, args ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - // Don't pass args or remember liveFired; they apply to the donor event. - var event = jQuery.extend( {}, args[ 0 ] ); - event.type = type; - event.originalEvent = {}; - event.liveFired = undefined; - jQuery.event.handle.call( elem, event ); - if ( event.isDefaultPrevented() ) { - args[ 0 ].preventDefault(); - } -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - - function handler( donor ) { - // Donor event is always a native one; fix it and switch its type. - // Let focusin/out handler cancel the donor focus/blur event. - var e = jQuery.event.fix( donor ); - e.type = fix; - e.originalEvent = {}; - jQuery.event.trigger( e, null, e.target ); - if ( e.isDefaultPrevented() ) { - donor.preventDefault(); - } - } - }); -} - -jQuery.each(["bind", "one"], function( i, name ) { - jQuery.fn[ name ] = function( type, data, fn ) { - var handler; - - // Handle object literals - if ( typeof type === "object" ) { - for ( var key in type ) { - this[ name ](key, data, type[key], fn); - } - return this; - } - - if ( arguments.length === 2 || data === false ) { - fn = data; - data = undefined; - } - - if ( name === "one" ) { - handler = function( event ) { - jQuery( this ).unbind( event, handler ); - return fn.apply( this, arguments ); - }; - handler.guid = fn.guid || jQuery.guid++; - } else { - handler = fn; - } - - if ( type === "unload" && name !== "one" ) { - this.one( type, data, fn ); - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.add( this[i], type, handler, data ); - } - } - - return this; - }; -}); - -jQuery.fn.extend({ - unbind: function( type, fn ) { - // Handle object literals - if ( typeof type === "object" && !type.preventDefault ) { - for ( var key in type ) { - this.unbind(key, type[key]); - } - - } else { - for ( var i = 0, l = this.length; i < l; i++ ) { - jQuery.event.remove( this[i], type, fn ); - } - } - - return this; - }, - - delegate: function( selector, types, data, fn ) { - return this.live( types, data, fn, selector ); - }, - - undelegate: function( selector, types, fn ) { - if ( arguments.length === 0 ) { - return this.unbind( "live" ); - - } else { - return this.die( types, null, fn, selector ); - } - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - - triggerHandler: function( type, data ) { - if ( this[0] ) { - return jQuery.event.trigger( type, data, this[0], true ); - } - }, - - toggle: function( fn ) { - // Save reference to arguments for access in closure - var args = arguments, - guid = fn.guid || jQuery.guid++, - i = 0, - toggler = function( event ) { - // Figure out which function to execute - var lastToggle = ( jQuery.data( this, "lastToggle" + fn.guid ) || 0 ) % i; - jQuery.data( this, "lastToggle" + fn.guid, lastToggle + 1 ); - - // Make sure that clicks stop - event.preventDefault(); - - // and execute the function - return args[ lastToggle ].apply( this, arguments ) || false; - }; - - // link all the functions, so any of them can unbind this click handler - toggler.guid = guid; - while ( i < args.length ) { - args[ i++ ].guid = guid; - } - - return this.click( toggler ); - }, - - hover: function( fnOver, fnOut ) { - return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); - } -}); - -var liveMap = { - focus: "focusin", - blur: "focusout", - mouseenter: "mouseover", - mouseleave: "mouseout" -}; - -jQuery.each(["live", "die"], function( i, name ) { - jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { - var type, i = 0, match, namespaces, preType, - selector = origSelector || this.selector, - context = origSelector ? this : jQuery( this.context ); - - if ( typeof types === "object" && !types.preventDefault ) { - for ( var key in types ) { - context[ name ]( key, data, types[key], selector ); - } - - return this; - } - - if ( name === "die" && !types && - origSelector && origSelector.charAt(0) === "." ) { - - context.unbind( origSelector ); - - return this; - } - - if ( data === false || jQuery.isFunction( data ) ) { - fn = data || returnFalse; - data = undefined; - } - - types = (types || "").split(" "); - - while ( (type = types[ i++ ]) != null ) { - match = rnamespaces.exec( type ); - namespaces = ""; - - if ( match ) { - namespaces = match[0]; - type = type.replace( rnamespaces, "" ); - } - - if ( type === "hover" ) { - types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); - continue; - } - - preType = type; - - if ( liveMap[ type ] ) { - types.push( liveMap[ type ] + namespaces ); - type = type + namespaces; - - } else { - type = (liveMap[ type ] || type) + namespaces; - } - - if ( name === "live" ) { - // bind live handler - for ( var j = 0, l = context.length; j < l; j++ ) { - jQuery.event.add( context[j], "live." + liveConvert( type, selector ), - { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); - } - - } else { - // unbind live handler - context.unbind( "live." + liveConvert( type, selector ), fn ); - } - } - - return this; - }; -}); - -function liveHandler( event ) { - var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, - elems = [], - selectors = [], - events = jQuery._data( this, "events" ); - - // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) - if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { - return; - } - - if ( event.namespace ) { - namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); - } - - event.liveFired = this; - - var live = events.live.slice(0); - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { - selectors.push( handleObj.selector ); - - } else { - live.splice( j--, 1 ); - } - } - - match = jQuery( event.target ).closest( selectors, event.currentTarget ); - - for ( i = 0, l = match.length; i < l; i++ ) { - close = match[i]; - - for ( j = 0; j < live.length; j++ ) { - handleObj = live[j]; - - if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) && !close.elem.disabled ) { - elem = close.elem; - related = null; - - // Those two events require additional checking - if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { - event.type = handleObj.preType; - related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; - - // Make sure not to accidentally match a child element with the same selector - if ( related && jQuery.contains( elem, related ) ) { - related = elem; - } - } - - if ( !related || related !== elem ) { - elems.push({ elem: elem, handleObj: handleObj, level: close.level }); - } - } - } - } - - for ( i = 0, l = elems.length; i < l; i++ ) { - match = elems[i]; - - if ( maxLevel && match.level > maxLevel ) { - break; - } - - event.currentTarget = match.elem; - event.data = match.handleObj.data; - event.handleObj = match.handleObj; - - ret = match.handleObj.origHandler.apply( match.elem, arguments ); - - if ( ret === false || event.isPropagationStopped() ) { - maxLevel = match.level; - - if ( ret === false ) { - stop = false; - } - if ( event.isImmediatePropagationStopped() ) { - break; - } - } - } - - return stop; -} - -function liveConvert( type, selector ) { - return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspaces, "&"); -} - -jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + - "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + - "change select submit keydown keypress keyup error").split(" "), function( i, name ) { - - // Handle event binding - jQuery.fn[ name ] = function( data, fn ) { - if ( fn == null ) { - fn = data; - data = null; - } - - return arguments.length > 0 ? - this.bind( name, data, fn ) : - this.trigger( name ); - }; - - if ( jQuery.attrFn ) { - jQuery.attrFn[ name ] = true; - } -}); - - - -/*! - * Sizzle CSS Selector Engine - * Copyright 2011, The Dojo Foundation - * Released under the MIT, BSD, and GPL Licenses. - * More information: http://sizzlejs.com/ - */ -(function(){ - -var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, - done = 0, - toString = Object.prototype.toString, - hasDuplicate = false, - baseHasDuplicate = true, - rBackslash = /\\/g, - rNonWord = /\W/; - -// Here we check if the JavaScript engine is using some sort of -// optimization where it does not always call our comparision -// function. If that is the case, discard the hasDuplicate value. -// Thus far that includes Google Chrome. -[0, 0].sort(function() { - baseHasDuplicate = false; - return 0; -}); - -var Sizzle = function( selector, context, results, seed ) { - results = results || []; - context = context || document; - - var origContext = context; - - if ( context.nodeType !== 1 && context.nodeType !== 9 ) { - return []; - } - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - var m, set, checkSet, extra, ret, cur, pop, i, - prune = true, - contextXML = Sizzle.isXML( context ), - parts = [], - soFar = selector; - - // Reset the position of the chunker regexp (start from head) - do { - chunker.exec( "" ); - m = chunker.exec( soFar ); - - if ( m ) { - soFar = m[3]; - - parts.push( m[1] ); - - if ( m[2] ) { - extra = m[3]; - break; - } - } - } while ( m ); - - if ( parts.length > 1 && origPOS.exec( selector ) ) { - - if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { - set = posProcess( parts[0] + parts[1], context ); - - } else { - set = Expr.relative[ parts[0] ] ? - [ context ] : - Sizzle( parts.shift(), context ); - - while ( parts.length ) { - selector = parts.shift(); - - if ( Expr.relative[ selector ] ) { - selector += parts.shift(); - } - - set = posProcess( selector, set ); - } - } - - } else { - // Take a shortcut and set the context if the root selector is an ID - // (but not if it'll be faster if the inner selector is an ID) - if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && - Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { - - ret = Sizzle.find( parts.shift(), context, contextXML ); - context = ret.expr ? - Sizzle.filter( ret.expr, ret.set )[0] : - ret.set[0]; - } - - if ( context ) { - ret = seed ? - { expr: parts.pop(), set: makeArray(seed) } : - Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); - - set = ret.expr ? - Sizzle.filter( ret.expr, ret.set ) : - ret.set; - - if ( parts.length > 0 ) { - checkSet = makeArray( set ); - - } else { - prune = false; - } - - while ( parts.length ) { - cur = parts.pop(); - pop = cur; - - if ( !Expr.relative[ cur ] ) { - cur = ""; - } else { - pop = parts.pop(); - } - - if ( pop == null ) { - pop = context; - } - - Expr.relative[ cur ]( checkSet, pop, contextXML ); - } - - } else { - checkSet = parts = []; - } - } - - if ( !checkSet ) { - checkSet = set; - } - - if ( !checkSet ) { - Sizzle.error( cur || selector ); - } - - if ( toString.call(checkSet) === "[object Array]" ) { - if ( !prune ) { - results.push.apply( results, checkSet ); - - } else if ( context && context.nodeType === 1 ) { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { - results.push( set[i] ); - } - } - - } else { - for ( i = 0; checkSet[i] != null; i++ ) { - if ( checkSet[i] && checkSet[i].nodeType === 1 ) { - results.push( set[i] ); - } - } - } - - } else { - makeArray( checkSet, results ); - } - - if ( extra ) { - Sizzle( extra, origContext, results, seed ); - Sizzle.uniqueSort( results ); - } - - return results; -}; - -Sizzle.uniqueSort = function( results ) { - if ( sortOrder ) { - hasDuplicate = baseHasDuplicate; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( var i = 1; i < results.length; i++ ) { - if ( results[i] === results[ i - 1 ] ) { - results.splice( i--, 1 ); - } - } - } - } - - return results; -}; - -Sizzle.matches = function( expr, set ) { - return Sizzle( expr, null, null, set ); -}; - -Sizzle.matchesSelector = function( node, expr ) { - return Sizzle( expr, null, null, [node] ).length > 0; -}; - -Sizzle.find = function( expr, context, isXML ) { - var set; - - if ( !expr ) { - return []; - } - - for ( var i = 0, l = Expr.order.length; i < l; i++ ) { - var match, - type = Expr.order[i]; - - if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { - var left = match[1]; - match.splice( 1, 1 ); - - if ( left.substr( left.length - 1 ) !== "\\" ) { - match[1] = (match[1] || "").replace( rBackslash, "" ); - set = Expr.find[ type ]( match, context, isXML ); - - if ( set != null ) { - expr = expr.replace( Expr.match[ type ], "" ); - break; - } - } - } - } - - if ( !set ) { - set = typeof context.getElementsByTagName !== "undefined" ? - context.getElementsByTagName( "*" ) : - []; - } - - return { set: set, expr: expr }; -}; - -Sizzle.filter = function( expr, set, inplace, not ) { - var match, anyFound, - old = expr, - result = [], - curLoop = set, - isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); - - while ( expr && set.length ) { - for ( var type in Expr.filter ) { - if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { - var found, item, - filter = Expr.filter[ type ], - left = match[1]; - - anyFound = false; - - match.splice(1,1); - - if ( left.substr( left.length - 1 ) === "\\" ) { - continue; - } - - if ( curLoop === result ) { - result = []; - } - - if ( Expr.preFilter[ type ] ) { - match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); - - if ( !match ) { - anyFound = found = true; - - } else if ( match === true ) { - continue; - } - } - - if ( match ) { - for ( var i = 0; (item = curLoop[i]) != null; i++ ) { - if ( item ) { - found = filter( item, match, i, curLoop ); - var pass = not ^ !!found; - - if ( inplace && found != null ) { - if ( pass ) { - anyFound = true; - - } else { - curLoop[i] = false; - } - - } else if ( pass ) { - result.push( item ); - anyFound = true; - } - } - } - } - - if ( found !== undefined ) { - if ( !inplace ) { - curLoop = result; - } - - expr = expr.replace( Expr.match[ type ], "" ); - - if ( !anyFound ) { - return []; - } - - break; - } - } - } - - // Improper expression - if ( expr === old ) { - if ( anyFound == null ) { - Sizzle.error( expr ); - - } else { - break; - } - } - - old = expr; - } - - return curLoop; -}; - -Sizzle.error = function( msg ) { - throw "Syntax error, unrecognized expression: " + msg; -}; - -var Expr = Sizzle.selectors = { - order: [ "ID", "NAME", "TAG" ], - - match: { - ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, - NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, - ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, - TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, - CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, - POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, - PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ - }, - - leftMatch: {}, - - attrMap: { - "class": "className", - "for": "htmlFor" - }, - - attrHandle: { - href: function( elem ) { - return elem.getAttribute( "href" ); - }, - type: function( elem ) { - return elem.getAttribute( "type" ); - } - }, - - relative: { - "+": function(checkSet, part){ - var isPartStr = typeof part === "string", - isTag = isPartStr && !rNonWord.test( part ), - isPartStrNotTag = isPartStr && !isTag; - - if ( isTag ) { - part = part.toLowerCase(); - } - - for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { - if ( (elem = checkSet[i]) ) { - while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} - - checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? - elem || false : - elem === part; - } - } - - if ( isPartStrNotTag ) { - Sizzle.filter( part, checkSet, true ); - } - }, - - ">": function( checkSet, part ) { - var elem, - isPartStr = typeof part === "string", - i = 0, - l = checkSet.length; - - if ( isPartStr && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - var parent = elem.parentNode; - checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; - } - } - - } else { - for ( ; i < l; i++ ) { - elem = checkSet[i]; - - if ( elem ) { - checkSet[i] = isPartStr ? - elem.parentNode : - elem.parentNode === part; - } - } - - if ( isPartStr ) { - Sizzle.filter( part, checkSet, true ); - } - } - }, - - "": function(checkSet, part, isXML){ - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); - }, - - "~": function( checkSet, part, isXML ) { - var nodeCheck, - doneName = done++, - checkFn = dirCheck; - - if ( typeof part === "string" && !rNonWord.test( part ) ) { - part = part.toLowerCase(); - nodeCheck = part; - checkFn = dirNodeCheck; - } - - checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); - } - }, - - find: { - ID: function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }, - - NAME: function( match, context ) { - if ( typeof context.getElementsByName !== "undefined" ) { - var ret = [], - results = context.getElementsByName( match[1] ); - - for ( var i = 0, l = results.length; i < l; i++ ) { - if ( results[i].getAttribute("name") === match[1] ) { - ret.push( results[i] ); - } - } - - return ret.length === 0 ? null : ret; - } - }, - - TAG: function( match, context ) { - if ( typeof context.getElementsByTagName !== "undefined" ) { - return context.getElementsByTagName( match[1] ); - } - } - }, - preFilter: { - CLASS: function( match, curLoop, inplace, result, not, isXML ) { - match = " " + match[1].replace( rBackslash, "" ) + " "; - - if ( isXML ) { - return match; - } - - for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { - if ( elem ) { - if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { - if ( !inplace ) { - result.push( elem ); - } - - } else if ( inplace ) { - curLoop[i] = false; - } - } - } - - return false; - }, - - ID: function( match ) { - return match[1].replace( rBackslash, "" ); - }, - - TAG: function( match, curLoop ) { - return match[1].replace( rBackslash, "" ).toLowerCase(); - }, - - CHILD: function( match ) { - if ( match[1] === "nth" ) { - if ( !match[2] ) { - Sizzle.error( match[0] ); - } - - match[2] = match[2].replace(/^\+|\s*/g, ''); - - // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' - var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( - match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || - !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); - - // calculate the numbers (first)n+(last) including if they are negative - match[2] = (test[1] + (test[2] || 1)) - 0; - match[3] = test[3] - 0; - } - else if ( match[2] ) { - Sizzle.error( match[0] ); - } - - // TODO: Move to normal caching system - match[0] = done++; - - return match; - }, - - ATTR: function( match, curLoop, inplace, result, not, isXML ) { - var name = match[1] = match[1].replace( rBackslash, "" ); - - if ( !isXML && Expr.attrMap[name] ) { - match[1] = Expr.attrMap[name]; - } - - // Handle if an un-quoted value was used - match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); - - if ( match[2] === "~=" ) { - match[4] = " " + match[4] + " "; - } - - return match; - }, - - PSEUDO: function( match, curLoop, inplace, result, not ) { - if ( match[1] === "not" ) { - // If we're dealing with a complex expression, or a simple one - if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { - match[3] = Sizzle(match[3], null, null, curLoop); - - } else { - var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); - - if ( !inplace ) { - result.push.apply( result, ret ); - } - - return false; - } - - } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { - return true; - } - - return match; - }, - - POS: function( match ) { - match.unshift( true ); - - return match; - } - }, - - filters: { - enabled: function( elem ) { - return elem.disabled === false && elem.type !== "hidden"; - }, - - disabled: function( elem ) { - return elem.disabled === true; - }, - - checked: function( elem ) { - return elem.checked === true; - }, - - selected: function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - parent: function( elem ) { - return !!elem.firstChild; - }, - - empty: function( elem ) { - return !elem.firstChild; - }, - - has: function( elem, i, match ) { - return !!Sizzle( match[3], elem ).length; - }, - - header: function( elem ) { - return (/h\d/i).test( elem.nodeName ); - }, - - text: function( elem ) { - var attr = elem.getAttribute( "type" ), type = elem.type; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); - }, - - radio: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; - }, - - checkbox: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; - }, - - file: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; - }, - - password: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; - }, - - submit: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "submit" === elem.type; - }, - - image: function( elem ) { - return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; - }, - - reset: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && "reset" === elem.type; - }, - - button: function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && "button" === elem.type || name === "button"; - }, - - input: function( elem ) { - return (/input|select|textarea|button/i).test( elem.nodeName ); - }, - - focus: function( elem ) { - return elem === elem.ownerDocument.activeElement; - } - }, - setFilters: { - first: function( elem, i ) { - return i === 0; - }, - - last: function( elem, i, match, array ) { - return i === array.length - 1; - }, - - even: function( elem, i ) { - return i % 2 === 0; - }, - - odd: function( elem, i ) { - return i % 2 === 1; - }, - - lt: function( elem, i, match ) { - return i < match[3] - 0; - }, - - gt: function( elem, i, match ) { - return i > match[3] - 0; - }, - - nth: function( elem, i, match ) { - return match[3] - 0 === i; - }, - - eq: function( elem, i, match ) { - return match[3] - 0 === i; - } - }, - filter: { - PSEUDO: function( elem, match, i, array ) { - var name = match[1], - filter = Expr.filters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - - } else if ( name === "contains" ) { - return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; - - } else if ( name === "not" ) { - var not = match[3]; - - for ( var j = 0, l = not.length; j < l; j++ ) { - if ( not[j] === elem ) { - return false; - } - } - - return true; - - } else { - Sizzle.error( name ); - } - }, - - CHILD: function( elem, match ) { - var type = match[1], - node = elem; - - switch ( type ) { - case "only": - case "first": - while ( (node = node.previousSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - if ( type === "first" ) { - return true; - } - - node = elem; - - case "last": - while ( (node = node.nextSibling) ) { - if ( node.nodeType === 1 ) { - return false; - } - } - - return true; - - case "nth": - var first = match[2], - last = match[3]; - - if ( first === 1 && last === 0 ) { - return true; - } - - var doneName = match[0], - parent = elem.parentNode; - - if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { - var count = 0; - - for ( node = parent.firstChild; node; node = node.nextSibling ) { - if ( node.nodeType === 1 ) { - node.nodeIndex = ++count; - } - } - - parent.sizcache = doneName; - } - - var diff = elem.nodeIndex - last; - - if ( first === 0 ) { - return diff === 0; - - } else { - return ( diff % first === 0 && diff / first >= 0 ); - } - } - }, - - ID: function( elem, match ) { - return elem.nodeType === 1 && elem.getAttribute("id") === match; - }, - - TAG: function( elem, match ) { - return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; - }, - - CLASS: function( elem, match ) { - return (" " + (elem.className || elem.getAttribute("class")) + " ") - .indexOf( match ) > -1; - }, - - ATTR: function( elem, match ) { - var name = match[1], - result = Expr.attrHandle[ name ] ? - Expr.attrHandle[ name ]( elem ) : - elem[ name ] != null ? - elem[ name ] : - elem.getAttribute( name ), - value = result + "", - type = match[2], - check = match[4]; - - return result == null ? - type === "!=" : - type === "=" ? - value === check : - type === "*=" ? - value.indexOf(check) >= 0 : - type === "~=" ? - (" " + value + " ").indexOf(check) >= 0 : - !check ? - value && result !== false : - type === "!=" ? - value !== check : - type === "^=" ? - value.indexOf(check) === 0 : - type === "$=" ? - value.substr(value.length - check.length) === check : - type === "|=" ? - value === check || value.substr(0, check.length + 1) === check + "-" : - false; - }, - - POS: function( elem, match, i, array ) { - var name = match[2], - filter = Expr.setFilters[ name ]; - - if ( filter ) { - return filter( elem, i, match, array ); - } - } - } -}; - -var origPOS = Expr.match.POS, - fescape = function(all, num){ - return "\\" + (num - 0 + 1); - }; - -for ( var type in Expr.match ) { - Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); - Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); -} - -var makeArray = function( array, results ) { - array = Array.prototype.slice.call( array, 0 ); - - if ( results ) { - results.push.apply( results, array ); - return results; - } - - return array; -}; - -// Perform a simple check to determine if the browser is capable of -// converting a NodeList to an array using builtin methods. -// Also verifies that the returned array holds DOM nodes -// (which is not the case in the Blackberry browser) -try { - Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; - -// Provide a fallback method if it does not work -} catch( e ) { - makeArray = function( array, results ) { - var i = 0, - ret = results || []; - - if ( toString.call(array) === "[object Array]" ) { - Array.prototype.push.apply( ret, array ); - - } else { - if ( typeof array.length === "number" ) { - for ( var l = array.length; i < l; i++ ) { - ret.push( array[i] ); - } - - } else { - for ( ; array[i]; i++ ) { - ret.push( array[i] ); - } - } - } - - return ret; - }; -} - -var sortOrder, siblingCheck; - -if ( document.documentElement.compareDocumentPosition ) { - sortOrder = function( a, b ) { - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { - return a.compareDocumentPosition ? -1 : 1; - } - - return a.compareDocumentPosition(b) & 4 ? -1 : 1; - }; - -} else { - sortOrder = function( a, b ) { - // The nodes are identical, we can exit early - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Fallback to using sourceIndex (in IE) if it's available on both nodes - } else if ( a.sourceIndex && b.sourceIndex ) { - return a.sourceIndex - b.sourceIndex; - } - - var al, bl, - ap = [], - bp = [], - aup = a.parentNode, - bup = b.parentNode, - cur = aup; - - // If the nodes are siblings (or identical) we can do a quick check - if ( aup === bup ) { - return siblingCheck( a, b ); - - // If no parents were found then the nodes are disconnected - } else if ( !aup ) { - return -1; - - } else if ( !bup ) { - return 1; - } - - // Otherwise they're somewhere else in the tree so we need - // to build up a full list of the parentNodes for comparison - while ( cur ) { - ap.unshift( cur ); - cur = cur.parentNode; - } - - cur = bup; - - while ( cur ) { - bp.unshift( cur ); - cur = cur.parentNode; - } - - al = ap.length; - bl = bp.length; - - // Start walking down the tree looking for a discrepancy - for ( var i = 0; i < al && i < bl; i++ ) { - if ( ap[i] !== bp[i] ) { - return siblingCheck( ap[i], bp[i] ); - } - } - - // We ended someplace up the tree so do a sibling check - return i === al ? - siblingCheck( a, bp[i], -1 ) : - siblingCheck( ap[i], b, 1 ); - }; - - siblingCheck = function( a, b, ret ) { - if ( a === b ) { - return ret; - } - - var cur = a.nextSibling; - - while ( cur ) { - if ( cur === b ) { - return -1; - } - - cur = cur.nextSibling; - } - - return 1; - }; -} - -// Utility function for retreiving the text value of an array of DOM nodes -Sizzle.getText = function( elems ) { - var ret = "", elem; - - for ( var i = 0; elems[i]; i++ ) { - elem = elems[i]; - - // Get the text from text nodes and CDATA nodes - if ( elem.nodeType === 3 || elem.nodeType === 4 ) { - ret += elem.nodeValue; - - // Traverse everything else, except comment nodes - } else if ( elem.nodeType !== 8 ) { - ret += Sizzle.getText( elem.childNodes ); - } - } - - return ret; -}; - -// Check to see if the browser returns elements by name when -// querying by getElementById (and provide a workaround) -(function(){ - // We're going to inject a fake input element with a specified name - var form = document.createElement("div"), - id = "script" + (new Date()).getTime(), - root = document.documentElement; - - form.innerHTML = ""; - - // Inject it into the root element, check its status, and remove it quickly - root.insertBefore( form, root.firstChild ); - - // The workaround has to do additional checks after a getElementById - // Which slows things down for other browsers (hence the branching) - if ( document.getElementById( id ) ) { - Expr.find.ID = function( match, context, isXML ) { - if ( typeof context.getElementById !== "undefined" && !isXML ) { - var m = context.getElementById(match[1]); - - return m ? - m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? - [m] : - undefined : - []; - } - }; - - Expr.filter.ID = function( elem, match ) { - var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); - - return elem.nodeType === 1 && node && node.nodeValue === match; - }; - } - - root.removeChild( form ); - - // release memory in IE - root = form = null; -})(); - -(function(){ - // Check to see if the browser returns only elements - // when doing getElementsByTagName("*") - - // Create a fake element - var div = document.createElement("div"); - div.appendChild( document.createComment("") ); - - // Make sure no comments are found - if ( div.getElementsByTagName("*").length > 0 ) { - Expr.find.TAG = function( match, context ) { - var results = context.getElementsByTagName( match[1] ); - - // Filter out possible comments - if ( match[1] === "*" ) { - var tmp = []; - - for ( var i = 0; results[i]; i++ ) { - if ( results[i].nodeType === 1 ) { - tmp.push( results[i] ); - } - } - - results = tmp; - } - - return results; - }; - } - - // Check to see if an attribute returns normalized href attributes - div.innerHTML = ""; - - if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && - div.firstChild.getAttribute("href") !== "#" ) { - - Expr.attrHandle.href = function( elem ) { - return elem.getAttribute( "href", 2 ); - }; - } - - // release memory in IE - div = null; -})(); - -if ( document.querySelectorAll ) { - (function(){ - var oldSizzle = Sizzle, - div = document.createElement("div"), - id = "__sizzle__"; - - div.innerHTML = "

"; - - // Safari can't handle uppercase or unicode characters when - // in quirks mode. - if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { - return; - } - - Sizzle = function( query, context, extra, seed ) { - context = context || document; - - // Only use querySelectorAll on non-XML documents - // (ID selectors don't work in non-HTML documents) - if ( !seed && !Sizzle.isXML(context) ) { - // See if we find a selector to speed up - var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); - - if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { - // Speed-up: Sizzle("TAG") - if ( match[1] ) { - return makeArray( context.getElementsByTagName( query ), extra ); - - // Speed-up: Sizzle(".CLASS") - } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { - return makeArray( context.getElementsByClassName( match[2] ), extra ); - } - } - - if ( context.nodeType === 9 ) { - // Speed-up: Sizzle("body") - // The body element only exists once, optimize finding it - if ( query === "body" && context.body ) { - return makeArray( [ context.body ], extra ); - - // Speed-up: Sizzle("#ID") - } else if ( match && match[3] ) { - var elem = context.getElementById( match[3] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id === match[3] ) { - return makeArray( [ elem ], extra ); - } - - } else { - return makeArray( [], extra ); - } - } - - try { - return makeArray( context.querySelectorAll(query), extra ); - } catch(qsaError) {} - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - var oldContext = context, - old = context.getAttribute( "id" ), - nid = old || id, - hasParent = context.parentNode, - relativeHierarchySelector = /^\s*[+~]/.test( query ); - - if ( !old ) { - context.setAttribute( "id", nid ); - } else { - nid = nid.replace( /'/g, "\\$&" ); - } - if ( relativeHierarchySelector && hasParent ) { - context = context.parentNode; - } - - try { - if ( !relativeHierarchySelector || hasParent ) { - return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); - } - - } catch(pseudoError) { - } finally { - if ( !old ) { - oldContext.removeAttribute( "id" ); - } - } - } - } - - return oldSizzle(query, context, extra, seed); - }; - - for ( var prop in oldSizzle ) { - Sizzle[ prop ] = oldSizzle[ prop ]; - } - - // release memory in IE - div = null; - })(); -} - -(function(){ - var html = document.documentElement, - matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; - - if ( matches ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9 fails this) - var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), - pseudoWorks = false; - - try { - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( document.documentElement, "[test!='']:sizzle" ); - - } catch( pseudoError ) { - pseudoWorks = true; - } - - Sizzle.matchesSelector = function( node, expr ) { - // Make sure that attribute selectors are quoted - expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); - - if ( !Sizzle.isXML( node ) ) { - try { - if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { - var ret = matches.call( node, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || !disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9, so check for that - node.document && node.document.nodeType !== 11 ) { - return ret; - } - } - } catch(e) {} - } - - return Sizzle(expr, null, null, [node]).length > 0; - }; - } -})(); - -(function(){ - var div = document.createElement("div"); - - div.innerHTML = "
"; - - // Opera can't find a second classname (in 9.6) - // Also, make sure that getElementsByClassName actually exists - if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { - return; - } - - // Safari caches class attributes, doesn't catch changes (in 3.2) - div.lastChild.className = "e"; - - if ( div.getElementsByClassName("e").length === 1 ) { - return; - } - - Expr.order.splice(1, 0, "CLASS"); - Expr.find.CLASS = function( match, context, isXML ) { - if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { - return context.getElementsByClassName(match[1]); - } - }; - - // release memory in IE - div = null; -})(); - -function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 && !isXML ){ - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( elem.nodeName.toLowerCase() === cur ) { - match = elem; - break; - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { - for ( var i = 0, l = checkSet.length; i < l; i++ ) { - var elem = checkSet[i]; - - if ( elem ) { - var match = false; - - elem = elem[dir]; - - while ( elem ) { - if ( elem.sizcache === doneName ) { - match = checkSet[elem.sizset]; - break; - } - - if ( elem.nodeType === 1 ) { - if ( !isXML ) { - elem.sizcache = doneName; - elem.sizset = i; - } - - if ( typeof cur !== "string" ) { - if ( elem === cur ) { - match = true; - break; - } - - } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { - match = elem; - break; - } - } - - elem = elem[dir]; - } - - checkSet[i] = match; - } - } -} - -if ( document.documentElement.contains ) { - Sizzle.contains = function( a, b ) { - return a !== b && (a.contains ? a.contains(b) : true); - }; - -} else if ( document.documentElement.compareDocumentPosition ) { - Sizzle.contains = function( a, b ) { - return !!(a.compareDocumentPosition(b) & 16); - }; - -} else { - Sizzle.contains = function() { - return false; - }; -} - -Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; - - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -var posProcess = function( selector, context ) { - var match, - tmpSet = [], - later = "", - root = context.nodeType ? [context] : context; - - // Position selectors must be done after the filter - // And so must :not(positional) so we move all PSEUDOs to the end - while ( (match = Expr.match.PSEUDO.exec( selector )) ) { - later += match[0]; - selector = selector.replace( Expr.match.PSEUDO, "" ); - } - - selector = Expr.relative[selector] ? selector + "*" : selector; - - for ( var i = 0, l = root.length; i < l; i++ ) { - Sizzle( selector, root[i], tmpSet ); - } - - return Sizzle.filter( later, tmpSet ); -}; - -// EXPOSE -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.filters; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})(); - - -var runtil = /Until$/, - rparentsprev = /^(?:parents|prevUntil|prevAll)/, - // Note: This RegExp should be improved, or likely pulled from Sizzle - rmultiselector = /,/, - isSimple = /^.[^:#\[\.,]*$/, - slice = Array.prototype.slice, - POS = jQuery.expr.match.POS, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var self = this, - i, l; - - if ( typeof selector !== "string" ) { - return jQuery( selector ).filter(function() { - for ( i = 0, l = self.length; i < l; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }); - } - - var ret = this.pushStack( "", "find", selector ), - length, n, r; - - for ( i = 0, l = this.length; i < l; i++ ) { - length = ret.length; - jQuery.find( selector, this[i], ret ); - - if ( i > 0 ) { - // Make sure that the results are unique - for ( n = length; n < ret.length; n++ ) { - for ( r = 0; r < length; r++ ) { - if ( ret[r] === ret[n] ) { - ret.splice(n--, 1); - break; - } - } - } - } - } - - return ret; - }, - - has: function( target ) { - var targets = jQuery( target ); - return this.filter(function() { - for ( var i = 0, l = targets.length; i < l; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false), "not", selector); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true), "filter", selector ); - }, - - is: function( selector ) { - return !!selector && ( typeof selector === "string" ? - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); - }, - - closest: function( selectors, context ) { - var ret = [], i, l, cur = this[0]; - - // Array - if ( jQuery.isArray( selectors ) ) { - var match, selector, - matches = {}, - level = 1; - - if ( cur && selectors.length ) { - for ( i = 0, l = selectors.length; i < l; i++ ) { - selector = selectors[i]; - - if ( !matches[ selector ] ) { - matches[ selector ] = POS.test( selector ) ? - jQuery( selector, context || this.context ) : - selector; - } - } - - while ( cur && cur.ownerDocument && cur !== context ) { - for ( selector in matches ) { - match = matches[ selector ]; - - if ( match.jquery ? match.index( cur ) > -1 : jQuery( cur ).is( match ) ) { - ret.push({ selector: selector, elem: cur, level: level }); - } - } - - cur = cur.parentNode; - level++; - } - } - - return ret; - } - - // String - var pos = POS.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( i = 0, l = this.length; i < l; i++ ) { - cur = this[i]; - - while ( cur ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - - } else { - cur = cur.parentNode; - if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { - break; - } - } - } - } - - ret = ret.length > 1 ? jQuery.unique( ret ) : ret; - - return this.pushStack( ret, "closest", selectors ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - if ( !elem || typeof elem === "string" ) { - return jQuery.inArray( this[0], - // If it receives a string, the selector is used - // If it receives nothing, the siblings are used - elem ? jQuery( elem ) : this.parent().children() ); - } - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? - all : - jQuery.unique( all ) ); - }, - - andSelf: function() { - return this.add( this.prevObject ); - } -}); - -// A painfully simple check to see if an element is disconnected -// from a document (should be improved, where feasible). -function isDisconnected( node ) { - return !node || !node.parentNode || node.parentNode.nodeType === 11; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return jQuery.nth( elem, 2, "nextSibling" ); - }, - prev: function( elem ) { - return jQuery.nth( elem, 2, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( elem.parentNode.firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.makeArray( elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ), - // The variable 'args' was introduced in - // https://github.com/jquery/jquery/commit/52a0238 - // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. - // http://code.google.com/p/v8/issues/detail?id=1050 - args = slice.call(arguments); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret, name, args.join(",") ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - nth: function( cur, result, dir, elem ) { - result = result || 1; - var num = 0; - - for ( ; cur; cur = cur[dir] ) { - if ( cur.nodeType === 1 && ++num === result ) { - break; - } - } - - return cur; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem, i ) { - return (elem === qualifier) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem, i ) { - return (jQuery.inArray( elem, qualifier ) >= 0) === keep; - }); -} - - - - -var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, - rtagName = /<([\w:]+)/, - rtbody = /", "" ], - legend: [ 1, "
", "
" ], - thead: [ 1, "", "
" ], - tr: [ 2, "", "
" ], - td: [ 3, "", "
" ], - col: [ 2, "", "
" ], - area: [ 1, "", "" ], - _default: [ 0, "", "" ] - }; - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -// IE can't serialize and