diff --git a/Netdisco/Changes b/Netdisco/Changes index 5beaec8e..a5daeaae 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -2,10 +2,19 @@ [NEW FEATURES] - * Finally we have a discover/refresh daemon job :) + * Finally we have a discover/refresh/arpnip/macsuck daemon jobs :) * Also... a Scheduler which removes need for crontab installation - * The netdisco-do script can run a one-off discover for a device - * Can select MAC Address display format on Node and Device Port search + * The netdisco-do script can queue any one-off job + * Select MAC Address display format on Node and Device Port search + * Helper script to import the Netdisco 1.x Topology file to the database + * Support for pseudo devices (useful for dummy device links) + * Manual Topology editing via the web + * Job Queue view and delete page + * Empty device table prompts initial discover on homepage + * Support for App::NetdiscoX::Web::Plugin namespace + * Plugins can add columns to Device Ports display + * Observium Sparklines port column plugin + * Plugins can have CSS and Javascript loaded within [ENHANCEMENTS] @@ -14,12 +23,20 @@ * Port filter in device port display is now highlighted green * Navbar search is fuzzier * Phone node icon is a little phone handset + * User icon color indicates port_control/admin ability + * Buttons for discover/macsuck/arpnip on device details page + * Support 'path' config option as alternative to --path /mountpoint + * Local plugins can be placed in ${NETDISCO_HOME}/site_plugins/... + * Missing mibdirs causes all MIBs to be loaded (with a warning) [BUG FIXES] * Rename plugins developer doc to .pod * Update to latest Bootstrap and JQuery, and temp. fix #7326 in Bootstrap * Partial Name in Port search now working + * Add unique constraints to topology table + * Handle whitespace ahead of OUI data + * Wasn't using Bootstrap table class properly 2.007000_001 - 2013-03-17 diff --git a/Netdisco/Makefile.PL b/Netdisco/Makefile.PL index 1d7c2bf7..6cbd7267 100644 --- a/Netdisco/Makefile.PL +++ b/Netdisco/Makefile.PL @@ -19,6 +19,7 @@ requires 'HTML::Parser' => 3.70; requires 'HTTP::Tiny' => 0.029; requires 'JSON' => 0; requires 'List::MoreUtils' => 0.33; +requires 'MIME::Base64' => 3.13; requires 'Moo' => 1.001000; requires 'MCE' => 1.408; requires 'Net::DNS' => 0.72; @@ -32,7 +33,7 @@ requires 'Socket6' => 0.23; requires 'Starman' => 0.3008; requires 'SNMP::Info' => 3.01; requires 'SQL::Translator' => 0.11016; -requires 'Template::Toolkit' => 2.24; +requires 'Template' => 2.24; requires 'YAML' => 0.84; requires 'namespace::clean' => 0.24; requires 'version' => 0.9902; diff --git a/Netdisco/bin/nd-dbic-versions b/Netdisco/bin/nd-dbic-versions index 0e4148c9..a41e4e15 100755 --- a/Netdisco/bin/nd-dbic-versions +++ b/Netdisco/bin/nd-dbic-versions @@ -1,9 +1,17 @@ #!/usr/bin/env perl use FindBin; -use lib "$FindBin::Bin/../lib"; -use App::Netdisco; +FindBin::again(); +use Path::Class 'dir'; +BEGIN { + # stuff useful locations into @INC + unshift @INC, + dir($FindBin::RealBin)->parent->subdir('lib')->stringify, + dir($FindBin::RealBin, 'lib')->stringify; +} + +use App::Netdisco; use Dancer ':script'; use Dancer::Plugin::DBIC 'schema'; diff --git a/Netdisco/bin/nd-import-topology b/Netdisco/bin/nd-import-topology new file mode 100755 index 00000000..35469c2a --- /dev/null +++ b/Netdisco/bin/nd-import-topology @@ -0,0 +1,99 @@ +#!/usr/bin/env perl + +use FindBin; +FindBin::again(); +use Path::Class 'dir'; + +BEGIN { + # stuff useful locations into @INC + unshift @INC, + dir($FindBin::RealBin)->parent->subdir('lib')->stringify, + dir($FindBin::RealBin, 'lib')->stringify; +} + +use App::Netdisco; +use Dancer ':script'; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::Device 'get_device'; +use NetAddr::IP::Lite ':lower'; +use Try::Tiny; + +=head1 NAME + +nd-import-topology - Import a Nedisco 1.x Manual Topology File + +=head2 USAGE + + ./nd-import-topology /path/to/netdisco-topology.txt + +=head2 DESCRIPTION + +This helper script will read and import the content of a Netdisco 1.x format +Manual Topology file into the Netdisco 2.x database's C table. + +It's safe to run the script multiple times on the same file - any new data +will be imported. + +The file syntax must be like so: + + left-device + link:left-port,right-device,right-port + +The devices can be either host names or IPs. Data will be imported even if the +devices are currently unknown to Netdisco. + +=cut + +my $file = $ARGV[0]; +die "missing topology file name on command line\n" unless $file; + +chomp $file; +my $dev = undef; # current device +print "Loading topology information from $file\n"; + +open (DEVS,'<', $file) + or die "topo_load_file($file): $!\n"; + +while (my $line = ) { + chomp $line; + $line =~ s/(?txn_do(sub { + schema('netdisco')->resultset('Topology')->create({ + dev1 => $dev, + port1 => $from_port, + dev2 => get_device($to)->ip, + port2 => $to_port, + }); + }); + }; + } + elsif ($line =~ /^alias:(.*)/) { + # ignore aliases + } + else { + my $ip = NetAddr::IP::Lite->new($line) + or next; + next if $ip->addr eq '0.0.0.0'; + + $dev = get_device($ip->addr)->ip; + print " Set device: $dev\n"; + } +} + +close (DEVS); diff --git a/Netdisco/bin/netdisco-db-deploy b/Netdisco/bin/netdisco-db-deploy index ea75fbef..3a41d017 100755 --- a/Netdisco/bin/netdisco-db-deploy +++ b/Netdisco/bin/netdisco-db-deploy @@ -1,9 +1,17 @@ #!/usr/bin/env perl use FindBin; -use lib "$FindBin::Bin/../lib"; -use App::Netdisco; +FindBin::again(); +use Path::Class 'dir'; +BEGIN { + # stuff useful locations into @INC + unshift @INC, + dir($FindBin::RealBin)->parent->subdir('lib')->stringify, + dir($FindBin::RealBin, 'lib')->stringify; +} + +use App::Netdisco; use Dancer ':script'; use Dancer::Plugin::DBIC 'schema'; @@ -69,7 +77,8 @@ try { }; # upgrade from whatever dbix_class_schema_versions says, to $VERSION -my $db_version = $schema->get_db_version; +# except that get_db_version will be 0 at first deploy +my $db_version = ($schema->get_db_version || 1); my $target_version = $schema->schema_version; # one step at a time, in case user has applied local changes already diff --git a/Netdisco/bin/netdisco-deploy b/Netdisco/bin/netdisco-deploy index a03bf1f2..017f439b 100755 --- a/Netdisco/bin/netdisco-deploy +++ b/Netdisco/bin/netdisco-deploy @@ -134,7 +134,7 @@ sub deploy_oui { if ($resp->{success}) { foreach my $line (split /\n/, $resp->{content}) { - if ($line =~ m/^(.{2}-.{2}-.{2})\s+\(hex\)\s+(.*)\s*$/i) { + if ($line =~ m/^\s*(.{2}-.{2}-.{2})\s+\(hex\)\s+(.*)\s*$/i) { my ($oui, $company) = ($1, $2); $oui =~ s/-/:/g; $data{lc($oui)} = $company; diff --git a/Netdisco/bin/netdisco-do b/Netdisco/bin/netdisco-do index aade7701..1eac0a46 100755 --- a/Netdisco/bin/netdisco-do +++ b/Netdisco/bin/netdisco-do @@ -50,6 +50,8 @@ if (!length $action) { package MyWorker; use Moo; with 'App::Netdisco::Daemon::Worker::Poller::Device'; + with 'App::Netdisco::Daemon::Worker::Poller::Arpnip'; + with 'App::Netdisco::Daemon::Worker::Poller::Macsuck'; } my $worker = MyWorker->new(); diff --git a/Netdisco/bin/netdisco-web-fg b/Netdisco/bin/netdisco-web-fg index a8de4fa0..201ec04a 100755 --- a/Netdisco/bin/netdisco-web-fg +++ b/Netdisco/bin/netdisco-web-fg @@ -34,7 +34,16 @@ set plack_middlewares => [ ]; use App::Netdisco::Web; -dance; +use Plack::Builder; + +my $app = sub { + my $env = shift; + my $request = Dancer::Request->new(env => $env); + Dancer->dance($request); +}; + +my $path = (setting('path') || '/'); +builder { mount $path => $app }; =head1 NAME diff --git a/Netdisco/lib/App/Netdisco/Core/Arpnip.pm b/Netdisco/lib/App/Netdisco/Core/Arpnip.pm new file mode 100644 index 00000000..d4015d1b --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Core/Arpnip.pm @@ -0,0 +1,176 @@ +package App::Netdisco::Core::Arpnip; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::PortMAC 'get_port_macs'; +use App::Netdisco::Util::SanityCheck 'check_mac'; +use App::Netdisco::Util::DNS ':all'; +use NetAddr::IP::Lite ':lower'; +use Time::HiRes 'gettimeofday'; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/ do_arpnip store_arp /; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Core::Arpnip + +=head1 DESCRIPTION + +Helper subroutines to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 do_arpnip( $device, $snmp ) + +Given a Device database object, and a working SNMP connection, connect to a +device and discover its ARP cache for IPv4 and Neighbor cache for IPv6. + +Will also discover subnets in use on the device and update the Subnets table. + +=cut + +sub do_arpnip { + my ($device, $snmp) = @_; + + unless ($device->in_storage) { + debug sprintf ' [%s] arpnip - skipping device not yet discovered', $device->ip; + return; + } + + my $port_macs = get_port_macs($device); + + # get v4 arp table + my @v4 = _get_arps($device, $port_macs, $snmp->at_paddr, $snmp->at_netaddr); + # get v6 neighbor cache + my @v6 = _get_arps($device, $port_macs, $snmp->ipv6_n2p_mac, $snmp->ipv6_n2p_addr); + + # get directly connected networks + my @subnets = _gather_subnets($device, $snmp); + # TODO: IPv6 subnets + + # would be possible just to use now() on updated records, but by using this + # same value for them all, we _can_ if we want add a job at the end to + # select and do something with the updated set (no reason to yet, though) + my $now = 'to_timestamp('. (join '.', gettimeofday) .')'; + + # update node_ip with ARP and Neighbor Cache entries + store_arp(@$_, $now) for @v4; + debug sprintf ' [%s] arpnip - processed %s ARP Cache entries', + $device->ip, scalar @v4; + + store_arp(@$_, $now) for @v6; + debug sprintf ' [%s] arpnip - processed %s IPv6 Neighbor Cache entries', + $device->ip, scalar @v6; + + _store_subnet($_, $now) for @subnets; + debug sprintf ' [%s] arpnip - processed %s Subnet entries', + $device->ip, scalar @subnets; +} + +# get an arp table (v4 or v6) +sub _get_arps { + my ($device, $port_macs, $paddr, $netaddr) = @_; + my @arps = (); + + while (my ($arp, $node) = each %$paddr) { + my $ip = $netaddr->{$arp}; + next unless defined $ip; + next unless check_mac($device, $node, $port_macs); + push @arps, [$node, $ip, hostname_from_ip($ip)]; + } + + return @arps; +} + +=head2 store_arp( $mac, $ip, $name, $now? ) + +Stores a new entry to the C table with the given MAC, IP (v4 or v6) +and DNS host name. + +Will mark old entries for this IP as no longer C. + +Optionally a literal string can be passed in the fourth argument for the +C timestamp, otherwise the current timestamp (C) is used. + +=cut + +sub store_arp { + my ($mac, $ip, $name, $now) = @_; + $now ||= 'now()'; + + schema('netdisco')->txn_do(sub { + my $current = schema('netdisco')->resultset('NodeIp') + ->search({ip => $ip, -bool => 'active'}) + ->search(undef, { + columns => [qw/mac ip/], + order_by => [qw/mac ip/], + for => 'update' + }); + $current->first; # lock rows + $current->update({active => \'false'}); + + schema('netdisco')->resultset('NodeIp') + ->search({'me.mac' => $mac, 'me.ip' => $ip}) + ->update_or_create( + { + dns => $name, + active => \'true', + time_last => \$now, + }, + { + order_by => [qw/mac ip/], + for => 'update', + }); + }); +} + +# gathers device subnets +sub _gather_subnets { + my ($device, $snmp) = @_; + my @subnets = (); + + my $ip_netmask = $snmp->ip_netmask; + my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8'); + + foreach my $entry (keys %$ip_netmask) { + my $ip = NetAddr::IP::Lite->new($entry); + my $addr = $ip->addr; + + next if $addr eq '0.0.0.0'; + next if $ip->within($localnet); + next if setting('ignore_private_nets') and $ip->is_rfc1918; + + my $netmask = $ip_netmask->{$addr}; + next if $netmask eq '255.255.255.255' or $netmask eq '0.0.0.0'; + + my $cidr = NetAddr::IP::Lite->new($addr, $netmask)->network->cidr; + + debug sprintf ' [%s] arpnip - found subnet %s', $device->ip, $cidr; + push @subnets, $cidr; + } + + return @subnets; +} + +# update subnets with new networks +sub _store_subnet { + my ($subnet, $now) = @_; + + schema('netdisco')->txn_do(sub { + schema('netdisco')->resultset('Subnet')->update_or_create( + { + net => $subnet, + last_discover => \$now, + }, + { for => 'update' }); + }); +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm b/Netdisco/lib/App/Netdisco/Core/Discover.pm similarity index 80% rename from Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm rename to Netdisco/lib/App/Netdisco/Core/Discover.pm index d759503f..590fd8ad 100644 --- a/Netdisco/lib/App/Netdisco/Util/DiscoverAndStore.pm +++ b/Netdisco/lib/App/Netdisco/Core/Discover.pm @@ -1,4 +1,4 @@ -package App::Netdisco::Util::DiscoverAndStore; +package App::Netdisco::Core::Discover; use Dancer qw/:syntax :script/; use Dancer::Plugin::DBIC 'schema'; @@ -13,13 +13,13 @@ our @EXPORT = (); our @EXPORT_OK = qw/ store_device store_interfaces store_wireless store_vlans store_power store_modules - find_neighbors + store_neighbors discover_new_neighbors /; our %EXPORT_TAGS = (all => \@EXPORT_OK); =head1 NAME -App::Netdisco::Util::DiscoverAndStore +App::Netdisco::Core::Discover =head1 DESCRIPTION @@ -52,6 +52,7 @@ sub store_device { my $hostname = hostname_from_ip($device->ip); $device->dns($hostname) if length $hostname; + my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8'); # build device aliases suitable for DBIC my @aliases; @@ -60,7 +61,7 @@ sub store_device { my $addr = $ip->addr; next if $addr eq '0.0.0.0'; - next if $ip->within(NetAddr::IP::Lite->new('127.0.0.0/8')); + next if $ip->within($localnet); next if setting('ignore_private_nets') and $ip->is_rfc1918; my $iid = $ip_index->{$addr}; @@ -105,7 +106,7 @@ sub store_device { my $gone = $device->device_ips->delete; debug sprintf ' [%s] device - removed %s aliases', $device->ip, $gone; - $device->update_or_insert; + $device->update_or_insert(undef, {for => 'update'}); $device->device_ips->populate(\@aliases); debug sprintf ' [%s] device - added %d new aliases', $device->ip, scalar @aliases; @@ -118,7 +119,7 @@ sub _set_canonical_ip { my $oldip = $device->ip; my $newip = $snmp->root_ip; - if (length $newip) { + if (defined $newip) { if ($oldip ne $newip) { debug sprintf ' [%s] device - changing root IP to alt IP %s', $oldip, $newip; @@ -252,7 +253,7 @@ sub store_interfaces { my $gone = $device->ports->delete; debug sprintf ' [%s] interfaces - removed %s interfaces', $device->ip, $gone; - $device->update_or_insert; + $device->update_or_insert(undef, {for => 'update'}); $device->ports->populate(\@interfaces); debug sprintf ' [%s] interfaces - added %d new interfaces', $device->ip, scalar @interfaces; @@ -562,26 +563,34 @@ sub store_modules { }); } -=head2 find_neighbors( $device, $snmp ) +=head2 store_neighbors( $device, $snmp ) + +returns: C<@to_discover> Given a Device database object, and a working SNMP connection, discover and store the device's port neighbors information. -If any neighbor is unknown to Netdisco, a discover job for it will immediately -be queued (modulo configuration file C setting). +Entries in the Topology database table will override any discovered device +port relationships. The Device database object can be a fresh L object which is not yet stored to the database. +A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples. + =cut -sub find_neighbors { +sub store_neighbors { my ($device, $snmp) = @_; + my @to_discover = (); + + # first allow any manually configred topology to be set + _set_manual_topology($device, $snmp); my $c_ip = $snmp->c_ip; unless ($snmp->hasCDP or scalar keys %$c_ip) { debug sprintf ' [%s] neigh - CDP/LLDP not enabled!', $device->ip; - return; + return @to_discover; } my $interfaces = $snmp->interfaces; @@ -642,17 +651,28 @@ sub find_neighbors { } } + # IP Phone detection type fixup + if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) { + $remote_type = 'IP Phone - '. $remote_type + if $remote_type !~ /ip phone/i; + } + else { + $remote_type = ''; + } + # hack for devices seeing multiple neighbors on the port if (ref [] eq ref $remote_ip) { debug sprintf ' [%s] neigh - port %s has multiple neighbors, setting remote as self', $device->ip, $port; - foreach my $n (@$remote_ip) { - debug sprintf - ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', - $device->ip, $n, $remote_type, $port; - _enqueue_discover($n, $remote_type); + if (wantarray) { + foreach my $n (@$remote_ip) { + debug sprintf + ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', + $device->ip, $n, $remote_type, $port; + push @to_discover, [$n, $remote_type]; + } } # set self as remote IP to suppress any further work @@ -660,6 +680,14 @@ sub find_neighbors { $remote_port = $port; } else { + # what we came here to do.... discover the neighbor + if (wantarray) { + debug sprintf + ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', + $device->ip, $remote_ip, $remote_type, $port; + push @to_discover, [$remote_ip, $remote_type]; + } + $remote_port = $c_port->{$entry}; if (defined $remote_port) { @@ -672,12 +700,7 @@ sub find_neighbors { } } - # XXX too custom? IP Phone detection - if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) { - $remote_type = 'IP Phone - '. $remote_type - if $remote_type !~ /ip phone/i; - } - + # if all the data looks sane, update the port row with neighbor info my $portrow = schema('netdisco')->resultset('DevicePort') ->single({ip => $device->ip, port => $port}); @@ -687,44 +710,120 @@ sub find_neighbors { next; } + if ($portrow->manual_topo) { + info sprintf ' [%s] neigh - %s has manually defined topology', + $device->ip, $port; + next; + } + $portrow->update({ remote_ip => $remote_ip, remote_port => $remote_port, remote_type => $remote_type, remote_id => $remote_id, + is_uplink => \"true", + manual_topo => \"false", }); - - debug sprintf - ' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue', - $device->ip, $remote_ip, $remote_type, $port; - _enqueue_discover($remote_ip, $remote_type); } + + return @to_discover; } -# only enqueue if device is not already discovered, and -# discover_no_type config permits the discovery -sub _enqueue_discover { - my ($ip, $remote_type) = @_; +# take data from the topology table and update remote_ip and remote_port +# in the devices table. only use root_ips and skip any bad topo entries. +sub _set_manual_topology { + my ($device, $snmp) = @_; - my $device = get_device($ip); - return if $device->in_storage; + schema('netdisco')->txn_do(sub { + # clear manual topology flags + schema('netdisco')->resultset('DevicePort')->update({manual_topo => \'false'}); - my $remote_type_match = setting('discover_no_type'); - if ($remote_type and $remote_type_match - and $remote_type =~ m/$remote_type_match/) { - debug sprintf ' queue - %s, type [%s] excluded by discover_no_type', - $ip, $remote_type; - return; - } + my $topo_links = schema('netdisco')->resultset('Topology'); + debug sprintf ' [%s] neigh - setting manual topology links', $device->ip; + + while (my $link = $topo_links->next) { + # could fail for broken topo, but we ignore to try the rest + try { + schema('netdisco')->txn_do(sub { + # only work on root_ips + my $left = get_device($link->dev1); + my $right = get_device($link->dev2); + + # skip bad entries + return unless ($left->in_storage and $right->in_storage); + + $left->ports + ->single({port => $link->port1}, {for => 'update'}) + ->update({ + remote_ip => $right->ip, + remote_port => $link->port2, + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + + $right->ports + ->single({port => $link->port2}, {for => 'update'}) + ->update({ + remote_ip => $left->ip, + remote_port => $link->port1, + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + }); + }; + } + }); +} + +=head2 discover_new_neighbors( $device, $snmp ) + +Given a Device database object, and a working SNMP connection, discover and +store the device's port neighbors information. + +Entries in the Topology database table will override any discovered device +port relationships. + +The Device database object can be a fresh L object which is +not yet stored to the database. + +Any discovered neighbor unknown to Netdisco will have a C job +immediately queued (subject to the filtering by the C +setting). + +=cut + +sub discover_new_neighbors { + my @to_discover = store_neighbors(@_); + + # only enqueue if device is not already discovered, and + # discover_no_type config permits the discovery + foreach my $neighbor (@to_discover) { + my ($ip, $remote_type) = @$neighbor; + + my $device = get_device($ip); + next if $device->in_storage; + + my $remote_type_match = setting('discover_no_type'); + if ($remote_type and $remote_type_match + and $remote_type =~ m/$remote_type_match/) { + debug sprintf ' queue - %s, type [%s] excluded by discover_no_type', + $ip, $remote_type; + next; + } - try { # could fail if queued job already exists - schema('netdisco')->resultset('Admin')->create({ - device => $ip, - action => 'discover', - status => 'queued', - }); - }; + try { + schema('netdisco')->resultset('Admin')->create({ + device => $ip, + action => 'discover', + status => 'queued', + }); + }; + } } 1; diff --git a/Netdisco/lib/App/Netdisco/Core/Macsuck.pm b/Netdisco/lib/App/Netdisco/Core/Macsuck.pm new file mode 100644 index 00000000..071e9a7c --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Core/Macsuck.pm @@ -0,0 +1,457 @@ +package App::Netdisco::Core::Macsuck; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::PortMAC 'get_port_macs'; +use App::Netdisco::Util::SanityCheck 'check_mac'; +use App::Netdisco::Util::SNMP 'snmp_comm_reindex'; +use Time::HiRes 'gettimeofday'; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/ + do_macsuck + store_node + store_wireless_client_info +/; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Core::Macsuck + +=head1 DESCRIPTION + +Helper subroutines to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 do_macsuck( $device, $snmp ) + +Given a Device database object, and a working SNMP connection, connect to a +device and discover the MAC addresses listed against each physical port +without a neighbor. + +If the device has VLANs, C will walk each VALN to get the MAC +addresses from there. + +It will also gather wireless client information if C +configuration setting is enabled. + +=cut + +sub do_macsuck { + my ($device, $snmp) = @_; + + unless ($device->in_storage) { + debug sprintf + ' [%s] macsuck - skipping device not yet discovered', + $device->ip; + return; + } + + # would be possible just to use now() on updated records, but by using this + # same value for them all, we _can_ if we want add a job at the end to + # select and do something with the updated set (no reason to yet, though) + my $now = 'to_timestamp('. (join '.', gettimeofday) .')'; + my $total_nodes = 0; + + # do this before we start messing with the snmp community string + store_wireless_client_info($device, $snmp, $now); + + # cache the device ports to save hitting the database for many single rows + my $device_ports = {map {($_->port => $_)} $device->ports->all}; + my $port_macs = get_port_macs($device); + + # get forwarding table data via basic snmp connection + my $fwtable = { 0 => _walk_fwtable($device, $snmp, $port_macs, $device_ports) }; + + # ...then per-vlan if supported + my @vlan_list = _get_vlan_list($device, $snmp); + foreach my $vlan (@vlan_list) { + snmp_comm_reindex($snmp, $vlan); + $fwtable->{$vlan} = _walk_fwtable($device, $snmp, $port_macs, $device_ports); + } + + # now it's time to call store_node for every node discovered + # on every port on every vlan on this device. + + # reverse sort allows vlan 0 entries to be included only as fallback + foreach my $vlan (reverse sort keys %$fwtable) { + foreach my $port (keys %{ $fwtable->{$vlan} }) { + if ($device_ports->{$port}->is_uplink) { + debug sprintf + ' [%s] macsuck - port %s is uplink, topo broken - skipping.', + $device->ip, $port; + next; + } + + debug sprintf ' [%s] macsuck - port %s vlan %s : %s nodes', + $device->ip, $port, $vlan, scalar keys %{ $fwtable->{$vlan}->{$port} }; + + foreach my $mac (keys %{ $fwtable->{$vlan}->{$port} }) { + # remove vlan 0 entry for this MAC addr + delete $fwtable->{0}->{$_}->{$mac} + for keys %{ $fwtable->{0} }; + + ++$total_nodes; + store_node($device->ip, $vlan, $port, $mac, $now); + } + } + } + + debug sprintf ' [%s] macsuck - %s forwarding table entries', + $device->ip, $total_nodes; + $device->update({last_macsuck => \$now}); +} + +=head2 store_node( $ip, $vlan, $port, $mac, $now? ) + +Writes a fresh entry to the Netdisco C database table. Will mark old +entries for this data as no longer C. + +All four fields in the tuple are required. If you don't know the VLAN ID, +Netdisco supports using ID "0". + +Optionally, a fifth argument can be the literal string passed to the time_last +field of the database record. If not provided, it defauls to C. + +=cut + +sub store_node { + my ($ip, $vlan, $port, $mac, $now) = @_; + $now ||= 'now()'; + + schema('netdisco')->txn_do(sub { + my $nodes = schema('netdisco')->resultset('Node'); + + # TODO: probably needs changing if we're to support VTP domains + my $old = $nodes->search( + { + mac => $mac, + vlan => $vlan, + -bool => 'active', + -not => { + switch => $ip, + port => $port, + }, + }); + + # lock rows, + # and get the count so we know whether to set time_recent + my $old_count = scalar $old->search(undef, + { + columns => [qw/switch vlan port mac/], + order_by => [qw/switch vlan port mac/], + for => 'update', + })->all; + + $old->update({ active => \'false' }); + + my $new = $nodes->search( + { + 'me.switch' => $ip, + 'me.port' => $port, + 'me.mac' => $mac, + }, + { + order_by => [qw/switch vlan port mac/], + for => 'update', + }); + + # lock rows + $new->search({vlan => [$vlan, 0, undef]})->first; + + # upgrade old schema + $new->search({vlan => [$vlan, 0, undef]}) + ->update({vlan => $vlan}); + + $new->update_or_create({ + vlan => $vlan, + active => \'true', + oui => substr($mac,0,8), + time_last => \$now, + ($old_count ? (time_recent => \$now) : ()), + }); + }); +} + +# return a list of vlan numbers which are OK to macsuck on this device +sub _get_vlan_list { + my ($device, $snmp) = @_; + + return () unless $snmp->cisco_comm_indexing; + + my (%vlans, %vlan_names); + my $i_vlan = $snmp->i_vlan || {}; + + # get list of vlans in use + while (my ($idx, $vlan) = each %$i_vlan) { + # hack: if vlan id comes as 1.142 instead of 142 + $vlan =~ s/^\d+\.//; + + ++$vlans{$vlan}; + } + + unless (scalar keys %vlans) { + debug sprintf ' [%s] macsuck - no VLANs found.', $device->ip; + return (); + } + + my $v_name = $snmp->v_name || {}; + + # get vlan names (required for config which filters by name) + while (my ($idx, $name) = each %$v_name) { + # hack: if vlan id comes as 1.142 instead of 142 + (my $vlan = $idx) =~ s/^\d+\.//; + + # just in case i_vlan is different to v_name set + ++$vlans{$vlan}; + + $vlan_names{$vlan} = $name; + } + + debug sprintf ' [%s] macsuck - VLANs: %s', $device->ip, + (join ',', sort keys %vlans); + + my @ok_vlans = (); + foreach my $vlan (sort keys %vlans) { + my $name = $vlan_names{$vlan} || '(unnamed)'; + + # FIXME: macsuck_no_vlan + # FIXME: macsuck_no_devicevlan + + if (setting('macsuck_no_unnamed') and $name eq '(unnamed)') { + debug sprintf + ' [%s] macsuck VLAN %s - skipped by macsuck_no_unnamed config', + $device->ip, $vlan; + next; + } + + if ($vlan == 0 or $vlan > 4094) { + debug sprintf ' [%s] macsuck - invalid VLAN number %s', + $device->ip, $vlan; + next; + } + + # check in use by a port on this device + if (scalar keys %$i_vlan and not exists $vlans{$vlan} + and not setting('macsuck_all_vlans')) { + + debug sprintf + ' [%s] macsuck VLAN %s/%s - not in use by any port - skipping.', + $device->ip, $vlan, $name; + next; + } + + push @ok_vlans, $vlan; + } + + return @ok_vlans; +} + +# walks the forwarding table (BRIDGE-MIB) for the device and returns a +# table of node entries. +sub _walk_fwtable { + my ($device, $snmp, $port_macs, $device_ports) = @_; + my $cache = {}; + + my $fw_mac = $snmp->fw_mac; + my $fw_port = $snmp->fw_port; + my $fw_vlan = $snmp->qb_fw_vlan; + my $bp_index = $snmp->bp_index; + my $interfaces = $snmp->interfaces; + + # to map forwarding table port to device port we have + # fw_port -> bp_index -> interfaces + + while (my ($idx, $mac) = each %$fw_mac) { + my $bp_id = $fw_port->{$idx}; + next unless check_mac($device, $mac, $port_macs); + + unless (defined $bp_id) { + debug sprintf + ' [%s] macsuck %s - %s has no fw_port mapping - skipping.', + $device->ip, $mac, $idx; + next; + } + + my $iid = $bp_index->{$bp_id}; + + unless (defined $iid) { + debug sprintf + ' [%s] macsuck %s - port %s has no bp_index mapping - skipping.', + $device->ip, $mac, $bp_id; + next; + } + + my $port = $interfaces->{$iid}; + + unless (defined $port) { + debug sprintf + ' [%s] macsuck %s - iid %s has no port mapping - skipping.', + $device->ip, $mac, $iid; + next; + } + + # TODO: add proper port channel support! + if ($port =~ m/port.channel/i) { + debug sprintf + ' [%s] macsuck %s - port %s is LAG member - skipping.', + $device->ip, $mac, $port; + next; + } + + # this uses the cached $ports resultset to limit hits on the db + my $device_port = $device_ports->{$port}; + + unless (defined $device_port) { + debug sprintf + ' [%s] macsuck %s - port %s is not in database - skipping.', + $device->ip, $mac, $port; + next; + } + + # check to see if the port is connected to another device + # and if we have that device in the database. + + # we have several ways to detect "uplink" port status: + # * a neighbor was discovered using CDP/LLDP + # * a mac addr is seen which belongs to any device port/interface + # * (TODO) admin sets is_uplink_admin on the device_port + + if ($device_port->is_uplink) { + if (my $neighbor = $device_port->neighbor) { + debug sprintf + ' [%s] macsuck %s - port %s has neighbor %s - skipping.', + $device->ip, $mac, $port, $neighbor->ip; + next; + } + elsif (my $remote = $device_port->remote_ip) { + debug sprintf + ' [%s] macsuck %s - port %s has undiscovered neighbor %s', + $device->ip, $mac, $port, $remote; + # continue!! + } + else { + debug sprintf + ' [%s] macsuck %s - port %s is detected uplink - skipping.', + $device->ip, $mac, $port; + next; + } + } + + if (exists $port_macs->{$mac}) { + my $switch_ip = $port_macs->{$mac}; + if ($device->ip eq $switch_ip) { + debug sprintf + ' [%s] macsuck %s - port %s connects to self - skipping.', + $device->ip, $mac, $port; + next; + } + + debug sprintf ' [%s] macsuck %s - port %s is probably an uplink', + $device->ip, $mac, $port; + $device_port->update({is_uplink => \'true'}); + + # when there's no CDP/LLDP, we only want to gather macs at the + # topology edge, hence skip ports with known device macs. + next unless setting('macsuck_bleed'); + } + + ++$cache->{$port}->{$mac}; + } + + return $cache; +} + +=head2 store_wireless_client_info( $device, $snmp, $now? ) + +Given a Device database object, and a working SNMP connection, connect to a +device and discover 802.11 related information for all connected wireless +clients. + +If the device doesn't support the 802.11 MIBs, then this will silently return. + +If the device does support the 802.11 MIBs but Netdisco's configuration +does not permit polling (C must be true) then a debug +message is logged and the subroutine returns. + +Otherwise, client information is gathered and stored to the database. + +Optionally, a third argument can be the literal string passed to the time_last +field of the database record. If not provided, it defauls to C. + +=cut + +sub store_wireless_client_info { + my ($device, $snmp, $now) = @_; + $now ||= 'now()'; + + my $cd11_txrate = $snmp->cd11_txrate; + return unless $cd11_txrate and scalar keys %$cd11_txrate; + + if (setting('store_wireless_client')) { + debug sprintf ' [%s] macsuck - gathering wireless client info', + $device->ip; + } + else { + debug sprintf ' [%s] macsuck - dot11 info available but skipped due to config', + $device->ip; + return; + } + + my $cd11_rateset = $snmp->cd11_rateset(); + my $cd11_uptime = $snmp->cd11_uptime(); + my $cd11_sigstrength = $snmp->cd11_sigstrength(); + my $cd11_sigqual = $snmp->cd11_sigqual(); + my $cd11_mac = $snmp->cd11_mac(); + my $cd11_port = $snmp->cd11_port(); + my $cd11_rxpkt = $snmp->cd11_rxpkt(); + my $cd11_txpkt = $snmp->cd11_txpkt(); + my $cd11_rxbyte = $snmp->cd11_rxbyte(); + my $cd11_txbyte = $snmp->cd11_txbyte(); + my $cd11_ssid = $snmp->cd11_ssid(); + + while (my ($idx, $txrates) = each %$cd11_txrate) { + my $rates = $cd11_rateset->{$idx}; + my $mac = $cd11_mac->{$idx}; + next unless defined $mac; # avoid null entries + # there can be more rows in txrate than other tables + + my $txrate = defined $txrates->[$#$txrates] + ? int($txrates->[$#$txrates]) + : undef; + + my $maxrate = defined $rates->[$#$rates] + ? int($rates->[$#$rates]) + : undef; + + schema('netdisco')->txn_do(sub { + schema('netdisco')->resultset('NodeWireless') + ->search({ 'me.mac' => $mac }) + ->update_or_create({ + txrate => $txrate, + maxrate => $maxrate, + uptime => $cd11_uptime->{$idx}, + rxpkt => $cd11_rxpkt->{$idx}, + txpkt => $cd11_txpkt->{$idx}, + rxbyte => $cd11_rxbyte->{$idx}, + txbyte => $cd11_txbyte->{$idx}, + sigqual => $cd11_sigqual->{$idx}, + sigstrength => $cd11_sigstrength->{$idx}, + ssid => ($cd11_ssid->{$idx} || 'unknown'), + time_last => \$now, + }, { + order_by => [qw/mac ssid/], + for => 'update', + }); + }); + } +} + +1; diff --git a/Netdisco/lib/App/Netdisco/DB.pm b/Netdisco/lib/App/Netdisco/DB.pm index db2129ad..edb43bfc 100644 --- a/Netdisco/lib/App/Netdisco/DB.pm +++ b/Netdisco/lib/App/Netdisco/DB.pm @@ -8,7 +8,7 @@ use base 'DBIx::Class::Schema'; __PACKAGE__->load_namespaces; -our $VERSION = 17; # schema version used for upgrades, keep as integer +our $VERSION = 20; # schema version used for upgrades, keep as integer use Path::Class; use File::Basename; @@ -17,7 +17,11 @@ my (undef, $libpath, undef) = fileparse( $INC{ 'App/Netdisco/DB.pm' } ); our $schema_versions_dir = Path::Class::Dir->new($libpath) ->subdir("DB", "schema_versions")->stringify; -__PACKAGE__->load_components(qw/Schema::Versioned/); +__PACKAGE__->load_components(qw/ + Schema::Versioned + +App::Netdisco::DB::ExplicitLocking +/); + __PACKAGE__->upgrade_directory($schema_versions_dir); 1; diff --git a/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm b/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm new file mode 100644 index 00000000..25fe31aa --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm @@ -0,0 +1,165 @@ +package App::Netdisco::DB::ExplicitLocking; + +use strict; +use warnings FATAL => 'all'; + +our %lock_modes; + +BEGIN { + %lock_modes = ( + ACCESS_SHARE => 'ACCESS SHARE', + ROW_SHARE => 'ROW SHARE', + ROW_EXCLUSIVE => 'ROW EXCLUSIVE', + SHARE_UPDATE_EXCLUSIVE => 'SHARE UPDATE EXCLUSIVE', + SHARE => 'SHARE', + SHARE_ROW_EXCLUSIVE => 'SHARE ROW EXCLUSIVE', + EXCLUSIVE => 'EXCLUSIVE', + ACCESS_EXCLUSIVE => 'ACCESS EXCLUSIVE', + ); +} + +use constant \%lock_modes; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = (keys %lock_modes); +our %EXPORT_TAGS = (modes => \@EXPORT_OK); + +sub txn_do_locked { + my ($self, $table, $mode, $sub) = @_; + my $sql_fmt = q{LOCK TABLE %s IN %%s MODE}; + my $schema = $self; + + if ($self->can('result_source')) { + # ResultSet component + $sub = $mode; + $mode = $table; + $table = $self->result_source->from; + $schema = $self->result_source->schema; + } + + $schema->throw_exception('missing Table name to txn_do_locked()') + unless length $table; + + $table = [$table] if ref '' eq ref $table; + my $table_fmt = join ', ', ('%s' x scalar @$table); + my $sql = sprintf $sql_fmt, $table_fmt; + + if (ref '' eq ref $mode and length $mode) { + scalar grep {$_ eq $mode} values %lock_modes + or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()'); + } + else { + $sub = $mode; + $mode = 'ACCESS EXCLUSIVE'; + } + + $schema->txn_do(sub { + my @params = map {$schema->storage->dbh->quote_identifier($_)} @$table; + $schema->storage->dbh->do(sprintf $sql, @params, $mode); + $sub->(); + }); +} + +=head1 NAME + +App::Netdisco::DB::ExplicitLocking - Support for PostgreSQL Lock Modes + +=head1 SYNOPSIS + +In your L schema: + + package My::Schema; + __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking'); + +Then, in your application code: + + use App::Netdisco::DB::ExplicitLocking ':modes'; + $schema->txn_do_locked($table, MODE_NAME, sub { ... }); + +This also works for the ResultSet: + + package My::Schema::ResultSet::TableName; + __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking'); + +Then, in your application code: + + use App::Netdisco::DB::ExplicitLocking ':modes'; + $schema->resultset('TableName')->txn_do_locked(MODE_NAME, sub { ... }); + +=head1 DESCRIPTION + +This L component provides an easy way to execute PostgreSQL table +locks before a transaction block. + +You can load the component in either the Schema class or ResultSet class (or +both) and then use an interface very similar to C's C. + +The package also exports constants for each of the table lock modes supported +by PostgreSQL, which must be used if specifying the mode (default mode is +C). + +=head1 EXPORTS + +With the C<:modes> tag (as in SYNOPSIS above) the following constants are +exported and must be used if specifying the lock mode: + +=over 4 + +=item * C + +=item * C + +=item * C + +=item * C + +=item * C + +=item * C + +=item * C + +=item * C + +=back + +=head1 METHODS + +=head2 C<< $schema->txn_do_locked($table|\@tables, MODE_NAME?, $subref) >> + +This is the method signature used when the component is loaded into your +Schema class. The reason you might want to use this over the ResultSet version +(below) is to specify multiple tables to be locked before the transaction. + +The first argument is one or more tables, and is required. Note that these are +the real table names in PostgreSQL, and not C ResultSet aliases +or anything like that. + +The mode name is optional, and defaults to C. You must use +one of the exported constants in this parameter. + +Finally pass a subroutine reference, just as you would to the normal +C C method. Note that additional arguments are not +supported. + +=head2 C<< $resultset->txn_do_locked(MODE_NAME?, $subref) >> + +This is the method signature used when the component is loaded into your +ResultSet class. If you don't yet have a ResultSet class (which is the default +- normally only Result classes are created) then you can create a stub which +simply loads this component (and inherits from C). + +This is the simplest way to use this module if you only want to lock one table +before your transaction block. + +The first argument is the optional mode name, which defaults to C. You must use one of the exported constants in this parameter. + +The second argument is a subroutine reference, just as you would pass to the +normal C C method. Note that additional arguments are +not supported. + +=cut + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Admin.pm b/Netdisco/lib/App/Netdisco/DB/Result/Admin.pm index fb98b3fa..50dd0fa5 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/Admin.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/Admin.pm @@ -55,4 +55,46 @@ __PACKAGE__->add_columns( __PACKAGE__->set_primary_key("job"); # You can replace this text with custom code or comments, and it will be preserved on regeneration + +=head1 ADDITIONAL COLUMNS + +=head2 entererd_stamp + +Formatted version of the C field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +between the date stamp and time stamp. That is: + + 2012-02-06 12:49 + +=cut + +sub entered_stamp { return (shift)->get_column('entered_stamp') } + +=head2 started_stamp + +Formatted version of the C field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +between the date stamp and time stamp. That is: + + 2012-02-06 12:49 + +=cut + +sub started_stamp { return (shift)->get_column('started_stamp') } + +=head2 finished_stamp + +Formatted version of the C field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +between the date stamp and time stamp. That is: + + 2012-02-06 12:49 + +=cut + +sub finished_stamp { return (shift)->get_column('finished_stamp') } + 1; diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Device.pm b/Netdisco/lib/App/Netdisco/DB/Result/Device.pm index 19774496..d0b81e24 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/Device.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/Device.pm @@ -180,6 +180,16 @@ __PACKAGE__->has_many( =head1 ADDITIONAL COLUMNS +=head2 port_count + +Returns the number of ports on this device. Enable this +column by applying the C modifier to C. + +=cut + +sub port_count { return (shift)->get_column('port_count') } + + =head2 uptime_age Formatted version of the C field. diff --git a/Netdisco/lib/App/Netdisco/DB/Result/DevicePort.pm b/Netdisco/lib/App/Netdisco/DB/Result/DevicePort.pm index aad723cb..85c7047e 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/DevicePort.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/DevicePort.pm @@ -7,6 +7,8 @@ package App::Netdisco::DB::Result::DevicePort; use strict; use warnings; +use MIME::Base64 'encode_base64url'; + use base 'DBIx::Class::Core'; __PACKAGE__->table("device_port"); __PACKAGE__->add_columns( @@ -51,6 +53,10 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 1 }, "remote_id", { data_type => "text", is_nullable => 1 }, + "manual_topo", + { data_type => "bool", is_nullable => 0, default_value => \"false" }, + "is_uplink", + { data_type => "bool", is_nullable => 1 }, "vlan", { data_type => "text", is_nullable => 1 }, "pvid", @@ -262,4 +268,13 @@ See the C and C modifiers to C. sub is_free { return (shift)->get_column('is_free') } +=head2 base64url_port + +Returns a Base64 encoded version of the C column value suitable for use +in a URL. + +=cut + +sub base64url_port { return encode_base64url((shift)->port) } + 1; diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Node.pm b/Netdisco/lib/App/Netdisco/DB/Result/Node.pm index 746e277e..6b22144c 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/Node.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/Node.pm @@ -44,7 +44,7 @@ __PACKAGE__->add_columns( original => { default_value => \"now()" }, }, "vlan", - { data_type => "text", is_nullable => 1, default_value => '0' }, + { data_type => "text", is_nullable => 0, default_value => '0' }, ); __PACKAGE__->set_primary_key("mac", "switch", "port", "vlan"); diff --git a/Netdisco/lib/App/Netdisco/DB/Result/NodeWireless.pm b/Netdisco/lib/App/Netdisco/DB/Result/NodeWireless.pm index dc00a0e5..ad1f5fd6 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/NodeWireless.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/NodeWireless.pm @@ -38,7 +38,7 @@ __PACKAGE__->add_columns( original => { default_value => \"now()" }, }, "ssid", - { data_type => "text", is_nullable => 1, default_value => '' }, + { data_type => "text", is_nullable => 0, default_value => '' }, ); __PACKAGE__->set_primary_key("mac", "ssid"); diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Topology.pm b/Netdisco/lib/App/Netdisco/DB/Result/Topology.pm index 8a4a8746..2e067479 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/Topology.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/Topology.pm @@ -19,4 +19,7 @@ __PACKAGE__->add_columns( { data_type => "text", is_nullable => 0 }, ); +__PACKAGE__->add_unique_constraint(['dev1','port1']); +__PACKAGE__->add_unique_constraint(['dev2','port2']); + 1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm new file mode 100644 index 00000000..21c033ab --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm @@ -0,0 +1,41 @@ +package App::Netdisco::DB::ResultSet::Admin; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings FATAL => 'all'; + +=head1 ADDITIONAL METHODS + +=head2 with_times + +This is a modifier for any C (including the helpers below) which +will add the following additional synthesized columns to the result set: + +=over 4 + +=item entered_stamp + +=item started_stamp + +=item finished_stamp + +=back + +=cut + +sub with_times { + my ($rs, $cond, $attrs) = @_; + + return $rs + ->search_rs($cond, $attrs) + ->search({}, + { + '+columns' => { + entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI')", + started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI')", + finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI')", + }, + }); +} + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm index f5e6f0f8..097fd902 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Device.pm @@ -491,4 +491,34 @@ sub get_distinct_col { )->get_column($col)->all; } +=head2 with_port_count + +This is a modifier for any C which +will add the following additional synthesized column to the result set: + +=over 4 + +=item port_count + +=back + +=cut + +sub with_port_count { + my ($rs, $cond, $attrs) = @_; + + return $rs + ->search_rs($cond, $attrs) + ->search({}, + { + '+columns' => { port_count => + $rs->result_source->schema->resultset('DevicePort') + ->search( + { 'dp.ip' => { -ident => 'me.ip' } }, + { alias => 'dp' } + )->count_rs->as_query + }, + }); +} + 1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm index 483323fa..12ad40a1 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Node.pm @@ -4,6 +4,10 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings FATAL => 'all'; +__PACKAGE__->load_components(qw/ + +App::Netdisco::DB::ExplicitLocking +/); + =head1 search_by_mac( \%cond, \%attrs? ) my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55', active => 1}); diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm index 4a652e72..2a514c48 100644 --- a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeIp.pm @@ -4,6 +4,10 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings FATAL => 'all'; +__PACKAGE__->load_components(qw/ + +App::Netdisco::DB::ExplicitLocking +/); + my $search_attr = { order_by => {'-desc' => 'time_last'}, '+columns' => [ diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm new file mode 100644 index 00000000..a86cf712 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm @@ -0,0 +1,11 @@ +package App::Netdisco::DB::ResultSet::NodeWireless; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings FATAL => 'all'; + +__PACKAGE__->load_components(qw/ + +App::Netdisco::DB::ExplicitLocking +/); + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm new file mode 100644 index 00000000..75e85a2e --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm @@ -0,0 +1,11 @@ +package App::Netdisco::DB::ResultSet::Subnet; +use base 'DBIx::Class::ResultSet'; + +use strict; +use warnings FATAL => 'all'; + +__PACKAGE__->load_components(qw/ + +App::Netdisco::DB::ExplicitLocking +/); + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-18-PostgreSQL.sql b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-18-PostgreSQL.sql new file mode 100644 index 00000000..140886bd --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-18-PostgreSQL.sql @@ -0,0 +1,10 @@ +-- Convert schema '/home/devver/netdisco-ng/Netdisco/bin/../lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-PostgreSQL.sql' to '/home/devver/netdisco-ng/Netdisco/bin/../lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-PostgreSQL.sql':; + +BEGIN; + +ALTER TABLE topology ADD CONSTRAINT topology_dev1_port1 UNIQUE (dev1, port1); + +ALTER TABLE topology ADD CONSTRAINT topology_dev2_port2 UNIQUE (dev2, port2); + +COMMIT; + diff --git a/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-19-PostgreSQL.sql b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-19-PostgreSQL.sql new file mode 100644 index 00000000..be8c3c48 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-19-PostgreSQL.sql @@ -0,0 +1,5 @@ +BEGIN; + +ALTER TABLE device_port ADD COLUMN "manual_topo" bool DEFAULT false NOT NULL; + +COMMIT; diff --git a/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-19-20-PostgreSQL.sql b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-19-20-PostgreSQL.sql new file mode 100644 index 00000000..f3666156 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-19-20-PostgreSQL.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE device_port ADD COLUMN "is_uplink" bool; +ALTER TABLE device_port ADD COLUMN "is_uplink_admin" bool; + +COMMIT; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Queue.pm b/Netdisco/lib/App/Netdisco/Daemon/Queue.pm index 7dae68f3..bbec8db0 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Queue.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Queue.pm @@ -33,7 +33,7 @@ sub capacity_for { debug "checking local capacity for action $action"; my $action_map = { - Poller => [qw/refresh discover discovernew discover_neighbors/], + Poller => [qw/discoverall discover arpwalk arpnip macwalk macsuck/], Interactive => [qw/location contact portcontrol portname vlan power/], }; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Interactive.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Interactive.pm index 37819c3b..a575e83c 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Interactive.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Interactive.pm @@ -61,7 +61,7 @@ sub close_job { try { schema('netdisco')->resultset('Admin') - ->find($job->job) + ->find($job->job, {for => 'update'}) ->update({ status => $status, log => $log, diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm index 3a6871d3..075d09c3 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Manager.pm @@ -14,7 +14,7 @@ my $fqdn = hostfqdn || 'localhost'; my $role_map = { (map {$_ => 'Poller'} - qw/refresh discover discovernew discover_neighbors/), + qw/discoverall discover arpwalk arpnip macwalk macsuck/), (map {$_ => 'Interactive'} qw/location contact portcontrol portname vlan power/) }; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm index 5b6ca4a9..3eb00346 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller.pm @@ -10,6 +10,8 @@ use namespace::clean; # add dispatch methods for poller tasks with 'App::Netdisco::Daemon::Worker::Poller::Device'; +with 'App::Netdisco::Daemon::Worker::Poller::Arpnip'; +with 'App::Netdisco::Daemon::Worker::Poller::Macsuck'; sub worker_body { my $self = shift; @@ -61,7 +63,7 @@ sub close_job { try { schema('netdisco')->resultset('Admin') - ->find($job->job) + ->find($job->job, {for => 'update'}) ->update({ status => $status, log => $log, diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm new file mode 100644 index 00000000..c9f98b7b --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm @@ -0,0 +1,71 @@ +package App::Netdisco::Daemon::Worker::Poller::Arpnip; + +use Dancer qw/:moose :syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::SNMP 'snmp_connect'; +use App::Netdisco::Util::Device 'get_device'; +use App::Netdisco::Core::Arpnip 'do_arpnip'; +use App::Netdisco::Daemon::Util ':all'; + +use NetAddr::IP::Lite ':lower'; + +use Role::Tiny; +use namespace::clean; + +# queue an arpnip job for all devices known to Netdisco +sub arpwalk { + my ($self, $job) = @_; + + my $devices = schema('netdisco')->resultset('Device')->get_column('ip'); + my $jobqueue = schema('netdisco')->resultset('Admin'); + + schema('netdisco')->txn_do(sub { + # clean up user submitted jobs older than 1min, + # assuming skew between schedulers' clocks is not greater than 1min + $jobqueue->search({ + action => 'arpnip', + status => 'queued', + entered => { '<' => \"(now() - interval '1 minute')" }, + })->delete; + + # is scuppered by any user job submitted in last 1min (bad), or + # any similar job from another scheduler (good) + $jobqueue->populate([ + map {{ + device => $_, + action => 'arpnip', + status => 'queued', + }} ($devices->all) + ]); + }); + + return job_done("Queued arpnip job for all devices"); +} + +sub arpnip { + my ($self, $job) = @_; + + my $host = NetAddr::IP::Lite->new($job->device); + my $device = get_device($host->addr); + + if ($device->in_storage + and $device->vendor and $device->vendor eq 'netdisco') { + return job_done("Skipped arpnip for pseudo-device $host"); + } + + my $snmp = snmp_connect($device); + if (!defined $snmp) { + return job_error("arpnip failed: could not SNMP connect to $host"); + } + + unless ($snmp->has_layer(3)) { + return job_done("Skipped arpnip for device $host without OSI layer 3 capability"); + } + + do_arpnip($device, $snmp); + + return job_done("Ended arpnip for ". $host->addr); +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Device.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Device.pm index d6c3dd25..93cf9105 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Device.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Device.pm @@ -5,7 +5,7 @@ use Dancer::Plugin::DBIC 'schema'; use App::Netdisco::Util::SNMP 'snmp_connect'; use App::Netdisco::Util::Device 'get_device'; -use App::Netdisco::Util::DiscoverAndStore ':all'; +use App::Netdisco::Core::Discover ':all'; use App::Netdisco::Daemon::Util ':all'; use NetAddr::IP::Lite ':lower'; @@ -14,29 +14,48 @@ use Role::Tiny; use namespace::clean; # queue a discover job for all devices known to Netdisco -sub refresh { +sub discoverall { my ($self, $job) = @_; my $devices = schema('netdisco')->resultset('Device')->get_column('ip'); + my $jobqueue = schema('netdisco')->resultset('Admin'); - schema('netdisco')->resultset('Admin')->populate([ - map {{ - device => $_, + schema('netdisco')->txn_do(sub { + # clean up user submitted jobs older than 1min, + # assuming skew between schedulers' clocks is not greater than 1min + $jobqueue->search({ action => 'discover', status => 'queued', - }} ($devices->all) - ]); + entered => { '<' => \"(now() - interval '1 minute')" }, + })->delete; + + # is scuppered by any user job submitted in last 1min (bad), or + # any similar job from another scheduler (good) + $jobqueue->populate([ + map {{ + device => $_, + action => 'discover', + status => 'queued', + }} ($devices->all) + ]); + }); return job_done("Queued discover job for all devices"); } +# queue a discover job for one device, and its *new* neighbors sub discover { my ($self, $job) = @_; my $host = NetAddr::IP::Lite->new($job->device); my $device = get_device($host->addr); - my $snmp = snmp_connect($device); + if ($device->in_storage + and $device->vendor and $device->vendor eq 'netdisco') { + return job_done("Skipped discover for pseudo-device $host"); + } + + my $snmp = snmp_connect($device); if (!defined $snmp) { return job_error("discover failed: could not SNMP connect to $host"); } @@ -47,42 +66,9 @@ sub discover { store_vlans($device, $snmp); store_power($device, $snmp); store_modules($device, $snmp); + discover_new_neighbors($device, $snmp); - return job_done("Ended discover for $host"); -} - -# run find_neighbors on all known devices, and run discover on any -# newly found devices. -sub discovernew { - my ($self, $job) = @_; - - my $devices = schema('netdisco')->resultset('Device')->get_column('ip'); - - schema('netdisco')->resultset('Admin')->populate([ - map {{ - device => $_, - action => 'discover_neighbors', - status => 'queued', - }} ($devices->all) - ]); - - return job_done("Queued discover_neighbors job for all devices"); -} - -sub discover_neighbors { - my ($self, $job) = @_; - - my $host = NetAddr::IP::Lite->new($job->device); - my $device = get_device($host->addr); - my $snmp = snmp_connect($device); - - if (!defined $snmp) { - return job_error("discover_neighbors failed: could not SNMP connect to $host"); - } - - find_neighbors($device, $snmp); - - return job_done("Ended find_neighbors for $host"); + return job_done("Ended discover for ". $host->addr); } 1; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm new file mode 100644 index 00000000..39c3a4ee --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm @@ -0,0 +1,71 @@ +package App::Netdisco::Daemon::Worker::Poller::Macsuck; + +use Dancer qw/:moose :syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::SNMP 'snmp_connect'; +use App::Netdisco::Util::Device 'get_device'; +use App::Netdisco::Core::Macsuck ':all'; +use App::Netdisco::Daemon::Util ':all'; + +use NetAddr::IP::Lite ':lower'; + +use Role::Tiny; +use namespace::clean; + +# queue a macsuck job for all devices known to Netdisco +sub macwalk { + my ($self, $job) = @_; + + my $devices = schema('netdisco')->resultset('Device')->get_column('ip'); + my $jobqueue = schema('netdisco')->resultset('Admin'); + + schema('netdisco')->txn_do(sub { + # clean up user submitted jobs older than 1min, + # assuming skew between schedulers' clocks is not greater than 1min + $jobqueue->search({ + action => 'macsuck', + status => 'queued', + entered => { '<' => \"(now() - interval '1 minute')" }, + })->delete; + + # is scuppered by any user job submitted in last 1min (bad), or + # any similar job from another scheduler (good) + $jobqueue->populate([ + map {{ + device => $_, + action => 'macsuck', + status => 'queued', + }} ($devices->all) + ]); + }); + + return job_done("Queued macsuck job for all devices"); +} + +sub macsuck { + my ($self, $job) = @_; + + my $host = NetAddr::IP::Lite->new($job->device); + my $device = get_device($host->addr); + + if ($device->in_storage + and $device->vendor and $device->vendor eq 'netdisco') { + return job_done("Skipped macsuck for pseudo-device $host"); + } + + my $snmp = snmp_connect($device); + if (!defined $snmp) { + return job_error("macsuck failed: could not SNMP connect to $host"); + } + + unless ($snmp->has_layer(2)) { + return job_done("Skipped macsuck for device $host without OSI layer 2 capability"); + } + + do_macsuck($device, $snmp); + + return job_done("Ended macsuck for ". $host->addr); +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Daemon/Worker/Scheduler.pm b/Netdisco/lib/App/Netdisco/Daemon/Worker/Scheduler.pm index f7ad185b..b827ed83 100644 --- a/Netdisco/lib/App/Netdisco/Daemon/Worker/Scheduler.pm +++ b/Netdisco/lib/App/Netdisco/Daemon/Worker/Scheduler.pm @@ -11,12 +11,11 @@ use namespace::clean; my $jobactions = { map {$_ => undef} qw/ - refresh - discovernew + discoverall + arpwalk + macwalk / # saveconfigs -# macwalk -# arpwalk # nbtwalk # backup }; diff --git a/Netdisco/lib/App/Netdisco/Manual/Configuration.pod b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod new file mode 100644 index 00000000..76f13309 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Manual/Configuration.pod @@ -0,0 +1,142 @@ +=head1 NAME + +App::Netdisco::Manual::Configuration - How to Configure Netdisco + +=head1 INTRODUCTION + +The configuration files for Netdisco come with all options set to sensible +default values, and just a few that you must initially set yourself. + +However as you use the system over time, there are many situations where you +might want to tune the behaviour of Netdisco, and for that we have a lot of +configuration settings available. + +=head2 GUIDANCE + +There are two configuration files: C (which lives inside Netdisco) +and C (which usually lives in C<${HOME}/environments>). + +The C file includes defaults for every setting, and should be left +alone. Any time you want to set an option, use only the C +file. The two are merged when Netdisco starts, with your settings in +C overriding the defaults from C. + +The configuration file format for Netdisco is YAML. This is easy for humans to +edit, but you should take care over whitespace and avoid TAB characters. YAML +supports several data types: + +=over 4 + +=item * + +Boolean - True/False value, using C<1> and C<0> or C and C +respectively + +=item * + +List - Set of things using C<[a, b, c]> on one line or C<-> on separate lines + +=item * + +Dictionary - Key/Value pairs (like Perl Hash) using C<{key1: val1, key2, +val2}> on one line or C on separate lines + +=item * + +String - Quoted, just like in Perl (and essential if the item contains the +colon character) + +=back + +=head1 SUPPORTED SETTINGS + +=head2 Essential Settings + +If you followed the installation instructions, then you should have set the +database connection parameters to match those of your local system. That is, +the C (DB name, host, port), C and C. + +=head2 General Settings + +=head3 C + +Default: C + +The log level used by Netdisco. It's useful to see warning messages from the +backend poller, as this can highlight broken topology. + +=head3 C + +Default: C + +Destination for log messages. Console means standard ouput. When set to +C, the default destination is the C<${HOME}/logs> directory. + +=head3 C + +Default: C<< '[%P] %L @%D> %m' >> + +Structure of the log messages. See L +for details. + +=head2 Web Frontend + +=head3 C + +Default: None + +Set this to your local site's domain name. This is usually removed from node +names in the web interface to make things more readable. + +=head3 C + +Default: C + +Enable this to disable login authentication in the web frontend. The username +will be set to C so if you want to allow extended permissions (C +or C, create a dummy user with the appropriate flag, in the +database: + + netdisco=> insert into users (username, port_control) values ('guest', true); + +=head3 C + +Default: C<5000> + +Port which the web server listens on. Netdisco comes with a good pre-forking +web server, so you can change this to C<80> if you want to use it directly. +However the default is designed to work well with servers such as Apache in +reverse-proxy mode. + +=head3 C + +Default: List of L names + +Netdisco's plugin system allows the user more control over the user interface. +Plugins can be distributed independently from Netdisco and are a better +alternative to source code patches. This setting is the list of Plugins which +are used in the default Netdisco distribution. + +You can override this to set your own list. If you only want to add to the +default list then use C, which allows the Netdisco +developers to update C in a future release. + +=head3 C + +Default: None + +List of additional L names to load. See also the +C setting. + +=head2 Netdisco Core + +=head2 Backend Daemon + +=head2 Dancer Internal + +=head1 UNSUPPORTED SETTINGS + +These settings are from Netdisco 1.x but are yet to be supported in Netdisco +2. If you really need the feature, please let the developers know. + +=cut diff --git a/Netdisco/lib/App/Netdisco/Manual/Deployment.pod b/Netdisco/lib/App/Netdisco/Manual/Deployment.pod index 878651e5..1da7b8a6 100644 --- a/Netdisco/lib/App/Netdisco/Manual/Deployment.pod +++ b/Netdisco/lib/App/Netdisco/Manual/Deployment.pod @@ -22,6 +22,11 @@ parameter to the web startup script: ~/bin/netdisco-web --path /netdisco2 +Alternatively, can set the C configuration option in your +C file: + + path: '/netdisco2' + =head1 Behind a Proxy By default the web application daemon starts listening on port 5000 and goes @@ -39,8 +44,13 @@ configuration would be: Allow from all +You also need to set the following configuration in your C +file: + + behind_proxy: 1 + To combine this with Non-root Hosting as above, simply change the paths -referenced in the configuration like so (and use C<--path> option): +referenced in the configuration like so (and use Non-root Hosting as above): ProxyPass /netdisco2 http://localhost:5000/ ProxyPassReverse /netdisco2 http://localhost:5000/ diff --git a/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod b/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod index 23908a02..67a18357 100644 --- a/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod +++ b/Netdisco/lib/App/Netdisco/Manual/WritingPlugins.pod @@ -175,6 +175,24 @@ any query parameters which might customize the report search. See the L module for a simple example of how to implement the handler. +=head1 Admin Tasks + +These components appear in the black navigation bar under an Admin menu, but only +if the logged in user has Administrator rights in Netdisco. + +To register an item for display in the Admin menu, use the following code: + + register_admin_task({ + tag => 'newfeature', + label => 'My New Feature', + }); + +This causes an item to appear in the Admin menu with a visible text of "My New +Feature" which when clicked sends the user to the C page. +Note that this won't work for any target link - the path must be an +App::Netdisco Dancer route handler. Please bug the App::Netdisco devs if you +want arbitrary links supported. + =head1 Templates All of Netdisco's web page templates are stashed away in its distribution, diff --git a/Netdisco/lib/App/Netdisco/Util/PortMAC.pm b/Netdisco/lib/App/Netdisco/Util/PortMAC.pm new file mode 100644 index 00000000..4ec33a53 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Util/PortMAC.pm @@ -0,0 +1,56 @@ +package App::Netdisco::Util::PortMAC; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/ get_port_macs /; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Util::PortMAC + +=head1 DESCRIPTION + +Helper subroutine to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 get_port_macs( $device ) + +Returns a Hash reference of C<< { MAC => IP } >> for all interface MAC +addresses on a device. + +=cut + +sub get_port_macs { + my $device = shift; + my $port_macs = {}; + + unless ($device->in_storage) { + debug sprintf ' [%s] get_port_macs - skipping device not yet discovered', + $device->ip; + return $port_macs; + } + + my $dp_macs = schema('netdisco')->resultset('DevicePort') + ->search({ mac => { '!=' => undef} }); + while (my $r = $dp_macs->next) { + $port_macs->{ $r->mac } = $r->ip; + } + + my $d_macs = schema('netdisco')->resultset('Device') + ->search({ mac => { '!=' => undef} }); + while (my $r = $d_macs->next) { + $port_macs->{ $r->mac } = $r->ip; + } + + return $port_macs; +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Util/SNMP.pm b/Netdisco/lib/App/Netdisco/Util/SNMP.pm index ee294a65..a2703288 100644 --- a/Netdisco/lib/App/Netdisco/Util/SNMP.pm +++ b/Netdisco/lib/App/Netdisco/Util/SNMP.pm @@ -10,7 +10,7 @@ use Path::Class 'dir'; use base 'Exporter'; our @EXPORT = (); our @EXPORT_OK = qw/ - snmp_connect snmp_connect_rw + snmp_connect snmp_connect_rw snmp_comm_reindex /; our %EXPORT_TAGS = (all => \@EXPORT_OK); @@ -88,8 +88,8 @@ sub _snmp_connect_generic { my $comm_type = pop; my @communities = @{ setting($comm_type) || []}; unshift @communities, $device->snmp_comm - if length $device->snmp_comm - and length $comm_type and $comm_type eq 'community'; + if defined $device->snmp_comm + and defined $comm_type and $comm_type eq 'community'; my $info = undef; VERSION: foreach my $ver (@versions) { @@ -123,7 +123,7 @@ sub _try_connect { $info = $class->new(%$snmp_args, Version => $ver, Community => $comm); undef $info unless ( (not defined $info->error) - and length $info->uptime + and defined $info->uptime and ($info->layers or $info->description) and $info->class ); @@ -149,7 +149,35 @@ sub _try_connect { sub _build_mibdirs { my $home = (setting('mibhome') || $ENV{NETDISCO_HOME} || $ENV{HOME}); return map { dir($home, $_) } - @{ setting('mibdirs') || [] }; + @{ setting('mibdirs') || _get_mibdirs_content($home) }; +} + +sub _get_mibdirs_content { + my $home = shift; + warning 'Netdisco SNMP work will be really slow - loading ALL MIBs. Please set mibdirs.'; + my @list = map {s|$home/||; $_} grep {-d} glob("$home/*"); + return \@list; +} + +=head2 snmp_comm_reindex( $snmp, $vlan ) + +Takes an established L instance and makes a fresh connection using +community indexing, with the given C<$vlan> ID. Works for all SNMP versions. + +=cut + +sub snmp_comm_reindex { + my ($snmp, $vlan) = @_; + + my $ver = $snmp->snmp_ver; + my $comm = $snmp->snmp_comm; + + if ($ver == 3) { + $snmp->update(Context => "vlan-$vlan"); + } + else { + $snmp->update(Community => $comm . '@' . $vlan); + } } 1; diff --git a/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm b/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm new file mode 100644 index 00000000..43a3fe5a --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Util/SanityCheck.pm @@ -0,0 +1,115 @@ +package App::Netdisco::Util::SanityCheck; + +use Dancer qw/:syntax :script/; +use Dancer::Plugin::DBIC 'schema'; + +use App::Netdisco::Util::PortMAC ':all'; +use Net::MAC; + +use base 'Exporter'; +our @EXPORT = (); +our @EXPORT_OK = qw/ check_mac /; +our %EXPORT_TAGS = (all => \@EXPORT_OK); + +=head1 NAME + +App::Netdisco::Util::SanityCheck + +=head1 DESCRIPTION + +Helper subroutines to support parts of the Netdisco application. + +There are no default exports, however the C<:all> tag will export all +subroutines. + +=head1 EXPORT_OK + +=head2 check_mac( $device, $node, $port_macs? ) + +Given a Device database object and a MAC address, perform various sanity +checks which need to be done before writing an ARP/Neighbor entry to the +database storage. + +Returns false, and might log a debug level message, if the checks fail. + +Returns a true value if these checks pass: + +=over 4 + +=item * + +MAC address is well-formed (according to common formats) + +=item * + +MAC address is not all-zero, broadcast, CLIP, VRRP or HSRP + +=item * + +MAC address does not belong to an interface on any known Device + +=back + +Optionally pass a cached set of Device port MAC addresses as the third +argument, or else C will retrieve this for itself from the +database. + +=cut + +sub check_mac { + my ($device, $node, $port_macs) = @_; + $port_macs ||= get_port_macs($device); + my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); + + # incomplete MAC addresses (BayRS frame relay DLCI, etc) + if ($mac->get_error) { + debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping', + $device->ip, $node; + return 0; + } + else { + # lower case, hex, colon delimited, 8-bit groups + $node = lc $mac->as_IEEE; + } + + # broadcast MAC addresses + return 0 if $node eq 'ff:ff:ff:ff:ff:ff'; + + # all-zero MAC addresses + return 0 if $node eq '00:00:00:00:00:00'; + + # CLIP + return 0 if $node eq '00:00:00:00:00:01'; + + # multicast + if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) { + debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # VRRP + if (index($node, '00:00:5e:00:01:') == 0) { + debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # HSRP + if (index($node, '00:00:0c:07:ac:') == 0) { + debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping', + $device->ip, $node; + return 0; + } + + # device's own MACs + if (exists $port_macs->{$node}) { + debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping', + $device->ip, $node; + return 0; + } + + return 1; +} + +1; diff --git a/Netdisco/lib/App/Netdisco/Web.pm b/Netdisco/lib/App/Netdisco/Web.pm index 797b7283..8781cb08 100644 --- a/Netdisco/lib/App/Netdisco/Web.pm +++ b/Netdisco/lib/App/Netdisco/Web.pm @@ -8,11 +8,14 @@ use Dancer::Plugin::DBIC; use Socket6 (); # to ensure dependency is met use HTML::Entities (); # to ensure dependency is met use URI::QueryParam (); # part of URI, to add helper methods +use Path::Class 'dir'; use App::Netdisco::Web::AuthN; +use App::Netdisco::Web::Static; use App::Netdisco::Web::Search; use App::Netdisco::Web::Device; use App::Netdisco::Web::Report; +use App::Netdisco::Web::AdminTask; use App::Netdisco::Web::TypeAhead; use App::Netdisco::Web::PortControl; @@ -20,8 +23,9 @@ sub _load_web_plugins { my $plugin_list = shift; foreach my $plugin (@$plugin_list) { + $plugin =~ s/^X::/+App::NetdiscoX::Web::Plugin::/; $plugin = 'App::Netdisco::Web::Plugin::'. $plugin - unless $plugin =~ m/^\+/; + if $plugin !~ m/^\+/; $plugin =~ s/^\+//; debug "loading Netdisco plugin $plugin"; @@ -35,6 +39,7 @@ if (setting('web_plugins') and ref [] eq ref setting('web_plugins')) { } if (setting('extra_web_plugins') and ref [] eq ref setting('extra_web_plugins')) { + unshift @INC, dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'site_plugins')->stringify; _load_web_plugins( setting('extra_web_plugins') ); } @@ -50,9 +55,18 @@ hook 'before_template' => sub { # allow very long lists of ports $Template::Directive::WHILE_MAX = 10_000; + + # allow hash keys with leading underscores + $Template::Stash::PRIVATE = undef; }; get '/' => sub { + if (var('user') and var('user')->admin) { + if (schema('netdisco')->resultset('Device')->count == 0) { + var('nodevices' => true); + } + } + template 'index'; }; diff --git a/Netdisco/lib/App/Netdisco/Web/AdminTask.pm b/Netdisco/lib/App/Netdisco/Web/AdminTask.pm new file mode 100644 index 00000000..a7e9211e --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/AdminTask.pm @@ -0,0 +1,80 @@ +package App::Netdisco::Web::AdminTask; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use Try::Tiny; + +sub add_job { + my ($jobtype, $device) = @_; + + if ($device) { + $device = NetAddr::IP::Lite->new($device); + return send_error('Bad device', 400) + if ! $device or $device->addr eq '0.0.0.0'; + } + + try { + # job might already be in the queue, so this could die + schema('netdisco')->resultset('Admin')->create({ + ($device ? (device => $device->addr) : ()), + action => $jobtype, + status => 'queued', + username => session('user'), + userip => request->remote_address, + }); + }; +} + +# we have a separate list for jobs needing a device to avoid queueing +# such a job when there's no device param (it could still be duff, tho). +my %jobs = map { $_ => 1} qw/ + discover + macsuck + arpnip +/; +my %jobs_all = map {$_ => 1} qw/ + discoverall + macwalk + arpwalk +/; + +foreach my $jobtype (keys %jobs_all, keys %jobs) { + ajax "/ajax/control/admin/$jobtype" => sub { + send_error('Forbidden', 403) + unless var('user')->admin; + send_error('Missing device', 400) + if exists $jobs{$jobtype} and not param('device'); + + add_job($jobtype, param('device')); + }; + + post "/admin/$jobtype" => sub { + send_error('Forbidden', 403) + unless var('user')->admin; + send_error('Missing device', 400) + if exists $jobs{$jobtype} and not param('device'); + + add_job($jobtype, param('device')); + redirect uri_for('/admin/jobqueue')->path_query; + }; +} + +get '/admin/*' => sub { + my ($tag) = splat; + + if (! eval { var('user')->admin }) { + return redirect uri_for('/')->path_query; + } + + # trick the ajax into working as if this were a tabbed page + params->{tab} = $tag; + + var(nav => 'admin'); + template 'admintask', { + task => setting('_admin_tasks')->{ $tag }, + }; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/AuthN.pm b/Netdisco/lib/App/Netdisco/Web/AuthN.pm index e051071e..12afd6ef 100644 --- a/Netdisco/lib/App/Netdisco/Web/AuthN.pm +++ b/Netdisco/lib/App/Netdisco/Web/AuthN.pm @@ -18,30 +18,31 @@ hook 'before' => sub { if (session('user') && session->id) { var(user => schema('netdisco')->resultset('User') ->find(session('user'))); + + # really just for dev work, to quieten the logs + var('user')->port_control(0) if setting('no_port_control'); } }; post '/login' => sub { - status(302); - if (param('username') and param('password')) { my $user = schema('netdisco')->resultset('User')->find(param('username')); + if ($user) { my $sum = Digest::MD5::md5_hex(param('password')); if (($sum and $user->password) and ($sum eq $user->password)) { session(user => $user->username); - header(Location => uri_for('/inventory')->path_query()); - return; + return redirect uri_for('/inventory')->path_query; } } } - header(Location => uri_for('/', {failed => 1})->path_query()); + + redirect uri_for('/', {failed => 1})->path_query; }; get '/logout' => sub { session->destroy; - status(302); - header(Location => uri_for('/', {logout => 1})->path_query()); + redirect uri_for('/', {logout => 1})->path_query; }; true; diff --git a/Netdisco/lib/App/Netdisco/Web/Device.pm b/Netdisco/lib/App/Netdisco/Web/Device.pm index 7f2a5c66..dbd93bed 100644 --- a/Netdisco/lib/App/Netdisco/Web/Device.pm +++ b/Netdisco/lib/App/Netdisco/Web/Device.pm @@ -5,10 +5,12 @@ use Dancer::Plugin::Ajax; use Dancer::Plugin::DBIC; hook 'before' => sub { - # list of port detail columns - var('port_columns' => [ - { name => 'c_admin', label => 'Admin Controls', default => '' }, + my @default_port_columns_left = ( + { name => 'c_admin', label => 'Port Controls', default => '' }, { name => 'c_port', label => 'Port', default => 'on' }, + ); + + my @default_port_columns_right = ( { name => 'c_descr', label => 'Description', default => '' }, { name => 'c_type', label => 'Type', default => '' }, { name => 'c_duplex', label => 'Duplex', default => '' }, @@ -24,7 +26,21 @@ hook 'before' => sub { { name => 'c_neighbors', label => 'Connected Devices', default => 'on' }, { name => 'c_stp', label => 'Spanning Tree', default => '' }, { name => 'c_up', label => 'Status', default => '' }, - ]); + ); + + # build list of port detail columns + my @port_columns = (); + + push @port_columns, + grep {$_->{position} eq 'left'} @{ setting('_extra_device_port_cols') }; + push @port_columns, @default_port_columns_left; + push @port_columns, + grep {$_->{position} eq 'mid'} @{ setting('_extra_device_port_cols') }; + push @port_columns, @default_port_columns_right; + push @port_columns, + grep {$_->{position} eq 'right'} @{ setting('_extra_device_port_cols') }; + + var('port_columns' => \@port_columns); # view settings for port connected devices var('connected_properties' => [ @@ -100,9 +116,7 @@ get '/device' => sub { }); if (!defined $dev) { - status(302); - header(Location => uri_for('/', {nosuchdevice => 1})->path_query()); - return; + return redirect uri_for('/', {nosuchdevice => 1})->path_query; } params->{'tab'} ||= 'details'; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin.pm b/Netdisco/lib/App/Netdisco/Web/Plugin.pm index 49c7e6a6..29dc5ba2 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin.pm @@ -4,12 +4,16 @@ use Dancer ':syntax'; use Dancer::Plugin; set( - 'navbar_items' => [], - 'search_tabs' => [], - 'device_tabs' => [], - 'reports_menu' => {}, - 'reports' => {}, - 'report_order' => [qw/Device Port Node VLAN Network Wireless/], + '_additional_css' => [], + '_additional_javascript' => [], + '_extra_device_port_cols' => [], + '_navbar_items' => [], + '_search_tabs' => [], + '_device_tabs' => [], + '_admin_tasks' => {}, + '_reports_menu' => {}, + '_reports' => {}, + '_report_order' => [qw/Device Port Node VLAN Network Wireless/], ); # this is what Dancer::Template::TemplateToolkit does by default @@ -19,8 +23,7 @@ register 'register_template_path' => sub { my ($self, $path) = plugin_args(@_); if (!length $path) { - error "bad template path to register_template_paths"; - return; + return error "bad template path to register_template_paths"; } unshift @@ -28,6 +31,49 @@ register 'register_template_path' => sub { $path; }; +sub _register_include { + my ($type, $plugin) = @_; + + if (!length $type) { + return error "bad type to _register_include"; + } + + if (!length $plugin) { + return error "bad plugin name to register_$type"; + } + + push @{ setting("_additional_$type") }, $plugin; +} + +register 'register_css' => sub { + my ($self, $plugin) = plugin_args(@_); + _register_include('css', $plugin); +}; + +register 'register_javascript' => sub { + my ($self, $plugin) = plugin_args(@_); + _register_include('javascript', $plugin); +}; + +register 'register_device_port_column' => sub { + my ($self, $config) = plugin_args(@_); + $config->{default} ||= ''; + $config->{position} ||= 'right'; + + if (!length $config->{name} or !length $config->{label}) { + return error "bad config to register_device_port_column"; + } + + foreach my $item (@{ setting('_extra_device_port_cols') }) { + if ($item->{name} eq $config->{name}) { + $item = $config; + return; + } + } + + push @{ setting('_extra_device_port_cols') }, $config; +}; + register 'register_navbar_item' => sub { my ($self, $config) = plugin_args(@_); @@ -35,29 +81,39 @@ register 'register_navbar_item' => sub { or !length $config->{path} or !length $config->{label}) { - error "bad config to register_navbar_item"; - return; + return error "bad config to register_navbar_item"; } - foreach my $item (@{ setting('navbar_items') }) { + foreach my $item (@{ setting('_navbar_items') }) { if ($item->{tag} eq $config->{tag}) { $item = $config; return; } } - push @{ setting('navbar_items') }, $config; + push @{ setting('_navbar_items') }, $config; }; -sub _register_tab { - my ($nav, $config) = @_; - my $stash = setting("${nav}_tabs"); +register 'register_admin_task' => sub { + my ($self, $config) = plugin_args(@_); if (!length $config->{tag} or !length $config->{label}) { - error "bad config to register_${nav}_item"; - return; + return error "bad config to register_admin_task"; + } + + setting('_admin_tasks')->{ $config->{tag} } = $config; +}; + +sub _register_tab { + my ($nav, $config) = @_; + my $stash = setting("_${nav}_tabs"); + + if (!length $config->{tag} + or !length $config->{label}) { + + return error "bad config to register_${nav}_item"; } foreach my $item (@{ $stash }) { @@ -82,26 +138,25 @@ register 'register_device_tab' => sub { register 'register_report' => sub { my ($self, $config) = plugin_args(@_); - my @categories = @{ setting('report_order') }; + my @categories = @{ setting('_report_order') }; if (!length $config->{category} or !length $config->{tag} or !length $config->{label} or 0 == scalar grep {$config->{category} eq $_} @categories) { - error "bad config to register_report"; - return; + return error "bad config to register_report"; } - foreach my $item (@{setting('reports_menu')->{ $config->{category} }}) { + foreach my $item (@{setting('_reports_menu')->{ $config->{category} }}) { if ($item eq $config->{tag}) { - setting('reports')->{$config->{tag}} = $config; + setting('_reports')->{$config->{tag}} = $config; return; } } - push @{setting('reports_menu')->{ $config->{category} }}, $config->{tag}; - setting('reports')->{$config->{tag}} = $config; + push @{setting('_reports_menu')->{ $config->{category} }}, $config->{tag}; + setting('_reports')->{$config->{tag}} = $config; }; register_plugin; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm new file mode 100644 index 00000000..c932c55c --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm @@ -0,0 +1,37 @@ +package App::Netdisco::Web::Plugin::AdminTask::JobQueue; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; + +register_admin_task({ + tag => 'jobqueue', + label => 'Job Queue', +}); + +ajax '/ajax/control/admin/jobqueue/del' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + send_error('Missing job', 400) unless length param('job'); + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Admin') + ->search({job => param('job')})->delete; + }); +}; + +ajax '/ajax/content/admin/jobqueue' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + + my $set = schema('netdisco')->resultset('Admin') + ->with_times + ->search({}, {order_by => { -desc => [qw/entered device action/] }}); + + content_type('text/html'); + template 'ajax/admintask/jobqueue.tt', { + results => $set, + }, { layout => undef }; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm new file mode 100644 index 00000000..6a4d9300 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm @@ -0,0 +1,103 @@ +package App::Netdisco::Web::Plugin::AdminTask::PseudoDevice; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; +use NetAddr::IP::Lite ':lower'; + +register_admin_task({ + tag => 'pseudodevice', + label => 'Pseudo Devices', +}); + +sub _sanity_ok { + return 0 unless var('user') and var('user')->admin; + + return 0 unless length param('dns') + and param('dns') =~ m/^[[:print:]]+$/ + and param('dns') !~ m/[[:space:]]/; + + my $ip = NetAddr::IP::Lite->new(param('ip')); + return 0 unless ($ip and$ip->addr ne '0.0.0.0'); + + return 0 unless length param('ports') + and param('ports') =~ m/^[[:digit:]]+$/; + + return 1; +} + +ajax '/ajax/control/admin/pseudodevice/add' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Device') + ->create({ + ip => param('ip'), + dns => param('dns'), + vendor => 'netdisco', + last_discover => \'now()', + }); + return unless $device; + + $device->ports->populate([ + ['port'], + map {["Port$_"]} @{[1 .. param('ports')]}, + ]); + }); +}; + +ajax '/ajax/control/admin/pseudodevice/del' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Device') + ->find({ip => param('ip')}); + + $device->ports->delete; + $device->delete; + }); +}; + +ajax '/ajax/control/admin/pseudodevice/update' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Device') + ->with_port_count->find({ip => param('ip')}); + return unless $device; + my $count = $device->port_count; + + if (param('ports') > $count) { + my $start = $count + 1; + $device->ports->populate([ + ['port'], + map {["Port$_"]} @{[$start .. param('ports')]}, + ]); + } + elsif (param('ports') < $count) { + my $start = param('ports') + 1; + $device->ports + ->single({port => "Port$_"})->delete + for ($start .. $count); + } + }); +}; + +ajax '/ajax/content/admin/pseudodevice' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + + my $set = schema('netdisco')->resultset('Device') + ->search( + {vendor => 'netdisco'}, + {order_by => { -desc => 'last_discover' }}, + )->with_port_count; + + content_type('text/html'); + template 'ajax/admintask/pseudodevice.tt', { + results => $set, + }, { layout => undef }; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm new file mode 100644 index 00000000..c0dbd2ac --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm @@ -0,0 +1,103 @@ +package App::Netdisco::Web::Plugin::AdminTask::Topology; + +use Dancer ':syntax'; +use Dancer::Plugin::Ajax; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; +use NetAddr::IP::Lite ':lower'; + +register_admin_task({ + tag => 'topology', + label => 'Manual Device Topology', +}); + +sub _sanity_ok { + return 0 unless var('user') and var('user')->admin; + + my $dev1 = NetAddr::IP::Lite->new(param('dev1')); + return 0 unless ($dev1 and $dev1->addr ne '0.0.0.0'); + + my $dev2 = NetAddr::IP::Lite->new(param('dev2')); + return 0 unless ($dev2 and $dev2->addr ne '0.0.0.0'); + + return 0 unless length param('port1'); + return 0 unless length param('port2'); + + return 1; +} + +ajax '/ajax/control/admin/topology/add' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + my $device = schema('netdisco')->resultset('Topology') + ->create({ + dev1 => param('dev1'), + port1 => param('port1'), + dev2 => param('dev2'), + port2 => param('port2'), + }); + + # re-set remote device details in affected ports + # could fail for bad device or port names + try { + schema('netdisco')->txn_do(sub { + # only work on root_ips + my $left = get_device(param('dev1')); + my $right = get_device(param('dev2')); + + # skip bad entries + return unless ($left->in_storage and $right->in_storage); + + $left->ports + ->single({port => param('port1')}, {for => 'update'}) + ->update({ + remote_ip => param('dev2'), + remote_port => param('port2'), + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + + $right->ports + ->single({port => param('port2')}, {for => 'update'}) + ->update({ + remote_ip => param('dev1'), + remote_port => param('port1'), + remote_type => undef, + remote_id => undef, + is_uplink => \"true", + manual_topo => \"true", + }); + }); + }; +}; + +ajax '/ajax/control/admin/topology/del' => sub { + send_error('Bad Request', 400) unless _sanity_ok(); + + schema('netdisco')->txn_do(sub { + my $device = schema('netdisco')->resultset('Topology') + ->search({ + dev1 => param('dev1'), + port1 => param('port1'), + dev2 => param('dev2'), + port2 => param('port2'), + })->delete; + }); +}; + +ajax '/ajax/content/admin/topology' => sub { + send_error('Forbidden', 403) unless var('user')->admin; + + my $set = schema('netdisco')->resultset('Topology') + ->search({},{order_by => [qw/dev1 dev2 port1/]}); + + content_type('text/html'); + template 'ajax/admintask/topology.tt', { + results => $set, + }, { layout => undef }; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Addresses.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Addresses.pm index ad55025b..66013836 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Addresses.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Addresses.pm @@ -13,7 +13,7 @@ ajax '/ajax/content/device/addresses' => sub { my $q = param('q'); my $device = schema('netdisco')->resultset('Device') - ->search_for_device($q) or return; + ->search_for_device($q) or send_error('Bad device', 400); my $set = $device->device_ips->search({}, {order_by => 'alias'}); return unless $set->count; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Details.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Details.pm index 6e6d30a4..e40113fb 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Details.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Details.pm @@ -12,7 +12,7 @@ register_device_tab({ tag => 'details', label => 'Details' }); ajax '/ajax/content/device/details' => sub { my $q = param('q'); my $device = schema('netdisco')->resultset('Device') - ->with_times()->search_for_device($q) or return; + ->with_times()->search_for_device($q) or send_error('Bad device', 400); content_type('text/html'); template 'ajax/device/details.tt', { diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm index 106919e8..e43fb1da 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm @@ -43,7 +43,7 @@ get '/ajax/data/device/netmap' => sub { my $q = param('q'); my $device = schema('netdisco')->resultset('Device') - ->search_for_device($q) or return; + ->search_for_device($q) or send_error('Bad device', 400); my $start = $device->ip; my @devices = schema('netdisco')->resultset('Device')->search({}, { @@ -72,7 +72,7 @@ get '/ajax/data/device/netmap' => sub { _add_children($tree{children}, var('links')->{$start}); content_type('application/json'); - return to_json(\%tree); + to_json(\%tree); }; ajax '/ajax/data/device/alldevicelinks' => sub { @@ -93,7 +93,7 @@ ajax '/ajax/data/device/alldevicelinks' => sub { } content_type('application/json'); - return to_json(\%tree); + to_json(\%tree); }; true; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Ports.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Ports.pm index a083be99..6163b941 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Ports.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Device/Ports.pm @@ -14,7 +14,7 @@ ajax '/ajax/content/device/ports' => sub { my $q = param('q'); my $device = schema('netdisco')->resultset('Device') - ->search_for_device($q) or return; + ->search_for_device($q) or send_error('Bad device', 400); my $set = $device->ports; # refine by ports if requested @@ -77,7 +77,7 @@ ajax '/ajax/content/device/ports' => sub { template 'ajax/device/ports.tt', { results => $results, nodes => $nodes_name, - device => $device->ip, + device => $device, }, { layout => undef }; }; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Device.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Device.pm index 47cfdae4..58c933a1 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Device.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Device.pm @@ -21,7 +21,7 @@ ajax '/ajax/content/search/device' => sub { } else { my $q = param('q'); - return unless $q; + send_error('Missing query', 400) unless $q; $set = schema('netdisco')->resultset('Device')->search_fuzzy($q); } diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm index beaec028..ace6ecbb 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm @@ -14,7 +14,7 @@ register_search_tab({ tag => 'node', label => 'Node' }); # nodes matching the param as an IP or DNS hostname or MAC ajax '/ajax/content/search/node' => sub { my $node = param('q'); - return unless $node; + send_error('Missing node', 400) unless $node; content_type('text/html'); my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Port.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Port.pm index 047ce6ce..2415d359 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Port.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Port.pm @@ -11,7 +11,7 @@ register_search_tab({ tag => 'port', label => 'Port' }); # device ports with a description (er, name) matching ajax '/ajax/content/search/port' => sub { my $q = param('q'); - return unless $q; + send_error('Missing query', 400) unless $q; my $set; if ($q =~ m/^\d+$/) { diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/VLAN.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/VLAN.pm index 46b8ba44..f204c4e5 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/VLAN.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/VLAN.pm @@ -11,7 +11,7 @@ register_search_tab({ tag => 'vlan', label => 'VLAN' }); # devices carrying vlan xxx ajax '/ajax/content/search/vlan' => sub { my $q = param('q'); - return unless $q; + send_error('Missing query', 400) unless $q; my $set; if ($q =~ m/^\d+$/) { diff --git a/Netdisco/lib/App/Netdisco/Web/PortControl.pm b/Netdisco/lib/App/Netdisco/Web/PortControl.pm index ef126dd1..d2defc0a 100644 --- a/Netdisco/lib/App/Netdisco/Web/PortControl.pm +++ b/Netdisco/lib/App/Netdisco/Web/PortControl.pm @@ -4,42 +4,43 @@ use Dancer ':syntax'; use Dancer::Plugin::Ajax; use Dancer::Plugin::DBIC; -use Try::Tiny; - ajax '/ajax/portcontrol' => sub { - try { - my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]', - param('device'), (param('port') || ''), param('field'), - (param('action') || ''), (param('value') || ''); + send_error('Forbidden', 403) + unless var('user')->port_control; + send_error('No device/port/field', 400) + unless param('device') and param('port') and param('field'); - my %action_map = ( - 'location' => 'location', - 'contact' => 'contact', - 'c_port' => 'portcontrol', - 'c_name' => 'portname', - 'c_vlan' => 'vlan', - 'c_power' => 'power', - ); + my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]', + param('device'), (param('port') || ''), param('field'), + (param('action') || ''), (param('value') || ''); - my $action = $action_map{ param('field') }; - my $subaction = ($action =~ m/^(?:power|portcontrol)/ - ? (param('action') ."-other") - : param('value')); + my %action_map = ( + 'location' => 'location', + 'contact' => 'contact', + 'c_port' => 'portcontrol', + 'c_name' => 'portname', + 'c_vlan' => 'vlan', + 'c_power' => 'power', + ); - schema('netdisco')->resultset('Admin')->create({ - device => param('device'), - port => param('port'), - action => $action, - subaction => $subaction, - status => 'queued', - username => session('user'), - userip => request->remote_address, - log => $log, - }); - } - catch { - send_error('Failed to parse params or add DB record'); - }; + send_error('No action/value', 400) + unless (param('action') or param('value')); + + my $action = $action_map{ param('field') }; + my $subaction = ($action =~ m/^(?:power|portcontrol)/ + ? (param('action') ."-other") + : param('value')); + + schema('netdisco')->resultset('Admin')->create({ + device => param('device'), + port => param('port'), + action => $action, + subaction => $subaction, + status => 'queued', + username => session('user'), + userip => request->remote_address, + log => $log, + }); content_type('application/json'); to_json({}); @@ -47,7 +48,7 @@ ajax '/ajax/portcontrol' => sub { ajax '/ajax/userlog' => sub { my $user = session('user'); - send_error('No username') unless $user; + send_error('No username', 400) unless $user; my $rs = schema('netdisco')->resultset('Admin')->search({ username => $user, diff --git a/Netdisco/lib/App/Netdisco/Web/Report.pm b/Netdisco/lib/App/Netdisco/Web/Report.pm index fba23e90..e9ab7bb4 100644 --- a/Netdisco/lib/App/Netdisco/Web/Report.pm +++ b/Netdisco/lib/App/Netdisco/Web/Report.pm @@ -10,7 +10,7 @@ get '/report/*' => sub { var(nav => 'reports'); template 'report', { - report => setting('reports')->{ $tag }, + report => setting('_reports')->{ $tag }, }; }; diff --git a/Netdisco/lib/App/Netdisco/Web/Search.pm b/Netdisco/lib/App/Netdisco/Web/Search.pm index 939e071b..b623ab0c 100644 --- a/Netdisco/lib/App/Netdisco/Web/Search.pm +++ b/Netdisco/lib/App/Netdisco/Web/Search.pm @@ -65,9 +65,7 @@ get '/search' => sub { if (not param('tab')) { if (not $q) { - status(302); - header(Location => uri_for('/')->path_query()); - return; + return redirect uri_for('/')->path_query; } # pick most likely tab for initial results @@ -80,13 +78,11 @@ get '/search' => sub { if ($nd and $nd->count) { if ($nd->count == 1) { # redirect to device details for the one device - status(302); - header(Location => uri_for('/device', { + return redirect uri_for('/device', { tab => 'details', q => ($nd->first->dns || $nd->first->ip), f => '', - })->path_query()); - return; + })->path_query; } # multiple devices diff --git a/Netdisco/lib/App/Netdisco/Web/Static.pm b/Netdisco/lib/App/Netdisco/Web/Static.pm new file mode 100644 index 00000000..0522386e --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Static.pm @@ -0,0 +1,30 @@ +package App::Netdisco::Web::Static; + +use Dancer ':syntax'; +use Path::Class; + +get '/plugin/*/*.js' => sub { + my ($plugin) = splat; + + my $content = template + "plugin/$plugin/$plugin.js", {}, + { layout => undef }; + + send_file \$content, + content_type => 'application/javascript', + filename => "$plugin.js"; +}; + +get '/plugin/*/*.css' => sub { + my ($plugin) = splat; + + my $content = template + "plugin/$plugin/$plugin.css", {}, + { layout => undef }; + + send_file \$content, + content_type => 'text/css', + filename => "$plugin.css"; +}; + +true; diff --git a/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm b/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm index 8be780e0..38033796 100644 --- a/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm +++ b/Netdisco/lib/App/Netdisco/Web/TypeAhead.pm @@ -4,13 +4,51 @@ use Dancer ':syntax'; use Dancer::Plugin::Ajax; use Dancer::Plugin::DBIC; -# support typeahead with simple AJAX query for device names -ajax '/ajax/data/device/typeahead' => sub { - my $q = param('query'); +use App::Netdisco::Util::Web (); # for sort_port + +ajax '/ajax/data/devicename/typeahead' => sub { + my $q = param('query') || param('term'); my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q); content_type 'application/json'; - return to_json [map {$_->dns || $_->name || $_->ip} $set->all]; + to_json [map {$_->dns || $_->name || $_->ip} $set->all]; +}; + +ajax '/ajax/data/deviceip/typeahead' => sub { + my $q = param('query') || param('term'); + my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q); + + my @data = (); + while (my $d = $set->next) { + my $label = $d->ip; + if ($d->dns or $d->name) { + $label = sprintf '%s (%s)', + ($d->dns || $d->name), $d->ip; + } + push @data, {label => $label, value => $d->ip}; + } + + content_type 'application/json'; + to_json \@data; +}; + +ajax '/ajax/data/port/typeahead' => sub { + my $dev = param('dev1') || param('dev2'); + my $port = param('port1') || param('port2'); + send_error('Missing device', 400) unless length $dev; + + my $device = schema('netdisco')->resultset('Device') + ->find({ip => $dev}); + send_error('Bad device', 400) unless $device; + + my $set = $device->ports({},{order_by => 'port'}); + $set = $set->search({port => { -ilike => "\%$port\%" }}) + if length $port; + + my $results = [ sort { &App::Netdisco::Util::Web::sort_port($a->port, $b->port) } $set->all ]; + + content_type 'application/json'; + to_json [map {$_->port} @$results]; }; true; diff --git a/Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm b/Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm new file mode 100644 index 00000000..3409d9c2 --- /dev/null +++ b/Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm @@ -0,0 +1,21 @@ +package App::NetdiscoX::Web::Plugin::Observium; + +use Dancer ':syntax'; +use Dancer::Plugin::DBIC; + +use App::Netdisco::Web::Plugin; + +use File::ShareDir 'dist_dir'; +use Path::Class; + +register_device_port_column({ + name => 'observium', + position => 'mid', + label => 'Traffic', + default => 'on', +}); + +register_css('observium'); +register_javascript('observium'); + +true; diff --git a/Netdisco/share/config.yml b/Netdisco/share/config.yml index e8335960..c0290720 100644 --- a/Netdisco/share/config.yml +++ b/Netdisco/share/config.yml @@ -38,6 +38,9 @@ engines: web_plugins: - Inventory - Report::DuplexMismatch + - AdminTask::PseudoDevice + - AdminTask::Topology + - AdminTask::JobQueue - Search::Device - Search::Node - Search::VLAN diff --git a/Netdisco/share/public/css/jquery.qtip.min.css b/Netdisco/share/public/css/jquery.qtip.min.css new file mode 100644 index 00000000..62d3a19c --- /dev/null +++ b/Netdisco/share/public/css/jquery.qtip.min.css @@ -0,0 +1,2 @@ +/* qTip2 v2.0.1-105 basic css3 | qtip2.com | Licensed MIT, GPL | Sun Jun 02 2013 13:17:39 */ +.qtip{position:absolute;left:-28000px;top:-28000px;display:none;max-width:280px;min-width:50px;font-size:10.5px;line-height:12px;direction:ltr;box-shadow:none;padding:0}.qtip-content{position:relative;padding:5px 9px;overflow:hidden;text-align:left;word-wrap:break-word}.qtip-titlebar{position:relative;padding:5px 35px 5px 10px;overflow:hidden;border-width:0 0 1px;font-weight:700}.qtip-titlebar+.qtip-content{border-top-width:0!important}.qtip-close{position:absolute;right:-9px;top:-9px;cursor:pointer;outline:medium none;border-width:1px;border-style:solid;border-color:transparent}.qtip-titlebar .qtip-close{right:4px;top:50%;margin-top:-9px}* html .qtip-titlebar .qtip-close{top:16px}.qtip-titlebar .ui-icon,.qtip-icon .ui-icon{display:block;text-indent:-1000em;direction:ltr}.qtip-icon,.qtip-icon .ui-icon{-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;text-decoration:none}.qtip-icon .ui-icon{width:18px;height:14px;line-height:14px;text-align:center;text-indent:0;font:400 bold 10px/13px Tahoma,sans-serif;color:inherit;background:transparent none no-repeat -100em -100em}.qtip-focus{}.qtip-hover{}.qtip-default{border-width:1px;border-style:solid;border-color:#F1D031;background-color:#FFFFA3;color:#555}.qtip-default .qtip-titlebar{background-color:#FFEF93}.qtip-default .qtip-icon{border-color:#CCC;background:#F1F1F1;color:#777}.qtip-default .qtip-titlebar .qtip-close{border-color:#AAA;color:#111} .qtip-light{background-color:#fff;border-color:#E2E2E2;color:#454545}.qtip-light .qtip-titlebar{background-color:#f1f1f1} .qtip-dark{background-color:#505050;border-color:#303030;color:#f3f3f3}.qtip-dark .qtip-titlebar{background-color:#404040}.qtip-dark .qtip-icon{border-color:#444}.qtip-dark .qtip-titlebar .ui-state-hover{border-color:#303030} .qtip-cream{background-color:#FBF7AA;border-color:#F9E98E;color:#A27D35}.qtip-cream .qtip-titlebar{background-color:#F0DE7D}.qtip-cream .qtip-close .qtip-icon{background-position:-82px 0} .qtip-red{background-color:#F78B83;border-color:#D95252;color:#912323}.qtip-red .qtip-titlebar{background-color:#F06D65}.qtip-red .qtip-close .qtip-icon{background-position:-102px 0}.qtip-red .qtip-icon{border-color:#D95252}.qtip-red .qtip-titlebar .ui-state-hover{border-color:#D95252} .qtip-green{background-color:#CAED9E;border-color:#90D93F;color:#3F6219}.qtip-green .qtip-titlebar{background-color:#B0DE78}.qtip-green .qtip-close .qtip-icon{background-position:-42px 0} .qtip-blue{background-color:#E5F6FE;border-color:#ADD9ED;color:#5E99BD}.qtip-blue .qtip-titlebar{background-color:#D0E9F5}.qtip-blue .qtip-close .qtip-icon{background-position:-2px 0}.qtip-shadow{-webkit-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);-moz-box-shadow:1px 1px 3px 1px rgba(0,0,0,.15);box-shadow:1px 1px 3px 1px rgba(0,0,0,.15)}.qtip-rounded,.qtip-tipsy,.qtip-bootstrap{-moz-border-radius:5px;-webkit-border-radius:5px;border-radius:5px}.qtip-rounded .qtip-titlebar{-moz-border-radius:4px 4px 0 0;-webkit-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0}.qtip-youtube{-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 3px #333;-moz-box-shadow:0 0 3px #333;box-shadow:0 0 3px #333;color:#fff;border-width:0;background:#4A4A4A;background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,#000));background-image:-webkit-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-moz-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-ms-linear-gradient(top,#4A4A4A 0,#000 100%);background-image:-o-linear-gradient(top,#4A4A4A 0,#000 100%)}.qtip-youtube .qtip-titlebar{background-color:#4A4A4A;background-color:rgba(0,0,0,0)}.qtip-youtube .qtip-content{padding:.75em;font:12px arial,sans-serif;filter:progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);-ms-filter:"progid:DXImageTransform.Microsoft.Gradient(GradientType=0, StartColorStr=#4a4a4a, EndColorStr=#000000);"}.qtip-youtube .qtip-icon{border-color:#222}.qtip-youtube .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-jtools{background:#232323;background:rgba(0,0,0,.7);background-image:-webkit-gradient(linear,left top,left bottom,from(#717171),to(#232323));background-image:-moz-linear-gradient(top,#717171,#232323);background-image:-webkit-linear-gradient(top,#717171,#232323);background-image:-ms-linear-gradient(top,#717171,#232323);background-image:-o-linear-gradient(top,#717171,#232323);border:2px solid #ddd;border:2px solid rgba(241,241,241,1);-moz-border-radius:2px;-webkit-border-radius:2px;border-radius:2px;-webkit-box-shadow:0 0 12px #333;-moz-box-shadow:0 0 12px #333;box-shadow:0 0 12px #333}.qtip-jtools .qtip-titlebar{background-color:transparent;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171, endColorstr=#4A4A4A)"}.qtip-jtools .qtip-content{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A, endColorstr=#232323)"}.qtip-jtools .qtip-titlebar,.qtip-jtools .qtip-content{background:transparent;color:#fff;border:0 dashed transparent}.qtip-jtools .qtip-icon{border-color:#555}.qtip-jtools .qtip-titlebar .ui-state-hover{border-color:#333}.qtip-cluetip{-webkit-box-shadow:4px 4px 5px rgba(0,0,0,.4);-moz-box-shadow:4px 4px 5px rgba(0,0,0,.4);box-shadow:4px 4px 5px rgba(0,0,0,.4);background-color:#D9D9C2;color:#111;border:0 dashed transparent}.qtip-cluetip .qtip-titlebar{background-color:#87876A;color:#fff;border:0 dashed transparent}.qtip-cluetip .qtip-icon{border-color:#808064}.qtip-cluetip .qtip-titlebar .ui-state-hover{border-color:#696952;color:#696952}.qtip-tipsy{background:#000;background:rgba(0,0,0,.87);color:#fff;border:0 solid transparent;font-size:11px;font-family:'Lucida Grande',sans-serif;font-weight:700;line-height:16px;text-shadow:0 1px #000}.qtip-tipsy .qtip-titlebar{padding:6px 35px 0 10px;background-color:transparent}.qtip-tipsy .qtip-content{padding:6px 10px}.qtip-tipsy .qtip-icon{border-color:#222;text-shadow:none}.qtip-tipsy .qtip-titlebar .ui-state-hover{border-color:#303030}.qtip-tipped{border:3px solid #959FA9;-moz-border-radius:3px;-webkit-border-radius:3px;border-radius:3px;background-color:#F9F9F9;color:#454545;font-weight:400;font-family:serif}.qtip-tipped .qtip-titlebar{border-bottom-width:0;color:#fff;background:#3A79B8;background-image:-webkit-gradient(linear,left top,left bottom,from(#3A79B8),to(#2E629D));background-image:-webkit-linear-gradient(top,#3A79B8,#2E629D);background-image:-moz-linear-gradient(top,#3A79B8,#2E629D);background-image:-ms-linear-gradient(top,#3A79B8,#2E629D);background-image:-o-linear-gradient(top,#3A79B8,#2E629D);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D);-ms-filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8, endColorstr=#2E629D)"}.qtip-tipped .qtip-icon{border:2px solid #285589;background:#285589}.qtip-tipped .qtip-icon .ui-icon{background-color:#FBFBFB;color:#555}.qtip-bootstrap{font-size:14px;line-height:20px;color:#333;padding:1px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.qtip-bootstrap .qtip-titlebar{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0}.qtip-bootstrap .qtip-titlebar .qtip-close{right:11px;top:45%;border-style:none}.qtip-bootstrap .qtip-content{padding:9px 14px}.qtip-bootstrap .qtip-icon{background:transparent}.qtip-bootstrap .qtip-icon .ui-icon{width:auto;height:auto;float:right;font-size:20px;font-weight:700;line-height:18px;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.qtip-bootstrap .qtip-icon .ui-icon:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4;filter:alpha(opacity=40)}.qtip:not(.ie9haxors) div.qtip-content,.qtip:not(.ie9haxors) div.qtip-titlebar{filter:none;-ms-filter:none}.qtip .qtip-tip{margin:0 auto;overflow:hidden;z-index:10}x:-o-prefocus,.qtip .qtip-tip{visibility:hidden}.qtip .qtip-tip,.qtip .qtip-tip .qtip-vml,.qtip .qtip-tip canvas{position:absolute;color:#123456;background:transparent;border:0 dashed transparent}.qtip .qtip-tip canvas{top:0;left:0}.qtip .qtip-tip .qtip-vml{behavior:url(#default#VML);display:inline-block;visibility:visible}#qtip-overlay{position:fixed;left:-10000em;top:-10000em}#qtip-overlay.blurs{cursor:pointer}#qtip-overlay div{position:absolute;left:0;top:0;width:100%;height:100%;background-color:#000;opacity:.7;filter:alpha(opacity=70);-ms-filter:"alpha(Opacity=70)"}.qtipmodal-ie6fix{position:absolute!important} \ No newline at end of file diff --git a/Netdisco/share/public/css/nd_print.css b/Netdisco/share/public/css/nd_print.css index 5b7f23c4..0944b8e5 100644 --- a/Netdisco/share/public/css/nd_print.css +++ b/Netdisco/share/public/css/nd_print.css @@ -2,11 +2,11 @@ body { padding-top: 0px !important; } -#search_results > li:not(.active) { +#nd_search-results > li:not(.active) { display: none !important; } -.navbar, .sidebar { +.navbar, .nd_sidebar { display: none !important; } diff --git a/Netdisco/share/public/css/netdisco.css b/Netdisco/share/public/css/netdisco.css index dd486147..cc685b54 100644 --- a/Netdisco/share/public/css/netdisco.css +++ b/Netdisco/share/public/css/netdisco.css @@ -1,18 +1,19 @@ /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* for the fixed navbar make sure content stops short of page top*/ +/* style common to all pages in the site */ +/* for the fixed navbar make sure content stops short of page top*/ body { padding-top: 50px; } /* magnifying glass icon for search box */ -.navbar_icon { +.nd_navbar-icon { vertical-align: sub; cursor: pointer; } /* for the "logged in as..." text */ -.nd_navbartext { +.nd_navbar-text { color: #666; padding-top: 11px; } @@ -27,45 +28,36 @@ body { width: 100%; } -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* various styles to adjust the hero box used for homepage + login */ +/* jquery ui autocomplete scrollable */ +.ui-autocomplete { + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; +} -.nd_herorow { +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +/* styles to adjust the hero box used for homepage + login */ + +/* space between hero box and navbar */ +.nd_hero-row { margin-top: 50px; } +/* alter proportions of hero unit to make it "tighter" on content */ .hero-unit { padding: 30px 60px 40px 90px; } -.nd_loginform { +/* push user/pass/login form down+away from the Netdisco banner text */ +.nd_login-form { margin-top: 15px; margin-bottom: 0px; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* styles for Reports */ +/* styles for device inventory */ -/* from Bootstrap doc style sheet */ -.nd_show-grid [class*="span"] { - background-color: cornsilk; - text-align: center; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - min-height: 30px; - line-height: 30px; -} - -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* styles for Inventory */ - -#nd_dev_age_form { - margin-top: 10px; - margin-bottom: 12px; -} - -.nd_inv_tbl_head { +.nd_inventory-table-head { text-align: center; color: lightSlateGray; margin-top: 6px; @@ -73,12 +65,7 @@ body { } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* results table links */ - -.nd_stealthlink { - text-decoration: none !important; - color: #404040; -} +/* styles for links in results tables */ /* make the whole cell become a hyperlink in results table */ .nd_linkcell { @@ -87,110 +74,65 @@ body { height: 100%; } -/* special placing for edit icon in details tab */ -.nd_device_details_edit { - float: right !important; - font-size: 14px; +/* still a link, but styled like normal text */ +.nd_stealth-link { + text-decoration: none !important; + color: #404040; } -/* port admin up/down control */ -.nd_edit_icon, .nd_hand_icon { - cursor: pointer; - float: left; - display: none; -} - -.nd_power_icon { - cursor: pointer; -} - -.icon-off { - vertical-align: middle; - color: darkRed; -} - -.nd_power_on { - color: darkGreen; -} - -/* placement of port link when port admin hint is enabled */ -.nd_editable_cell > .nd_this_port_only { +/* nudge cell content to the right when port_control controls are enabled */ +.nd_editable-cell > .nd_this-port-only { margin-left: 18px; } -.nd_editable_cell > .nd_editable_cell_content { +.nd_editable-cell > .nd_editable-cell-content { margin-left: 18px; } - -/* style of editable content in table */ -[contenteditable]:focus { - background: #FFFFD3 !important; +.table .nd_nudge-for-icon { + padding-left: 25px; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* many styles for the collapsing lists */ +/* styles to position table cell content */ -/* mouse-over should be pointer to show JS collapser is clickable */ -.nd_collapser { - cursor: pointer; - color: #0088CC; -} - -/* collapser label should not have any decoration even though it's clickable */ -.clearfix > a { - text-decoration: none !important; -} - -/* collapser label should not have any decoration even though it's clickable */ -.nd_collapse_vlans { - text-decoration: none !important; - cursor: pointer; - color: #0088CC; -} - -/* class to control default state of collapsible lists on page load */ -.nd_collapse_pre_hidden { - display: none; -} - -/* for the tagged vlans total when hiding the full list */ -.vlan_total { - float: right; -} - -/* little up/down chevron to the right of some collapser link */ -.arrow-up-down { - float: right; - margin-top: 1px; - margin-right: 1px; - color: #555; -} - -/* draw little up arrow to the left of a label for collapsed list */ -.cell-arrow-up-down { - float: left; - margin-right: 6px; - color: #555; -} - -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* for table and to position cell content */ - -td { +.table td { vertical-align: baseline; } -.center_cell { +.table .nd_center-cell { text-align: center; } -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* tabs */ +/* fix layout of form fields inside the (topology) table */ +td div.input-append { + margin-bottom: 0px; +} -#search_results { +/* admin buttons in the device details view */ +td > form.nd_inline-form { + margin-bottom: 2px; +} + +/* fix layout of form fields inside the (pseudo devices) table */ +.nd_center-cell input { + margin-bottom: 0px; +} + +/* with two forms inside one cell, make the submit buttons side-by-side */ +.nd_inline-form { + display: inline; +} + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +/* styles for "tabs" and surrounding content */ + + +/* add a small bottom margin (gutter) below all pages */ +#nd_search-results { margin-bottom: 10px; } -#nd_device_name { +/* for any label which we want to appear alongside tabs, floated to the right */ +#nd_device-name { float: right; margin-bottom: 0px; margin-top: 9px; @@ -198,159 +140,131 @@ td { color: #6D5720; } -/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* style customization for many items which appear in the sidebar */ +/* when there's only one tab (report, task etc) change the text color */ +.nd_single-tab { + color: rgb(187,112,0) !important; +} -/* fixups for prepended checkbox in sidebar */ -.nd_searchcheckbox { - width: 123px; - padding-left: 8px; +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +/* style for port_control controls */ + +/* edit icon in details tab is in the label (not content) cell so nudge to RHS*/ +.nd_device-details-edit { + float: right !important; + font-size: 14px; +} + +/* port admin up/down control */ +.nd_edit-icon, .nd_hand-icon { + cursor: pointer; + float: left; + display: none; + margin-top: 3px; +} + +/* port power control */ +.nd_power-icon { cursor: pointer; } -/* for some reason bootstrap 2.1.0 displays add-on as block - no check supprt? */ -.nd_checkboxlabel { - display: inline; +/* the port power icon, whether it's on or off */ +.icon-off { + vertical-align: middle; + color: darkRed; } -/* fixups for placing the Archived "A" inside the prepended checkbox */ -.nd_legendlabel { +/* change color of icon from default of red (which is OK for power-off) */ +.nd_power-on { + color: darkGreen; +} + +/* style of editable content in any table - yellow background */ +[contenteditable]:focus { + background: #FFFFD3 !important; +} + +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +/* styles for collapsing lists - sidebar or main table cell content */ + +/* sidebar collapser is clickable and deep grey */ +.nd_collapser { + cursor: pointer; + color: #0088CC; +} + +/* vlans collapser also clickable and deep grey but with no link styling */ +.nd_collapse-vlans { + cursor: pointer; + color: #0088CC; + text-decoration: none !important; +} + +/* set default state of collapsible lists as collapsed (hidden) */ +.nd_collapse-pre-hidden { + display: none; +} + +/* for the tagged vlans total when hiding the full list */ +.nd_vlan-total { float: right; - line-height: 1.2; } -.nd_side_input { - margin-left: -3px; - width: 152px; +/* little up/down chevron to the right of some collapsed list */ +.nd_arrow-up-down-right { + float: right; + margin-top: 1px; + margin-right: 1px; + color: #555; } -.nd_side_select { - margin-left: -3px; - width: 165px; +/* little up arrow to the left of a label for collapsed list */ +.nd_arrow-up-down-left { + float: left; + margin-right: 6px; + color: #555; } -.sidebar .input-prepend { - margin-left: -2px; +/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ +/* styles for sidebar placement and sizing */ + +/* make the sidebar fixed on the screen */ +.container-fluid > .nd_sidebar { + position: absolute; + right: 20px; + width: 200px; + left: auto; +} + +/* nudge content in the sidebar closer to the left */ +.nd_sidebar-form { + padding-left: 0px; + margin-top: -9px; margin-bottom: 0px; } -/* nudge the port name/vlan filter over a little */ -#nd_port_query { - margin-left: 5px !important; - width: 152px; +/* reduce padding at the bottom of the sidebar content */ +.container-fluid > .nd_sidebar > .well { + padding-bottom: 15px; } -/* somewhere between span1 and span2 is desirable */ -#nd_days_select { - margin-top: 4px; - width: 56px; -} - -/* set the day/mon/year drop-down width */ -#nd_age_select { - margin-top: 4px; - width: 95px; -} - -/* set the MAC format drop-down width */ -#nd_mac_format { - margin-top: 4px; - width: 154px; -} - -/* set the MAC format drop-down width */ -#nd_node_mac_format { - margin-left: -2px; - margin-top: 4px; - width: 165px; -} - -/* sidebar submit button width and spacing from Node Props */ -.sidebar button { - margin-top: 9px; - margin-left: -3px; - width: 165px; -} -.sidebar #ports_submit { - margin-top: 9px; - width: 165px; -} - -/* little icon inside of search input fields */ -.field_clear_icon, .field_copy_icon { - position: absolute; - margin-left: 140px; - margin-top: 5px; - z-index: 1; - padding: 0px; - cursor: pointer; -} - -.field_copy_icon { - color: #999; -} - -.field_clear_icon { - background-color: #A9DBA9; - color: #3A87AD; -} - - /* for the ports form, but the positioning is slightly different */ -#ports_form .field_clear_icon { - margin-left: 149px; - margin-top: 5px; -} - -/* change highlighting for form fields which are being used in a search */ -form .clearfix.success select { - background-color: #A9DBA9; -} -form .clearfix.success input { - background-color: #A9DBA9; -} - -/* when we use font-awesome icons, override the size */ -#nd_legend i { - width: 9px; -} -.table-bordered i { - width: 9px; -} - -/* bring sidebar items closer together */ -.inputs-list label { - margin-bottom: 1px; -} - -.inputs-list i { - margin-right: 5px; - margin-left: 2px; -} - -/* nudge content closer to the header labels in the sidebar */ -.inputs-list li:first-child { - padding-top: 3px !important; +/* pull tab content away from the sidebar */ +.container-fluid > .content { + margin-right: 215px; + margin-left: 0px; } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* sidebar collapser */ +/* styles for sidebar position controls (collapse, pin) */ -.nd_sidebar_title { - margin-left: 10px; - margin-top: 6px; - margin-bottom: 12px; -} - -.sidebar_pinned { +.nd_sidebar-pinned { position: fixed !important; } -.sidebar_pin_clicked { +.nd_sidebar-pin-clicked { color: rgba(255,0,0,0.8) !important; } -/* for placing the sidebar pin icons */ -.sidebar_pin { +.nd_sidebar-pin { float: left; margin-top: 6px; margin-left: -16px; @@ -359,8 +273,7 @@ form .clearfix.success input { cursor: pointer; } -/* for placing the sidebar toggle icons */ -#sidebar_toggle_img_in { +#nd_sidebar-toggle-img-in { float: left; margin-top: -9px; margin-left: -16px; @@ -369,8 +282,7 @@ form .clearfix.success input { cursor: pointer; } -/* for placing the sidebar toggle icons */ -#sidebar_toggle_img_out { +#nd_sidebar-toggle-img-out { position: fixed; top: 60px; right: 7px; @@ -381,7 +293,7 @@ form .clearfix.success input { } /* question mark image with popover for netmap instructions */ -#netmap_help_img { +#nd_netmap-help { position: fixed; top: 160px; right: 7px; @@ -393,34 +305,147 @@ form .clearfix.success input { } /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ -/* sidebar placement and sizing */ +/* style customization for many items which appear in the sidebar */ -/* make the sidebar fixed on the screen */ -.container-fluid > .sidebar { - position: absolute; - right: 20px; - width: 200px; - left: auto; +.nd_sidebar-title { + margin-left: 10px; + margin-top: 6px; + margin-bottom: 12px; } -/* smaller padding below form button in sidebar well */ -.container-fluid > .sidebar > .well { - padding-bottom: 15px; +/* fixup for prepended checkbox in sidebar */ +.nd_searchcheckbox { + width: 123px; + padding-left: 8px; + cursor: pointer; } -/* make the content start more to the left now the sidebar is narrower */ -.container-fluid > .content { - margin-right: 215px; - margin-left: 0px; -} - -/* nudge content in the sidebar closer to the left */ -.nd_sidesearchform { - padding-left: 0px; - margin-top: -9px; +/* fixup for prepended checkbox in sidebar */ +.nd_sidebar .input-prepend { + margin-left: -2px; margin-bottom: 0px; } +/* for some reason bootstrap 2.1.0 displays add-on as block - no check supprt? */ +.nd_checkboxlabel { + display: inline; +} + +/* fixup for placing the Archived "A" inside the prepended checkbox */ +.nd_legendlabel { + float: right; + line-height: 1.2; +} + +/* placement of form field in sidebar */ +.nd_side-input { + margin-left: -3px; + width: 152px; +} + +/* placement of form field in sidebar */ +.nd_side-select { + margin-left: -3px; + width: 165px; +} + +/* nudge the port name/vlan filter over a little (as compared to nd_side-select) */ +#nd_port-query { + margin-left: 5px !important; + width: 152px; +} + +/* set the day/mon/year drop-down width */ +#nd_days-select { + margin-top: 4px; + width: 56px; +} + +/* set the day/mon/year drop-down width */ +#nd_age-select { + margin-top: 4px; + width: 95px; +} + +/* set the MAC format drop-down width */ +#nd_mac-format { + margin-top: 4px; + width: 154px; +} + +/* set the MAC format drop-down width */ +#nd_node-mac-format { + margin-left: -2px; + margin-top: 4px; + width: 165px; +} + +/* sidebar submit button width and spacing */ +.nd_sidebar button { + margin-top: 9px; + margin-left: -3px; + width: 165px; +} + +/* little icon inside of search input fields */ +.nd_field-clear-icon, .nd_field-copy-icon { + position: absolute; + margin-left: 140px; + margin-top: 5px; + z-index: 1; + padding: 0px; + cursor: pointer; +} + +/* little icon inside of search input fields */ +.nd_field-copy-icon { + color: #999; +} + +/* little icon inside of search input fields */ +.nd_field-clear-icon { + background-color: #A9DBA9; + color: #3A87AD; +} + + /* same for the ports form, but the positioning is slightly different */ +#ports_form .nd_field-clear-icon { + margin-left: 149px; + margin-top: 5px; +} + +/* change bg color for form fields which are being used in a search */ +form .clearfix.success select { + background-color: #A9DBA9; +} +form .clearfix.success input { + background-color: #A9DBA9; +} + +/* when we use font-awesome icons, override the size */ +#nd_legend i { + width: 9px; +} +.table i { + width: 9px; +} + +/* bring sidebar items closer together */ +.nd_inputs-list label { + margin-bottom: 1px; +} + +/* compact icons for the sidebar legend */ +.nd_inputs-list i { + margin-right: 5px; + margin-left: 2px; +} + +/* nudge content closer to the header labels in the sidebar */ +.nd_inputs-list li:first-child { + padding-top: 3px !important; +} + /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ /* D3 SVG */ diff --git a/Netdisco/share/public/css/smoothness/images/animated-overlay.gif b/Netdisco/share/public/css/smoothness/images/animated-overlay.gif new file mode 100644 index 00000000..d441f75e Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/animated-overlay.gif differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png b/Netdisco/share/public/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png new file mode 100644 index 00000000..277ea338 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png b/Netdisco/share/public/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png new file mode 100644 index 00000000..f094d9aa Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png new file mode 100644 index 00000000..2e28622a Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png new file mode 100644 index 00000000..0cf3ed0c Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png new file mode 100644 index 00000000..68160b98 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png new file mode 100644 index 00000000..21f39d18 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png new file mode 100644 index 00000000..b2c983cc Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/Netdisco/share/public/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png new file mode 100644 index 00000000..fe074dd4 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-icons_222222_256x240.png b/Netdisco/share/public/css/smoothness/images/ui-icons_222222_256x240.png new file mode 100644 index 00000000..c1cb1170 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-icons_222222_256x240.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-icons_2e83ff_256x240.png b/Netdisco/share/public/css/smoothness/images/ui-icons_2e83ff_256x240.png new file mode 100644 index 00000000..84b601bf Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-icons_2e83ff_256x240.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-icons_454545_256x240.png b/Netdisco/share/public/css/smoothness/images/ui-icons_454545_256x240.png new file mode 100644 index 00000000..b6db1acd Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-icons_454545_256x240.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-icons_888888_256x240.png b/Netdisco/share/public/css/smoothness/images/ui-icons_888888_256x240.png new file mode 100644 index 00000000..feea0e20 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-icons_888888_256x240.png differ diff --git a/Netdisco/share/public/css/smoothness/images/ui-icons_cd0a0a_256x240.png b/Netdisco/share/public/css/smoothness/images/ui-icons_cd0a0a_256x240.png new file mode 100644 index 00000000..ed5b6b09 Binary files /dev/null and b/Netdisco/share/public/css/smoothness/images/ui-icons_cd0a0a_256x240.png differ diff --git a/Netdisco/share/public/css/smoothness/jquery-ui.custom.min.css b/Netdisco/share/public/css/smoothness/jquery-ui.custom.min.css new file mode 100644 index 00000000..3844982e --- /dev/null +++ b/Netdisco/share/public/css/smoothness/jquery-ui.custom.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.10.3 - 2013-05-06 +* http://jqueryui.com +* Includes: jquery.ui.core.css, jquery.ui.autocomplete.css, jquery.ui.menu.css +* To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Verdana%2CArial%2Csans-serif&fwDefault=normal&fsDefault=1.1em&cornerRadius=4px&bgColorHeader=cccccc&bgTextureHeader=highlight_soft&bgImgOpacityHeader=75&borderColorHeader=aaaaaa&fcHeader=222222&iconColorHeader=222222&bgColorContent=ffffff&bgTextureContent=flat&bgImgOpacityContent=75&borderColorContent=aaaaaa&fcContent=222222&iconColorContent=222222&bgColorDefault=e6e6e6&bgTextureDefault=glass&bgImgOpacityDefault=75&borderColorDefault=d3d3d3&fcDefault=555555&iconColorDefault=888888&bgColorHover=dadada&bgTextureHover=glass&bgImgOpacityHover=75&borderColorHover=999999&fcHover=212121&iconColorHover=454545&bgColorActive=ffffff&bgTextureActive=glass&bgImgOpacityActive=65&borderColorActive=aaaaaa&fcActive=212121&iconColorActive=454545&bgColorHighlight=fbf9ee&bgTextureHighlight=glass&bgImgOpacityHighlight=55&borderColorHighlight=fcefa1&fcHighlight=363636&iconColorHighlight=2e83ff&bgColorError=fef1ec&bgTextureError=glass&bgImgOpacityError=95&borderColorError=cd0a0a&fcError=cd0a0a&iconColorError=cd0a0a&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=aaaaaa&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=8px&offsetTopShadow=-8px&offsetLeftShadow=-8px&cornerRadiusShadow=8px +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-clearfix{min-height:0}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important}.ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:2px;margin:0;display:block;outline:0}.ui-menu .ui-menu{margin-top:-3px;position:absolute}.ui-menu .ui-menu-item{margin:0;padding:0;width:100%;list-style-image:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}.ui-menu .ui-menu-divider{margin:5px -2px 5px -2px;height:0;font-size:0;line-height:0;border-width:1px 0 0}.ui-menu .ui-menu-item a{text-decoration:none;display:block;padding:2px .4em;line-height:1.5;min-height:0;font-weight:400}.ui-menu .ui-menu-item a.ui-state-focus,.ui-menu .ui-menu-item a.ui-state-active{font-weight:400;margin:-1px}.ui-menu .ui-state-disabled{font-weight:400;margin:.4em 0 .2em;line-height:1.5}.ui-menu .ui-state-disabled a{cursor:default}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item a{position:relative;padding-left:2em}.ui-menu .ui-icon{position:absolute;top:.2em;left:.2em}.ui-menu .ui-menu-icon{position:static;float:right}.ui-widget{font-family:Verdana,Arial,sans-serif;font-size:1.1em}.ui-widget .ui-widget{font-size:1em}.ui-widget input,.ui-widget select,.ui-widget textarea,.ui-widget button{font-family:Verdana,Arial,sans-serif;font-size:1em}.ui-widget-content{border:1px solid #aaa;background:#fff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x;color:#222}.ui-widget-content a{color:#222}.ui-widget-header{border:1px solid #aaa;background:#ccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x;color:#222;font-weight:bold}.ui-widget-header a{color:#222}.ui-state-default,.ui-widget-content .ui-state-default,.ui-widget-header .ui-state-default{border:1px solid #d3d3d3;background:#e6e6e6 url(images/ui-bg_glass_75_e6e6e6_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#555}.ui-state-default a,.ui-state-default a:link,.ui-state-default a:visited{color:#555;text-decoration:none}.ui-state-hover,.ui-widget-content .ui-state-hover,.ui-widget-header .ui-state-hover,.ui-state-focus,.ui-widget-content .ui-state-focus,.ui-widget-header .ui-state-focus{border:1px solid #999;background:#dadada url(images/ui-bg_glass_75_dadada_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-hover a,.ui-state-hover a:hover,.ui-state-hover a:link,.ui-state-hover a:visited{color:#212121;text-decoration:none}.ui-state-active,.ui-widget-content .ui-state-active,.ui-widget-header .ui-state-active{border:1px solid #aaa;background:#fff url(images/ui-bg_glass_65_ffffff_1x400.png) 50% 50% repeat-x;font-weight:normal;color:#212121}.ui-state-active a,.ui-state-active a:link,.ui-state-active a:visited{color:#212121;text-decoration:none}.ui-state-highlight,.ui-widget-content .ui-state-highlight,.ui-widget-header .ui-state-highlight{border:1px solid #fcefa1;background:#fbf9ee url(images/ui-bg_glass_55_fbf9ee_1x400.png) 50% 50% repeat-x;color:#363636}.ui-state-highlight a,.ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a{color:#363636}.ui-state-error,.ui-widget-content .ui-state-error,.ui-widget-header .ui-state-error{border:1px solid #cd0a0a;background:#fef1ec url(images/ui-bg_glass_95_fef1ec_1x400.png) 50% 50% repeat-x;color:#cd0a0a}.ui-state-error a,.ui-widget-content .ui-state-error a,.ui-widget-header .ui-state-error a{color:#cd0a0a}.ui-state-error-text,.ui-widget-content .ui-state-error-text,.ui-widget-header .ui-state-error-text{color:#cd0a0a}.ui-priority-primary,.ui-widget-content .ui-priority-primary,.ui-widget-header .ui-priority-primary{font-weight:bold}.ui-priority-secondary,.ui-widget-content .ui-priority-secondary,.ui-widget-header .ui-priority-secondary{opacity:.7;filter:Alpha(Opacity=70);font-weight:normal}.ui-state-disabled,.ui-widget-content .ui-state-disabled,.ui-widget-header .ui-state-disabled{opacity:.35;filter:Alpha(Opacity=35);background-image:none}.ui-state-disabled .ui-icon{filter:Alpha(Opacity=35)}.ui-icon{width:16px;height:16px}.ui-icon,.ui-widget-content .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-widget-header .ui-icon{background-image:url(images/ui-icons_222222_256x240.png)}.ui-state-default .ui-icon{background-image:url(images/ui-icons_888888_256x240.png)}.ui-state-hover .ui-icon,.ui-state-focus .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-active .ui-icon{background-image:url(images/ui-icons_454545_256x240.png)}.ui-state-highlight .ui-icon{background-image:url(images/ui-icons_2e83ff_256x240.png)}.ui-state-error .ui-icon,.ui-state-error-text .ui-icon{background-image:url(images/ui-icons_cd0a0a_256x240.png)}.ui-icon-blank{background-position:16px 16px}.ui-icon-carat-1-n{background-position:0 0}.ui-icon-carat-1-ne{background-position:-16px 0}.ui-icon-carat-1-e{background-position:-32px 0}.ui-icon-carat-1-se{background-position:-48px 0}.ui-icon-carat-1-s{background-position:-64px 0}.ui-icon-carat-1-sw{background-position:-80px 0}.ui-icon-carat-1-w{background-position:-96px 0}.ui-icon-carat-1-nw{background-position:-112px 0}.ui-icon-carat-2-n-s{background-position:-128px 0}.ui-icon-carat-2-e-w{background-position:-144px 0}.ui-icon-triangle-1-n{background-position:0 -16px}.ui-icon-triangle-1-ne{background-position:-16px -16px}.ui-icon-triangle-1-e{background-position:-32px -16px}.ui-icon-triangle-1-se{background-position:-48px -16px}.ui-icon-triangle-1-s{background-position:-64px -16px}.ui-icon-triangle-1-sw{background-position:-80px -16px}.ui-icon-triangle-1-w{background-position:-96px -16px}.ui-icon-triangle-1-nw{background-position:-112px -16px}.ui-icon-triangle-2-n-s{background-position:-128px -16px}.ui-icon-triangle-2-e-w{background-position:-144px -16px}.ui-icon-arrow-1-n{background-position:0 -32px}.ui-icon-arrow-1-ne{background-position:-16px -32px}.ui-icon-arrow-1-e{background-position:-32px -32px}.ui-icon-arrow-1-se{background-position:-48px -32px}.ui-icon-arrow-1-s{background-position:-64px -32px}.ui-icon-arrow-1-sw{background-position:-80px -32px}.ui-icon-arrow-1-w{background-position:-96px -32px}.ui-icon-arrow-1-nw{background-position:-112px -32px}.ui-icon-arrow-2-n-s{background-position:-128px -32px}.ui-icon-arrow-2-ne-sw{background-position:-144px -32px}.ui-icon-arrow-2-e-w{background-position:-160px -32px}.ui-icon-arrow-2-se-nw{background-position:-176px -32px}.ui-icon-arrowstop-1-n{background-position:-192px -32px}.ui-icon-arrowstop-1-e{background-position:-208px -32px}.ui-icon-arrowstop-1-s{background-position:-224px -32px}.ui-icon-arrowstop-1-w{background-position:-240px -32px}.ui-icon-arrowthick-1-n{background-position:0 -48px}.ui-icon-arrowthick-1-ne{background-position:-16px -48px}.ui-icon-arrowthick-1-e{background-position:-32px -48px}.ui-icon-arrowthick-1-se{background-position:-48px -48px}.ui-icon-arrowthick-1-s{background-position:-64px -48px}.ui-icon-arrowthick-1-sw{background-position:-80px -48px}.ui-icon-arrowthick-1-w{background-position:-96px -48px}.ui-icon-arrowthick-1-nw{background-position:-112px -48px}.ui-icon-arrowthick-2-n-s{background-position:-128px -48px}.ui-icon-arrowthick-2-ne-sw{background-position:-144px -48px}.ui-icon-arrowthick-2-e-w{background-position:-160px -48px}.ui-icon-arrowthick-2-se-nw{background-position:-176px -48px}.ui-icon-arrowthickstop-1-n{background-position:-192px -48px}.ui-icon-arrowthickstop-1-e{background-position:-208px -48px}.ui-icon-arrowthickstop-1-s{background-position:-224px -48px}.ui-icon-arrowthickstop-1-w{background-position:-240px -48px}.ui-icon-arrowreturnthick-1-w{background-position:0 -64px}.ui-icon-arrowreturnthick-1-n{background-position:-16px -64px}.ui-icon-arrowreturnthick-1-e{background-position:-32px -64px}.ui-icon-arrowreturnthick-1-s{background-position:-48px -64px}.ui-icon-arrowreturn-1-w{background-position:-64px -64px}.ui-icon-arrowreturn-1-n{background-position:-80px -64px}.ui-icon-arrowreturn-1-e{background-position:-96px -64px}.ui-icon-arrowreturn-1-s{background-position:-112px -64px}.ui-icon-arrowrefresh-1-w{background-position:-128px -64px}.ui-icon-arrowrefresh-1-n{background-position:-144px -64px}.ui-icon-arrowrefresh-1-e{background-position:-160px -64px}.ui-icon-arrowrefresh-1-s{background-position:-176px -64px}.ui-icon-arrow-4{background-position:0 -80px}.ui-icon-arrow-4-diag{background-position:-16px -80px}.ui-icon-extlink{background-position:-32px -80px}.ui-icon-newwin{background-position:-48px -80px}.ui-icon-refresh{background-position:-64px -80px}.ui-icon-shuffle{background-position:-80px -80px}.ui-icon-transfer-e-w{background-position:-96px -80px}.ui-icon-transferthick-e-w{background-position:-112px -80px}.ui-icon-folder-collapsed{background-position:0 -96px}.ui-icon-folder-open{background-position:-16px -96px}.ui-icon-document{background-position:-32px -96px}.ui-icon-document-b{background-position:-48px -96px}.ui-icon-note{background-position:-64px -96px}.ui-icon-mail-closed{background-position:-80px -96px}.ui-icon-mail-open{background-position:-96px -96px}.ui-icon-suitcase{background-position:-112px -96px}.ui-icon-comment{background-position:-128px -96px}.ui-icon-person{background-position:-144px -96px}.ui-icon-print{background-position:-160px -96px}.ui-icon-trash{background-position:-176px -96px}.ui-icon-locked{background-position:-192px -96px}.ui-icon-unlocked{background-position:-208px -96px}.ui-icon-bookmark{background-position:-224px -96px}.ui-icon-tag{background-position:-240px -96px}.ui-icon-home{background-position:0 -112px}.ui-icon-flag{background-position:-16px -112px}.ui-icon-calendar{background-position:-32px -112px}.ui-icon-cart{background-position:-48px -112px}.ui-icon-pencil{background-position:-64px -112px}.ui-icon-clock{background-position:-80px -112px}.ui-icon-disk{background-position:-96px -112px}.ui-icon-calculator{background-position:-112px -112px}.ui-icon-zoomin{background-position:-128px -112px}.ui-icon-zoomout{background-position:-144px -112px}.ui-icon-search{background-position:-160px -112px}.ui-icon-wrench{background-position:-176px -112px}.ui-icon-gear{background-position:-192px -112px}.ui-icon-heart{background-position:-208px -112px}.ui-icon-star{background-position:-224px -112px}.ui-icon-link{background-position:-240px -112px}.ui-icon-cancel{background-position:0 -128px}.ui-icon-plus{background-position:-16px -128px}.ui-icon-plusthick{background-position:-32px -128px}.ui-icon-minus{background-position:-48px -128px}.ui-icon-minusthick{background-position:-64px -128px}.ui-icon-close{background-position:-80px -128px}.ui-icon-closethick{background-position:-96px -128px}.ui-icon-key{background-position:-112px -128px}.ui-icon-lightbulb{background-position:-128px -128px}.ui-icon-scissors{background-position:-144px -128px}.ui-icon-clipboard{background-position:-160px -128px}.ui-icon-copy{background-position:-176px -128px}.ui-icon-contact{background-position:-192px -128px}.ui-icon-image{background-position:-208px -128px}.ui-icon-video{background-position:-224px -128px}.ui-icon-script{background-position:-240px -128px}.ui-icon-alert{background-position:0 -144px}.ui-icon-info{background-position:-16px -144px}.ui-icon-notice{background-position:-32px -144px}.ui-icon-help{background-position:-48px -144px}.ui-icon-check{background-position:-64px -144px}.ui-icon-bullet{background-position:-80px -144px}.ui-icon-radio-on{background-position:-96px -144px}.ui-icon-radio-off{background-position:-112px -144px}.ui-icon-pin-w{background-position:-128px -144px}.ui-icon-pin-s{background-position:-144px -144px}.ui-icon-play{background-position:0 -160px}.ui-icon-pause{background-position:-16px -160px}.ui-icon-seek-next{background-position:-32px -160px}.ui-icon-seek-prev{background-position:-48px -160px}.ui-icon-seek-end{background-position:-64px -160px}.ui-icon-seek-start{background-position:-80px -160px}.ui-icon-seek-first{background-position:-80px -160px}.ui-icon-stop{background-position:-96px -160px}.ui-icon-eject{background-position:-112px -160px}.ui-icon-volume-off{background-position:-128px -160px}.ui-icon-volume-on{background-position:-144px -160px}.ui-icon-power{background-position:0 -176px}.ui-icon-signal-diag{background-position:-16px -176px}.ui-icon-signal{background-position:-32px -176px}.ui-icon-battery-0{background-position:-48px -176px}.ui-icon-battery-1{background-position:-64px -176px}.ui-icon-battery-2{background-position:-80px -176px}.ui-icon-battery-3{background-position:-96px -176px}.ui-icon-circle-plus{background-position:0 -192px}.ui-icon-circle-minus{background-position:-16px -192px}.ui-icon-circle-close{background-position:-32px -192px}.ui-icon-circle-triangle-e{background-position:-48px -192px}.ui-icon-circle-triangle-s{background-position:-64px -192px}.ui-icon-circle-triangle-w{background-position:-80px -192px}.ui-icon-circle-triangle-n{background-position:-96px -192px}.ui-icon-circle-arrow-e{background-position:-112px -192px}.ui-icon-circle-arrow-s{background-position:-128px -192px}.ui-icon-circle-arrow-w{background-position:-144px -192px}.ui-icon-circle-arrow-n{background-position:-160px -192px}.ui-icon-circle-zoomin{background-position:-176px -192px}.ui-icon-circle-zoomout{background-position:-192px -192px}.ui-icon-circle-check{background-position:-208px -192px}.ui-icon-circlesmall-plus{background-position:0 -208px}.ui-icon-circlesmall-minus{background-position:-16px -208px}.ui-icon-circlesmall-close{background-position:-32px -208px}.ui-icon-squaresmall-plus{background-position:-48px -208px}.ui-icon-squaresmall-minus{background-position:-64px -208px}.ui-icon-squaresmall-close{background-position:-80px -208px}.ui-icon-grip-dotted-vertical{background-position:0 -224px}.ui-icon-grip-dotted-horizontal{background-position:-16px -224px}.ui-icon-grip-solid-vertical{background-position:-32px -224px}.ui-icon-grip-solid-horizontal{background-position:-48px -224px}.ui-icon-gripsmall-diagonal-se{background-position:-64px -224px}.ui-icon-grip-diagonal-se{background-position:-80px -224px}.ui-corner-all,.ui-corner-top,.ui-corner-left,.ui-corner-tl{border-top-left-radius:4px}.ui-corner-all,.ui-corner-top,.ui-corner-right,.ui-corner-tr{border-top-right-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-left,.ui-corner-bl{border-bottom-left-radius:4px}.ui-corner-all,.ui-corner-bottom,.ui-corner-right,.ui-corner-br{border-bottom-right-radius:4px}.ui-widget-overlay{background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30)}.ui-widget-shadow{margin:-8px 0 0 -8px;padding:8px;background:#aaa url(images/ui-bg_flat_0_aaaaaa_40x100.png) 50% 50% repeat-x;opacity:.3;filter:Alpha(Opacity=30);border-radius:8px} \ No newline at end of file diff --git a/Netdisco/share/public/javascripts/jquery-ui.custom.min.js b/Netdisco/share/public/javascripts/jquery-ui.custom.min.js new file mode 100644 index 00000000..4fbf3f87 --- /dev/null +++ b/Netdisco/share/public/javascripts/jquery-ui.custom.min.js @@ -0,0 +1,6 @@ +/*! jQuery UI - v1.10.3 - 2013-05-06 +* http://jqueryui.com +* Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.position.js, jquery.ui.autocomplete.js, jquery.ui.menu.js +* Copyright 2013 jQuery Foundation and other contributors Licensed MIT */ + +(function(e,t){function i(t,i){var a,n,r,o=t.nodeName.toLowerCase();return"area"===o?(a=t.parentNode,n=a.name,t.href&&n&&"map"===a.nodeName.toLowerCase()?(r=e("img[usemap=#"+n+"]")[0],!!r&&s(r)):!1):(/input|select|textarea|button|object/.test(o)?!t.disabled:"a"===o?t.href||i:i)&&s(t)}function s(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var a=0,n=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.3",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,s){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),s&&s.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var s,a,n=e(this[0]);n.length&&n[0]!==document;){if(s=n.css("position"),("absolute"===s||"relative"===s||"fixed"===s)&&(a=parseInt(n.css("zIndex"),10),!isNaN(a)&&0!==a))return a;n=n.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++a)})},removeUniqueId:function(){return this.each(function(){n.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,s){return!!e.data(t,s[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var s=e.attr(t,"tabindex"),a=isNaN(s);return(a||s>=0)&&i(t,!a)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,s){function a(t,i,s,a){return e.each(n,function(){i-=parseFloat(e.css(t,"padding"+this))||0,s&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),a&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var n="Width"===s?["Left","Right"]:["Top","Bottom"],r=s.toLowerCase(),o={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+s]=function(i){return i===t?o["inner"+s].call(this):this.each(function(){e(this).css(r,a(this,i)+"px")})},e.fn["outer"+s]=function(t,i){return"number"!=typeof t?o["outer"+s].call(this,t):this.each(function(){e(this).css(r,a(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,s){var a,n=e.ui[t].prototype;for(a in s)n.plugins[a]=n.plugins[a]||[],n.plugins[a].push([i,s[a]])},call:function(e,t,i){var s,a=e.plugins[t];if(a&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(s=0;a.length>s;s++)e.options[a[s][0]]&&a[s][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",a=!1;return t[s]>0?!0:(t[s]=1,a=t[s]>0,t[s]=0,a)}})})(jQuery);(function(e,t){var i=0,s=Array.prototype.slice,n=e.cleanData;e.cleanData=function(t){for(var i,s=0;null!=(i=t[s]);s++)try{e(i).triggerHandler("remove")}catch(a){}n(t)},e.widget=function(i,s,n){var a,r,o,h,l={},u=i.split(".")[0];i=i.split(".")[1],a=u+"-"+i,n||(n=s,s=e.Widget),e.expr[":"][a.toLowerCase()]=function(t){return!!e.data(t,a)},e[u]=e[u]||{},r=e[u][i],o=e[u][i]=function(e,i){return this._createWidget?(arguments.length&&this._createWidget(e,i),t):new o(e,i)},e.extend(o,r,{version:n.version,_proto:e.extend({},n),_childConstructors:[]}),h=new s,h.options=e.widget.extend({},h.options),e.each(n,function(i,n){return e.isFunction(n)?(l[i]=function(){var e=function(){return s.prototype[i].apply(this,arguments)},t=function(e){return s.prototype[i].apply(this,e)};return function(){var i,s=this._super,a=this._superApply;return this._super=e,this._superApply=t,i=n.apply(this,arguments),this._super=s,this._superApply=a,i}}(),t):(l[i]=n,t)}),o.prototype=e.widget.extend(h,{widgetEventPrefix:r?h.widgetEventPrefix:i},l,{constructor:o,namespace:u,widgetName:i,widgetFullName:a}),r?(e.each(r._childConstructors,function(t,i){var s=i.prototype;e.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete r._childConstructors):s._childConstructors.push(o),e.widget.bridge(i,o)},e.widget.extend=function(i){for(var n,a,r=s.call(arguments,1),o=0,h=r.length;h>o;o++)for(n in r[o])a=r[o][n],r[o].hasOwnProperty(n)&&a!==t&&(i[n]=e.isPlainObject(a)?e.isPlainObject(i[n])?e.widget.extend({},i[n],a):e.widget.extend({},a):a);return i},e.widget.bridge=function(i,n){var a=n.prototype.widgetFullName||i;e.fn[i]=function(r){var o="string"==typeof r,h=s.call(arguments,1),l=this;return r=!o&&h.length?e.widget.extend.apply(null,[r].concat(h)):r,o?this.each(function(){var s,n=e.data(this,a);return n?e.isFunction(n[r])&&"_"!==r.charAt(0)?(s=n[r].apply(n,h),s!==n&&s!==t?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):t):e.error("no such method '"+r+"' for "+i+" widget instance"):e.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+r+"'")}):this.each(function(){var t=e.data(this,a);t?t.option(r||{})._init():e.data(this,a,new n(r,this))}),l}},e.Widget=function(){},e.Widget._childConstructors=[],e.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(t,s){s=e(s||this.defaultElement||this)[0],this.element=e(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=e.widget.extend({},this.options,this._getCreateOptions(),t),this.bindings=e(),this.hoverable=e(),this.focusable=e(),s!==this&&(e.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(e){e.target===s&&this.destroy()}}),this.document=e(s.style?s.ownerDocument:s.document||s),this.window=e(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:e.noop,_getCreateEventData:e.noop,_create:e.noop,_init:e.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(e.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:e.noop,widget:function(){return this.element},option:function(i,s){var n,a,r,o=i;if(0===arguments.length)return e.widget.extend({},this.options);if("string"==typeof i)if(o={},n=i.split("."),i=n.shift(),n.length){for(a=o[i]=e.widget.extend({},this.options[i]),r=0;n.length-1>r;r++)a[n[r]]=a[n[r]]||{},a=a[n[r]];if(i=n.pop(),s===t)return a[i]===t?null:a[i];a[i]=s}else{if(s===t)return this.options[i]===t?null:this.options[i];o[i]=s}return this._setOptions(o),this},_setOptions:function(e){var t;for(t in e)this._setOption(t,e[t]);return this},_setOption:function(e,t){return this.options[e]=t,"disabled"===e&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!t).attr("aria-disabled",t),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var a,r=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=a=e(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,a=this.widget()),e.each(n,function(n,o){function h(){return i||r.options.disabled!==!0&&!e(this).hasClass("ui-state-disabled")?("string"==typeof o?r[o]:o).apply(r,arguments):t}"string"!=typeof o&&(h.guid=o.guid=o.guid||h.guid||e.guid++);var l=n.match(/^(\w+)\s*(.*)$/),u=l[1]+r.eventNamespace,c=l[2];c?a.delegate(c,u,h):s.bind(u,h)})},_off:function(e,t){t=(t||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(t).undelegate(t)},_delay:function(e,t){function i(){return("string"==typeof e?s[e]:e).apply(s,arguments)}var s=this;return setTimeout(i,t||0)},_hoverable:function(t){this.hoverable=this.hoverable.add(t),this._on(t,{mouseenter:function(t){e(t.currentTarget).addClass("ui-state-hover")},mouseleave:function(t){e(t.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(t){this.focusable=this.focusable.add(t),this._on(t,{focusin:function(t){e(t.currentTarget).addClass("ui-state-focus")},focusout:function(t){e(t.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(t,i,s){var n,a,r=this.options[t];if(s=s||{},i=e.Event(i),i.type=(t===this.widgetEventPrefix?t:this.widgetEventPrefix+t).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(e.isFunction(r)&&r.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},e.each({show:"fadeIn",hide:"fadeOut"},function(t,i){e.Widget.prototype["_"+t]=function(s,n,a){"string"==typeof n&&(n={effect:n});var r,o=n?n===!0||"number"==typeof n?i:n.effect||i:t;n=n||{},"number"==typeof n&&(n={duration:n}),r=!e.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),r&&e.effects&&e.effects.effect[o]?s[t](n):o!==t&&s[o]?s[o](n.duration,n.easing,a):s.queue(function(i){e(this)[t](),a&&a.call(s[0]),i()})}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,h=Math.round,l=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("
"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow?"":e.element.css("overflow-x"),s=e.isWindow?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widths?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(h.horizontal="center"),d>m&&m>r(n+a)&&(h.vertical="middle"),h.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,h)}),c.offset(t.extend(C,{using:l}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-o-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-o-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,m=-2*e.offset[1];0>c?(s=t.top+p+f+m+e.collisionHeight-o-a,t.top+p+f+m>c&&(0>s||r(c)>s)&&(t.top+=p+f+m)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+m-h,t.top+p+f+m>u&&(i>0||u>r(i))&&(t.top+=p+f+m))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(t){var e=0;t.widget("ui.autocomplete",{version:"1.10.3",defaultElement:"",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,undefined;e=!1,s=!1,i=!1;var a=t.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:e=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case a.UP:e=!0,this._keyEvent("previous",n);break;case a.DOWN:e=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),undefined):(this._searchTimeout(t),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(t),this._change(t),undefined)}}),this._initSource(),this.menu=t("