From 4d0e2461f5bc3f94ee1be0cd05ef1325b6d2a8ed Mon Sep 17 00:00:00 2001 From: Oliver Gorwits Date: Mon, 3 Jun 2013 20:38:33 +0100 Subject: [PATCH] Merge of og-work branch, many new features. Squashed commit of the following: commit a43c98962ae7b88787f8960090ebbe86d61450f2 Author: Oliver Gorwits Date: Mon Jun 3 20:37:39 2013 +0100 Missing mibdirs causes all MIBs to be loaded (with a warning) commit 09829a25b8d0a60d4c014ac6ebed35f0cf233a56 Author: Oliver Gorwits Date: Mon Jun 3 20:07:31 2013 +0100 local plugins site_plugins dir commit b0e804e558874e0736bbb1264aed7128274a3c79 Author: Oliver Gorwits Date: Mon Jun 3 19:59:04 2013 +0100 use send_error and redirect from Dancer commit 3d1185261ab4cec9aef53afc0d715d7fceeecd90 Author: Oliver Gorwits Date: Mon Jun 3 19:13:40 2013 +0100 support path config option commit 31ca119f84b08bd420d6ef7d7a19b16f9dd19988 Merge: 9a79855 4d2b3a5 Author: Oliver Gorwits Date: Mon Jun 3 00:06:17 2013 +0100 Merge remote-tracking branch 'origin/og-work' into og-work g-work" This reverts commit 9a798553615317d6571a35d32f4e6bf822b5560e, reversing changes made to 6fd61183545716cf13928c4555689d473e9fe898. Conflicts: Netdisco/share/views/plugin/device_port_column/c_observiumsparklines.tt commit 9a798553615317d6571a35d32f4e6bf822b5560e Merge: 6fd6118 c8c3b82 Author: Oliver Gorwits Date: Mon Jun 3 00:03:32 2013 +0100 Merge remote-tracking branch 'origin/master' into og-work commit 6fd61183545716cf13928c4555689d473e9fe898 Author: Oliver Gorwits Date: Sun Jun 2 15:47:45 2013 +0100 extra note about behind proxy commit 798086ca29b879fd0864deddcd9bcd1ee668f882 Author: Oliver Gorwits Date: Sun Jun 2 15:30:26 2013 +0100 complete the observium plugin commit 66b3ced17946ee94fc3ae5760f1eff97e3cfa133 Author: Oliver Gorwits Date: Sun Jun 2 12:48:06 2013 +0100 Plugins can have CSS and Javascript loaded within commit 4d2b3a53072f0c6b69e6e9b36154f83b3966fbc5 Author: Oliver Gorwits Date: Thu May 30 08:50:16 2013 +0100 get device dns to port template commit ed1bfa1ae706858a8301af1e3db6723d477a0f7f Author: Oliver Gorwits Date: Thu May 30 08:17:02 2013 +0100 observium sparklines plugin; support X:: namespace commit 76b7636c748b0408fb46b31b6b3969c884dd798c Author: Oliver Gorwits Date: Thu May 30 06:30:06 2013 +0100 rename private settings keys commit fdac8f6c3351dffb0a0895361e98f2a86e39904f Author: Oliver Gorwits Date: Thu May 30 05:59:53 2013 +0100 add macwalk and arpnip buttons to device details commit 3d688c7d83821ea65cc439488830068e6cd6c3f5 Author: Oliver Gorwits Date: Thu May 30 05:57:20 2013 +0100 Revert "reduce refresh to 5sec" This reverts commit 8ea9ec7dd9422d7cd17a85119b5b46ca0be4d030. commit dc62382112b255e63fce3868812ec386226401e5 Author: Oliver Gorwits Date: Thu May 30 05:50:34 2013 +0100 support for arpwalk and macwalk and all jobs via web commit 8bc7d83c985f03970217d9f00185f83a61ae173d Author: Oliver Gorwits Date: Thu May 30 05:35:41 2013 +0100 simplify discover options to only discoverall and discover commit 8ea9ec7dd9422d7cd17a85119b5b46ca0be4d030 Author: Oliver Gorwits Date: Wed May 29 20:23:08 2013 +0100 reduce refresh to 5sec commit 8c54e6c58b555f624b6bce1716a61ceb1e6e2eaa Author: Oliver Gorwits Date: Wed May 29 20:11:06 2013 +0100 show undiscovered neighbor properly commit e0ee25628febdcecbb93356934286d7bd273023f Author: Oliver Gorwits Date: Wed May 29 19:54:09 2013 +0100 avoid unecessary log for queueing commit d5565423f2635e50cc6f160a8f0d5e6e2da3468f Author: Oliver Gorwits Date: Wed May 29 19:51:37 2013 +0100 avoid warning on undefined remote type commit 5d9b58a6b27f191815040630463d6bdb9bbfddb8 Author: Oliver Gorwits Date: Wed May 29 19:48:22 2013 +0100 avoid explosion when not admin commit 377bb942e0b4088795f43ded29df2a14f2c1979f Author: Oliver Gorwits Date: Wed May 29 19:46:52 2013 +0100 avoid undefined warning commit 08806dcfa24dbb7727c63ff46aedc2ee9eff46cc Author: Oliver Gorwits Date: Wed May 29 19:46:42 2013 +0100 get_db_version will be 0 at first deploy commit 9511c17056e1931c8b2de051dde9f08d0c6cddaf Author: Oliver Gorwits Date: Wed May 29 19:15:55 2013 +0100 fix name of Template module commit eb0288de35fa7fc111d4b6fff19f163fa9b9ffe9 Author: Oliver Gorwits Date: Tue May 28 07:17:07 2013 +0100 initial config settings documentation commit 7f2ea7f8dc4c4a865c26e20bda20184d29ae8df1 Author: Oliver Gorwits Date: Mon May 27 15:18:15 2013 +0100 remove check_mac to own module, use in macsuck too commit b995cf63986888933de70edae22827bab0d17266 Author: Oliver Gorwits Date: Mon May 27 15:01:29 2013 +0100 show probable but undiscovered neighbor is ports display commit dd8d461188aba779aa3f8c3b6c01188d29094cf3 Author: Oliver Gorwits Date: Mon May 27 14:52:41 2013 +0100 new schema version for is_uplink and is_uplink_admin commit 3f6a7b5aa221857ab2a0f35f370a5c515d546351 Author: Oliver Gorwits Date: Mon May 27 14:47:59 2013 +0100 make sure device_port is updated when manual_topo is set commit 33bf9a6599565e3acebc33bcc54aed1b2106d7c7 Author: Oliver Gorwits Date: Sun May 26 19:51:49 2013 +0100 export store_arp and store_node commit 0ed356d5606bb1b5d421609052615ab4dfe1b991 Author: Oliver Gorwits Date: Sat May 25 17:12:31 2013 +0100 use row lock not table lock commit f830bc3a3baee2c885443a15c564a0d53f06147b Author: Oliver Gorwits Date: Sat May 25 16:38:33 2013 +0100 move macsuck/arpnip/discover to ::Core namespace commit be40788987989fe9f052963f8f382416b3dc49b1 Author: Oliver Gorwits Date: Fri May 24 21:10:34 2013 +0100 add maybe_uplink to device_port; more macsuck implementation commit 88371026d5b35d8cee55f073e0456417f94521a8 Author: Oliver Gorwits Date: Fri May 24 14:34:58 2013 +0100 start on macsuck; tweak update locking commit 6f7c87ac07185affe79e3b0b7d8468567c6c1d70 Author: Oliver Gorwits Date: Fri May 24 13:10:58 2013 +0100 ORDER BY ... FOR UPDATE will allow us to avoid table lock commit 7c438e01fc44dbf4a39832cfe0dbaf531e231325 Author: Oliver Gorwits Date: Fri May 24 12:12:46 2013 +0100 yet more efficient arpnip commit c74c56dc02a4e934ecf7245bfd376b94a47cd1c3 Author: Oliver Gorwits Date: Fri May 24 11:34:23 2013 +0100 guard against race with *_or_* DBIC methods commit d50c54972eebf01b75211b7cacc046b20ad42b12 Author: Oliver Gorwits Date: Mon May 20 23:42:41 2013 +0100 more efficient arpnip commit 73c8979130292fa22357d2acdc08497d5afcfcc1 Author: Oliver Gorwits Date: Sun May 19 22:52:15 2013 +0100 fix confusing name commit bf78e824110fec7b7ca9460871525f6f9cde6030 Author: Oliver Gorwits Date: Sun May 19 22:37:22 2013 +0100 fix mistake in DBIx::Class schema commit 6a5af958363271a9311e86421ffcd1c7c35a0fbc Author: Oliver Gorwits Date: Sun May 19 22:06:27 2013 +0100 arpnip implementation commit 594abd3f8243e118c72182528715466da0fb12d9 Author: Oliver Gorwits Date: Thu May 16 00:00:50 2013 +0100 PostgreSQL explicit locking support. Squashed commit of the following: commit 76e15391020dcae7465c2682205b01d643685c78 Author: Oliver Gorwits Date: Wed May 15 23:54:25 2013 +0100 finished explicit locking module commit 369387258b4058bb7d10c591dcfe44845bfcd83e Author: Oliver Gorwits Date: Tue May 14 23:50:42 2013 +0100 initial implementation of locking from schema object commit 55c6d4fe635ed4fb5506733313b3247d9412a046 Author: Oliver Gorwits Date: Tue May 14 21:05:01 2013 +0100 add discover button to device details page commit 11fd8bf964c9ceec1d54924ce3629fd186647e00 Author: Oliver Gorwits Date: Tue May 14 20:43:43 2013 +0100 fix typo and clear port box on autocomplete dropdown commit a00f9b5c2edcc6e0b6ecb716e06706c8ab8f8d2b Author: Oliver Gorwits Date: Tue May 14 20:38:54 2013 +0100 move admin tasks and remove JobControl package commit 74bc0023df4db8beeb6c8550fbc86d1896af493c Author: Oliver Gorwits Date: Sat May 11 18:25:04 2013 +0100 complete job queue delete and kill running timers properly when reloading page commit dd6947f38db8924e0984254a3fb2a716b16f3bf5 Author: Oliver Gorwits Date: Sat May 11 16:51:28 2013 +0100 fix improper use of bootstrap table class commit cd5b83f71eba2eb28eca5181f3fcf23de11e3904 Author: Oliver Gorwits Date: Sat May 11 15:55:45 2013 +0100 fix update view icon in sidebar commit e9349f325d74a0a62e7e1cd75d42d271ea5bc839 Author: Oliver Gorwits Date: Sat May 11 11:57:19 2013 +0100 css audit commit 201470275dd1a188bacd0345137dc53e20dc46c2 Author: Oliver Gorwits Date: Thu May 9 23:48:05 2013 +0100 add job queue to standard plugins list commit a18a3c72a3d19305525b89783ec9ac676ecd39b7 Author: Oliver Gorwits Date: Thu May 9 23:37:43 2013 +0100 fix table headings and improve Action display in Job Queue commit 70f5da8bb65d5fdf5f2ad5a56da70467269a10b9 Author: Oliver Gorwits Date: Thu May 9 23:30:32 2013 +0100 implement "no devices" prompt for admin users to do first discover commit 2e8ac831738ddd45e7976743036699be4358f1f9 Author: Oliver Gorwits Date: Thu May 9 21:53:39 2013 +0100 more js refactoring for report and search commit 479ac0e55df6845304ecfb96e2b06d2ef62970bf Author: Oliver Gorwits Date: Thu May 9 21:50:29 2013 +0100 refactor js for device tabs commit 6a17fe5d6c9ac638cbf129951f864dfb9050871e Author: Oliver Gorwits Date: Thu May 9 21:05:42 2013 +0100 fix crazy races with javasacript by using global delegations commit e94e3cef3bcc36bcad3d4d6dcdcc3a8c74a798d0 Author: Oliver Gorwits Date: Wed May 8 23:06:41 2013 +0100 remove Try::Tiny from web runtime commit c746e68b9b034c6e85b282be1f2599527b0265ee Author: Oliver Gorwits Date: Tue May 7 21:54:11 2013 +0100 make topo autocomplete more responsive commit 24c511786fe99733b1ad7de54bf35ce26ac70be7 Author: Oliver Gorwits Date: Tue May 7 21:52:17 2013 +0100 display name and IP for device typeahead commit 52ab7d12661f640fe5b9c6364a6fed47d547e397 Author: Oliver Gorwits Date: Tue May 7 21:47:05 2013 +0100 add drop-down control for the topo form fields commit 5744b6845f28d11a1b211d53c5d041e2db48dd0f Author: Oliver Gorwits Date: Tue May 7 21:25:30 2013 +0100 complete the topology editor (add/delete) commit b510fbe8c569bbeab19d8540f32c8557093313fc Author: Oliver Gorwits Date: Tue May 7 00:59:11 2013 +0100 add new admin tasks to default plugins list commit 11d55e01295c9f00cd4f3dd0880e4f1b8eb8aa7c Author: Oliver Gorwits Date: Tue May 7 00:56:19 2013 +0100 Manual Device Topology Needed to add the 'autocomplete' jQuery UI component because it can do minLength=0 properly. Used the smoothness UI theme. Added typeahead AJAX calls to support the topology searching. Added new plugin and template for the topology editing page. commit bf7a419d088fb58bda6f962f7e3fc3262571545c Author: Oliver Gorwits Date: Mon May 6 22:16:24 2013 +0100 add a little colour to lone tab titles commit 9690a31f1976582995aae259c35e760d15e32c38 Author: Oliver Gorwits Date: Mon May 6 22:01:13 2013 +0100 complete Manage Pseudo Devices commit 024f4d9a838cbeeec387ca11c9707d4c0a903d49 Author: Oliver Gorwits Date: Mon May 6 00:49:47 2013 +0100 use bootstrap font colour instead of css commit f75f1e5cbf51d024b0588ca9b410b0d0abad517f Author: Oliver Gorwits Date: Mon May 6 00:45:18 2013 +0100 add frontend update/del forms, and display port count commit f0899e16b3cd92df43ed705f1fbace6ae87753a3 Author: Oliver Gorwits Date: Sun May 5 23:53:20 2013 +0100 add frontend pseudo device add form commit 3271c019314554a9fc247f4720870c8dcd578555 Author: Oliver Gorwits Date: Sun May 5 21:45:17 2013 +0100 complete the code for admin tasks page loading commit 38f70624f39736e1cff7f7fc2e90e37ba9ef9a1f Author: Oliver Gorwits Date: Sun May 5 17:04:30 2013 +0100 set up file paths consistently in all scripts commit c761ca839b6f043b92fc753f2774453f371de091 Author: Oliver Gorwits Date: Sun May 5 17:00:30 2013 +0100 Helper script to import the Netdisco 1.x Topology file to the database commit f468b48049f3943e746457c7eba1d5bd7a292f0d Author: Oliver Gorwits Date: Sun May 5 16:20:39 2013 +0100 Handle whitespace ahead of OUI data commit 5c8a5754f6f4aa97711c231abfe175d74c93b340 Author: Oliver Gorwits Date: Sun May 5 16:16:20 2013 +0100 also set neighbor info when discovering device interfaces commit acb988b6afd915b82aae06859ef8e3857543110d Author: Oliver Gorwits Date: Sun May 5 15:34:20 2013 +0100 try to avoid duplicate execution of scheduled jobs commit c6bcaf66c59ab1b2debeed302068bf0db44614f6 Author: Oliver Gorwits Date: Sun May 5 14:16:25 2013 +0100 do not clobber manual topo when discovering neighbors commit d9a6a1882ae4aa47d90adbdbb0cb6a6c23f63f8b Author: Oliver Gorwits Date: Sun May 5 13:02:45 2013 +0100 User icon color indicates port_control/admin ability commit 2cdcb9db7e44c28a6b3836eec21cbaf38d1de4b0 Author: Oliver Gorwits Date: Mon Apr 29 23:34:27 2013 +0100 add support for admin tasks as plugins commit 075a770c9a5822717b30b6988b90b29ffca63aa6 Author: Oliver Gorwits Date: Mon Apr 29 22:23:20 2013 +0100 skip pseudo devices (vendor netdisco) commit 045c022d42f6ea6b0a7daef853256ff8e83bd870 Author: Oliver Gorwits Date: Mon Apr 29 21:58:33 2013 +0100 incorporate manual topo info from the topology db table commit 09285d42b4144501d31e8bb7e2e5306ac4a742c5 Author: Oliver Gorwits Date: Sat Apr 27 18:39:12 2013 +0100 add unique constraints to topology table commit 2780b72e490dbcf9c5a9a79c466fa0510d2d33ae Author: Oliver Gorwits Date: Sat Apr 27 15:38:05 2013 +0100 muted help text in sidebar commit 733d4f83fbff4515dd8929145ec8ef39039f481e Author: Oliver Gorwits Date: Sat Apr 27 14:39:54 2013 +0100 sorry, testing hook changes commit 71e366e3521b94e8e8254e374fdb03611ba09f51 Author: Oliver Gorwits Date: Sat Apr 27 14:34:36 2013 +0100 sorry, testing hook changes commit 7f9eaa99f52e9d610348e8f35dc735e209232453 Author: Oliver Gorwits Date: Sat Apr 27 14:33:44 2013 +0100 sorry, testing hook changes commit 5215fd632da35aeec4469ad204eb3be782b3f201 Author: Oliver Gorwits Date: Sat Apr 27 14:30:07 2013 +0100 sorry, testing hook changes commit be817d60c205d19154e62f650a078563100eb7a2 Author: Oliver Gorwits Date: Sat Apr 27 14:21:45 2013 +0100 sorry, testing hook changes commit 1fd36953587f9db67a242477fa6824e0f22b074b Author: Oliver Gorwits Date: Sat Apr 27 14:18:57 2013 +0100 sorry, testing hook changes commit ac448c4a91b9a5d40316a43912b47913f12bc967 Author: Oliver Gorwits Date: Sat Apr 27 14:13:03 2013 +0100 sorry, testing hook changes commit c563b8d9afe794d80b1efd97fbdbfedb7283e1e6 Author: Oliver Gorwits Date: Sat Apr 27 14:08:54 2013 +0100 sorry, testing hook changes commit 3abcfb01d54ff806241d94dd7cb9cd867e0d64aa Author: Oliver Gorwits Date: Sat Apr 27 14:06:25 2013 +0100 sorry, testing hook changes commit 877a81facf1b192bc3cb9cee7fb5b781f959e922 Author: Oliver Gorwits Date: Sat Apr 27 14:05:25 2013 +0100 sorry, testing hook changes --- Netdisco/Changes | 23 +- Netdisco/Makefile.PL | 3 +- Netdisco/bin/nd-dbic-versions | 12 +- Netdisco/bin/nd-import-topology | 99 ++++ Netdisco/bin/netdisco-db-deploy | 15 +- Netdisco/bin/netdisco-deploy | 2 +- Netdisco/bin/netdisco-do | 2 + Netdisco/bin/netdisco-web-fg | 11 +- Netdisco/lib/App/Netdisco/Core/Arpnip.pm | 176 ++++++ .../DiscoverAndStore.pm => Core/Discover.pm} | 195 ++++-- Netdisco/lib/App/Netdisco/Core/Macsuck.pm | 457 ++++++++++++++ Netdisco/lib/App/Netdisco/DB.pm | 8 +- .../lib/App/Netdisco/DB/ExplicitLocking.pm | 165 ++++++ Netdisco/lib/App/Netdisco/DB/Result/Admin.pm | 42 ++ Netdisco/lib/App/Netdisco/DB/Result/Device.pm | 10 + .../lib/App/Netdisco/DB/Result/DevicePort.pm | 15 + Netdisco/lib/App/Netdisco/DB/Result/Node.pm | 2 +- .../App/Netdisco/DB/Result/NodeWireless.pm | 2 +- .../lib/App/Netdisco/DB/Result/Topology.pm | 3 + .../lib/App/Netdisco/DB/ResultSet/Admin.pm | 41 ++ .../lib/App/Netdisco/DB/ResultSet/Device.pm | 30 + .../lib/App/Netdisco/DB/ResultSet/Node.pm | 4 + .../lib/App/Netdisco/DB/ResultSet/NodeIp.pm | 4 + .../App/Netdisco/DB/ResultSet/NodeWireless.pm | 11 + .../lib/App/Netdisco/DB/ResultSet/Subnet.pm | 11 + .../App-Netdisco-DB-17-18-PostgreSQL.sql | 10 + .../App-Netdisco-DB-18-19-PostgreSQL.sql | 5 + .../App-Netdisco-DB-19-20-PostgreSQL.sql | 6 + Netdisco/lib/App/Netdisco/Daemon/Queue.pm | 2 +- .../App/Netdisco/Daemon/Worker/Interactive.pm | 2 +- .../lib/App/Netdisco/Daemon/Worker/Manager.pm | 2 +- .../lib/App/Netdisco/Daemon/Worker/Poller.pm | 4 +- .../Netdisco/Daemon/Worker/Poller/Arpnip.pm | 71 +++ .../Netdisco/Daemon/Worker/Poller/Device.pm | 72 +-- .../Netdisco/Daemon/Worker/Poller/Macsuck.pm | 71 +++ .../App/Netdisco/Daemon/Worker/Scheduler.pm | 7 +- .../lib/App/Netdisco/Manual/Configuration.pod | 142 +++++ .../lib/App/Netdisco/Manual/Deployment.pod | 12 +- .../App/Netdisco/Manual/WritingPlugins.pod | 18 + Netdisco/lib/App/Netdisco/Util/PortMAC.pm | 56 ++ Netdisco/lib/App/Netdisco/Util/SNMP.pm | 38 +- Netdisco/lib/App/Netdisco/Util/SanityCheck.pm | 115 ++++ Netdisco/lib/App/Netdisco/Web.pm | 16 +- Netdisco/lib/App/Netdisco/Web/AdminTask.pm | 80 +++ Netdisco/lib/App/Netdisco/Web/AuthN.pm | 15 +- Netdisco/lib/App/Netdisco/Web/Device.pm | 28 +- Netdisco/lib/App/Netdisco/Web/Plugin.pm | 103 +++- .../Netdisco/Web/Plugin/AdminTask/JobQueue.pm | 37 ++ .../Web/Plugin/AdminTask/PseudoDevice.pm | 103 ++++ .../Netdisco/Web/Plugin/AdminTask/Topology.pm | 103 ++++ .../Netdisco/Web/Plugin/Device/Addresses.pm | 2 +- .../App/Netdisco/Web/Plugin/Device/Details.pm | 2 +- .../Netdisco/Web/Plugin/Device/Neighbors.pm | 6 +- .../App/Netdisco/Web/Plugin/Device/Ports.pm | 4 +- .../App/Netdisco/Web/Plugin/Search/Device.pm | 2 +- .../App/Netdisco/Web/Plugin/Search/Node.pm | 2 +- .../App/Netdisco/Web/Plugin/Search/Port.pm | 2 +- .../App/Netdisco/Web/Plugin/Search/VLAN.pm | 2 +- Netdisco/lib/App/Netdisco/Web/PortControl.pm | 67 +-- Netdisco/lib/App/Netdisco/Web/Report.pm | 2 +- Netdisco/lib/App/Netdisco/Web/Search.pm | 10 +- Netdisco/lib/App/Netdisco/Web/Static.pm | 30 + Netdisco/lib/App/Netdisco/Web/TypeAhead.pm | 46 +- .../lib/App/NetdiscoX/Web/Plugin/Observium.pm | 21 + Netdisco/share/config.yml | 3 + Netdisco/share/public/css/jquery.qtip.min.css | 2 + Netdisco/share/public/css/nd_print.css | 4 +- Netdisco/share/public/css/netdisco.css | 561 +++++++++--------- .../smoothness/images/animated-overlay.gif | Bin 0 -> 1738 bytes .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 212 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 208 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 335 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 207 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 262 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 262 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 332 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 280 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 6922 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4549 bytes .../images/ui-icons_454545_256x240.png | Bin 0 -> 6992 bytes .../images/ui-icons_888888_256x240.png | Bin 0 -> 6999 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4549 bytes .../css/smoothness/jquery-ui.custom.min.css | 5 + .../javascripts/jquery-ui.custom.min.js | 6 + .../public/javascripts/jquery.qtip.min.js | 7 + Netdisco/share/public/javascripts/netdisco.js | 56 +- .../javascripts/netdisco_portcontrol.js | 4 +- Netdisco/share/views/admintask.tt | 42 ++ .../share/views/ajax/admintask/jobqueue.tt | 51 ++ .../views/ajax/admintask/pseudodevice.tt | 44 ++ .../share/views/ajax/admintask/topology.tt | 64 ++ Netdisco/share/views/ajax/device/addresses.tt | 6 +- Netdisco/share/views/ajax/device/details.tt | 29 +- Netdisco/share/views/ajax/device/ports.tt | 126 ++-- .../share/views/ajax/report/duplexmismatch.tt | 26 +- Netdisco/share/views/ajax/search/device.tt | 2 +- .../share/views/ajax/search/node_by_ip.tt | 2 +- .../share/views/ajax/search/node_by_mac.tt | 2 +- Netdisco/share/views/ajax/search/port.tt | 2 +- Netdisco/share/views/ajax/search/vlan.tt | 12 +- Netdisco/share/views/device.tt | 22 +- Netdisco/share/views/index.tt | 17 +- Netdisco/share/views/inventory.tt | 8 +- Netdisco/share/views/js/admintask.js | 120 ++++ Netdisco/share/views/js/common.js | 19 +- Netdisco/share/views/js/device.js | 167 +++--- Netdisco/share/views/js/report.js | 6 +- Netdisco/share/views/js/search.js | 13 +- Netdisco/share/views/layouts/main.tt | 50 +- .../plugin/observium/device_port_column.tt | 6 + .../views/plugin/observium/observium.css | 6 + .../share/views/plugin/observium/observium.js | 25 + Netdisco/share/views/report.tt | 15 +- Netdisco/share/views/search.tt | 18 +- Netdisco/share/views/sidebar/device/ports.tt | 38 +- Netdisco/share/views/sidebar/search/device.tt | 38 +- Netdisco/share/views/sidebar/search/node.tt | 6 +- Netdisco/share/views/sidebar/search/port.tt | 2 +- TODO | 11 +- 119 files changed, 3524 insertions(+), 778 deletions(-) create mode 100755 Netdisco/bin/nd-import-topology create mode 100644 Netdisco/lib/App/Netdisco/Core/Arpnip.pm rename Netdisco/lib/App/Netdisco/{Util/DiscoverAndStore.pm => Core/Discover.pm} (80%) create mode 100644 Netdisco/lib/App/Netdisco/Core/Macsuck.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-18-PostgreSQL.sql create mode 100644 Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-19-PostgreSQL.sql create mode 100644 Netdisco/lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-19-20-PostgreSQL.sql create mode 100644 Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm create mode 100644 Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm create mode 100644 Netdisco/lib/App/Netdisco/Manual/Configuration.pod create mode 100644 Netdisco/lib/App/Netdisco/Util/PortMAC.pm create mode 100644 Netdisco/lib/App/Netdisco/Util/SanityCheck.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/AdminTask.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Static.pm create mode 100644 Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm create mode 100644 Netdisco/share/public/css/jquery.qtip.min.css create mode 100644 Netdisco/share/public/css/smoothness/images/animated-overlay.gif create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-icons_222222_256x240.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-icons_2e83ff_256x240.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-icons_454545_256x240.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-icons_888888_256x240.png create mode 100644 Netdisco/share/public/css/smoothness/images/ui-icons_cd0a0a_256x240.png create mode 100644 Netdisco/share/public/css/smoothness/jquery-ui.custom.min.css create mode 100644 Netdisco/share/public/javascripts/jquery-ui.custom.min.js create mode 100644 Netdisco/share/public/javascripts/jquery.qtip.min.js create mode 100644 Netdisco/share/views/admintask.tt create mode 100644 Netdisco/share/views/ajax/admintask/jobqueue.tt create mode 100644 Netdisco/share/views/ajax/admintask/pseudodevice.tt create mode 100644 Netdisco/share/views/ajax/admintask/topology.tt create mode 100644 Netdisco/share/views/js/admintask.js create mode 100644 Netdisco/share/views/plugin/observium/device_port_column.tt create mode 100644 Netdisco/share/views/plugin/observium/observium.css create mode 100644 Netdisco/share/views/plugin/observium/observium.js 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 0000000000000000000000000000000000000000..d441f75ebfbdf26a265dfccd670120d25c0a341c GIT binary patch literal 1738 zcmZ|OX;ji_6b5ixNYt8>l?gOuO)6lU%W(mxn(`>1S(XO;u`D+P%xqBvMr|w-Vyr1s z7R|Cn0b8|Hu<=Zmv1mFqh9Fj!NuZfKB2MP$e75`XJ@>=!y!Ux9xR3x;EW!q1^V>X| znVFuRUN`NqJ2)ybXh%e__h!!pv(M|S3+?9F%(K}zyE40MGyhWF5-IDgL&=%2-9`Nk z!1@8uk4t%_{(K~>N;sK&dzJbwJ=$kYTlL=$%#0Pfh>U{%i@~wWbvYsD_K-D`&+u1( z#Ma`>%q<^UhzGvi(hyE`zCD{-=2|zL5>wnB=DE!U?(CZG%q4@lDnCq_%&3DCla#(X zmBhDD+RN$aMWWHm?ig*>1Onn6~r?Ma~N2JKAxN>H%UtRyRqS)6Um!-Tz%-r=& zQmTb^JFIe3W^-kAm`}`2P|niMh>RYyd)S^f(dbrx965?rzbhP|XeP}o&&DSZ4|oYQ z)I{f!SfycYw?3=9W;o-B%U5xs(pP267X~9-7L|4WzaYexC0GtG8wWygm63rF{llCEraxzkc=IxvFQ-y37=_;e5 zJLq^gsSO0Ayz?a>E_?{dmUc+t#qv$)XN8$<<}rQ#)lsiw+pmL&J>~+hgpo>i$m+;l zZIa_ZRIfSeT$~v5d`EBV&*k`apPgjv&B|+d`Q!nyu{L4rs%ZfoF0*Kq8I%ByOcFpL zK=>wzofZo<+0GZLCnWM3oQ^pb(gRSf02;~cEn@LJ>~XB9IkEX{$N#Z`m%>S!U{uPx zloI%bLdo$Adxlh(Uv^yX7s5G&C zLwNRG>~T?G{kzupp8EcyLGPoPf)@&9Wqfw_l&uU-6cexk%5;uQg%wb=0k_733{i#& z1a2p)gV3S2+QG1-K9tZ}E~I<(P0r2aFFY-c{o?TUOz3Xjod#TLE2A_c?*T7t z=1>~%YW450{Qqno4t`}gvLnuMrcu8+#xEBoY%2_+Mb#Z6S38+r*M4O`-+!zl(@m`D zQsi|GA2l3gEy}LFe<#Hv8?$_L#u8E|3-bP$*La*E>B{X!Sy4i6?TKam!49aXCAW4S*P_O^H4^*DpiA40o}Uqw~Eo&veh1`|8i zD2$x+>_b^bXE4N;AW=5>iYak2%!JAh0j1*k1{p#iRCjbB7!cSws~U{1IA@acLII$t z$>X#A+^s6iJ5~DFG!xa?>z{=lxtdi1rzbM-(nqAu3D8h-&64xo6|E!p?pK0xT;qoK z`6%+SpBk+~M?nO}>2mTw!A{yZ6O>Z@kwSd4;8aWU5z!P~tQl?u==^+R`{OmOS}oZh zOXQ3{6kuz?Is^n^L7;9ieB9C+8B{>t+pDrlq4xGDDn#T#3T5$l1g`FTQkU;b-981j zNm{zC`$wn7etklM#qHI4=3m5gwa6DNS{?Z!vSObi_od{4eUo=_S2BKNpkSdiqe(k9WtkeM79;2-%CFbb)aB=&H1?i1}uwFzoZQ(38Kn1zBP ORn*B%u*Wk|4g3!*Rv{Mv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..277ea338e2b34c5b96dd0527013e2bec47fd7dc9 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F1SA+{?>A)!QcOwS?k)_>#w|r1Kptm-M`SUO z_5fqIli7AahM1>|V~EA+ zRdP`(kYX@0Ff`URFa@FzLrW`DQ!67QT?2C~0|ODJ$KEI!a`RI%(<*UmNR5yF4%EQl M>FVdQ&MBb@0AW}%^Z)<= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f094d9aa94cb1f8b48fa6d3297882e5e5f11b78f GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^8bF-F2qYNp$opRhQcOwS?k)_Bce{j_0C}7R9+AaB z+5?Q;PG;Ky8A6^ejv*T7lYj6t@hpC#;TbB#aBAWwna#KLs)4eqC9V-ADTyViR>?)F zK#IZ0z|dINz!Zo=3@xopO|6WKbPdd{3=Bk=9($u`$jwj5OsmALAvHewJ5U3Ir>mdK II;Vst0JIb{*Z=?k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2e28622a2d52e2d482bf936a8f0731399ceba9e8 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^j6gI&fCnc6a#?2AmP!?*K(O3p^r= zfwTu0yPeFo12TF&T^vI^j=w#x$i?I+((tf;UXnmgbH|3oY>pC!)f}(GR!16S-u+#{ ze6YEqRkW=8vGl=5qArKM<9}TC-}iEvB{zdaTcX5$wyRTK&ALbP0 Hl+XkK(RVVG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..68160b98f6dd5aa0b0f72af49d022c32c8943bc5 GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^j6gI&0LWmFTHNUZq?nSt-Ch3w7g=q17Rci)@Q5r1 z(jH*!b~4)z#PD=+46!(!T=8puqDZgOs>RXUCGx5b?-VBQkUm|IuXOmYJrBRJgj{Vx zMbNnqUkncy+qa2-mWYc>swkcIuvGK#>(0d)B7)5f`@$Ei28nH~0h*~=;u=wsl30>z zm0Xkxq!^4042^XSOo1rG(9+7()XK<6*TCG$z(9oQu{Vl_-29Zxv`X9>Qsbk)12r&s My85}Sb4q9e04fMg+W-In literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..21f39d18fd9d99ceb6f2a7b62b2af8ae35cf69e4 GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^j6gI&0LWmFTHNUZq?nSt-Ch3w7g=q17Rci)@Q5r1 z(jH*!b~4)z#PD=+46!(!TrvH)L6@80)r*_cdCvDr%)6ghVL16=s@mbz7H!uRdGeDa z?kzLg)16i!f8fKx84s0>4Kd3^85pR(`gsjSLvDUbW?Cg~4I$edvw#{H NJYD@<);T3K0RYwDPayyR literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b2c983ccc4699adf835a864238ddb59180b2b2da GIT binary patch literal 332 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 QJ~;stWz}Trq%1=J3#jBGg8%>k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..84b601bf0f726bf95801da487deaf2344a32e4b8 GIT binary patch literal 4549 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 F{{S81aohj^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b6db1acdd433be80a472b045018f25c7f2cf7e08 GIT binary patch literal 6992 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ed5b6b0930f672fa08e9b9bdbe5e55370fd1dc30 GIT binary patch literal 4549 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 DTfr$v literal 0 HcmV?d00001 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("