├── .gitignore ├── LICENSE ├── README.md ├── agg_builder_template.php ├── aggregates.php ├── ajax ├── deleteAggregate.php ├── getRRDNamesForHost.php └── saveAggregate.php ├── config.php.sample ├── fitb.css ├── fitb.js ├── fitb.sql ├── functions.php ├── graph.php ├── header.php ├── index.php ├── poller.php ├── poller_child.php ├── rrds └── .empty_dir ├── search.php ├── side.php ├── viewaggregate.php ├── viewgraph.php ├── viewhost.php └── viewport.php /.gitignore: -------------------------------------------------------------------------------- 1 | poller.log* 2 | config.php 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Laurie Denness 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FITB 2 | 3 | ## What is FITB? 4 | 5 | __FITB__ (_fit-bee_) or __"Fill in the blank"__ is a PHP and RRDtool based web interface designed to make polling every 6 | switch or router on your network easier. Think Cacti but simpler and automated. 7 | 8 | ## Features 9 | 10 | FITB automatically polls every port on a list of switches you give it. It's feature list includes: 11 | 12 | * Simple configuration: 1 line to add a new switch, just give it a name, address and SNMP community 13 | * Precise polling: 1 minute poll intervals to make sure you never miss a spike in your network 14 | * Easy searching: Search both interface aliases and names, and filter down by graph type and host 15 | * Automatic discovery: FITB finds every port in use on the switch and graphs it, and stops when it goes down. 16 | * Dynamic aggregate graph creation: shift click graphs to build on-the-fly aggregates and save them for later. 17 | 18 | Screenshots and a guide to FITB are available here: [http://www.flickr.com/photos/lozzd/sets/72157627375145065](http://www.flickr.com/photos/lozzd/sets/72157627375145065) 19 | 20 | ## Prerequisites 21 | * A webserver 22 | * PHP (including CLI) 23 | * RRDtool 24 | * MySQL 25 | * Cron (to run the poller, although you can run by hand/in a loop) 26 | * Some network switches supporting the standard network MIBs and SNMPv2 27 | 28 | ## Installation 29 | 1. Download/clone the repo into an appropriate folder either in your webservers directory or symlinked to it 30 | 2. Create a new database for FITB to keep it's state in. For example: 31 | 32 | mysql> create database fitb; 33 | mysql> grant all on fitb.* to fitbuser@localhost IDENTIFIED BY 'f1tbP4ss'; 34 | 35 | 3. Load the database structure into your database: 36 | 37 | # mysql fitb < fitb.sql 38 | 39 | 4. Check permissions on the directory for your RRD files. By default, this is the "rrds" directory in the FITB root. Make sure this directory is writeable by the user you wish to run the poller as. Either as you, or create a new user just for FITB. Your webserver only needs read only access. 40 | 5. Move config.php.sample to config.php, edit with your favourite editor and set the database connection information, and the path you created for your RRDs. 41 | 6. At this point you should be able to load FITB in your browser without errors. Reward yourself with a beverage. 42 | 43 | ## Configuring switches 44 | 45 | Configuring switches in FITB is designed to be as painless as possible. The procedure is as follows: 46 | 47 | 1. Open config.php 48 | 2. Copy a previous (or the example) line and edit it 49 | 3. Save the file. The next time the poller runs (or when you run your poller) the graphs will be created. 50 | 51 | The config line per switch is made up of the following: 52 | 53 | "switchname" => array("prettyname" => "switchname", "enabled" => true, "showoninterface" => true, "ip" => "switchname.yourcompany.com", "snmpcommunity" => "public", "graphtypes" => array('bits','ucastpkts','errors')), 54 | 55 | * switchname - The name used for the config file. 56 | * prettyname - Generally keep this the same as above. Keep it simple.. It is used for the filename and in the interface 57 | * enabled - True/false: Disables or enables polling of this host 58 | * showoninterface - True/false: Allows you to hide a switch from the interface menus 59 | * ip - The hostname or IP address of the host 60 | * snmpcommunity - The SNMP community of the host 61 | * graphtypes - An array of the types of graphs you want for this host. Currently supported: 62 | * bits - bits/sec, in and out. 63 | * ucastpkts - Unicast packets/sec, in and out. 64 | * errors - Errors/sec in and out, and discards/sec in and out 65 | * mcastpkts - Multicast packets/sec, in and out. 66 | * bcastpkts - Broadcast packets/sec, in and out 67 | 68 | ## Setting up polling 69 | 70 | FITB is designed to poll every minute. To achieve this, I recommend setting up the poller parent to run in your favourite 71 | crond on your host. 72 | 73 | For example: 74 | 75 | */1 * * * * /usr/bin/php /var/www/fitb/poller.php >> /var/www/fitb/poller.log 2>&1 76 | 77 | The poller will spawn a child for every host in its configuration, and also run a cleanup job for any graphs due to be purged 78 | (purging of out of date/stale graphs is controlled from the config). 79 | 80 | The poller child grabs the information for every port on that host, and if the port is in an UP state, creates/updates that 81 | port's graph. It also updates the port's alias if it has changed. 82 | 83 | If a port goes down, it becomes marked as STALE and it will be pushed to the bottom of the interface. There is a configurable 84 | purge which also removes the graphs from the interface completely and removes the file from disk if it passes a certain age, 85 | thus keeping your disk clean of out of date ports. 86 | 87 | ## The FITB interface 88 | 89 | I highly suggest checking the [screenshot guide](http://www.flickr.com/photos/lozzd/sets/72157627375145065) for information on how the interface is laid out 90 | 91 | The interface is designed to be as simple as possible: A small status line in the top right, along with the search function, 92 | and a list of hosts down the left. 93 | 94 | As you drill down through the different graph types and hosts, the search filters in the top right change automatically. 95 | This means at any point you can expand or filter down your search to find the port you want quickly. 96 | 97 | The search function allows for searching of both interface names and aliases, and the filter drop downs (graph type/host) 98 | update the view instantly. 99 | 100 | There is a time period drop down in the top right that affects all the graphs in the current view. Due to FITB's 1 minute 101 | polling that means you can go down to a 5 minute view of any port. 102 | 103 | Aggregate graphs can be created by shift-clicking desired component graphs. When one or more graphs have been added, a status indicator will appear on the bottom right of the page. Click the link in the status area to open the aggregate editor, fine tune your graph, and save for later. Note that this feature requires a modern browser. 104 | 105 | ## Known issues/limitations 106 | * Your hosts must support SNMPv2, SNMPv1 did not have the information/resolution I required so it was written with v2 107 | in mind. most switches support this though. 108 | * Keep this inside your network. I can't be held responsible for massive security holes. 109 | * PHP versions 5.3.0 and higher require that the time zone be explicitly set for date functions. You can either set 110 | your system date globally in php.ini, or you can set it in the config.php using a date_default_timezone_set() call. 111 | -------------------------------------------------------------------------------- /agg_builder_template.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | x 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | Add another graph 18 | 19 | 20 |
21 | 22 | 23 | 24 |
25 |
26 |
-------------------------------------------------------------------------------- /aggregates.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | FITB - Aggregates 14 | 15 | 16 | 17 | 18 | 19 | 20 | for the header ?> 21 |
22 | for the side bar ?> 23 |
24 |

View Aggregates

25 | 0) { 30 | echo ''; 46 | echo '

* Shift-click a graph to start building a new aggregate.

'; 47 | echo '
'; 48 | 49 | } else { 50 | echo "No aggregates were found. Shift-click a graph to start building an aggregate."; 51 | } 52 | } else { 53 | echo "
Connection to FITB database failed, have you set up the database and specified the correct connection parameters in config.php?"; 54 | } 55 | ?> 56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /ajax/deleteAggregate.php: -------------------------------------------------------------------------------- 1 | $deleted); 9 | echo json_encode($res); -------------------------------------------------------------------------------- /ajax/getRRDNamesForHost.php: -------------------------------------------------------------------------------- 1 | $friendlytitle, 19 | 'type' => $type, 20 | 'stack' => $stack 21 | ); 22 | $agg_id = saveAggregate($graphs_array, $meta); 23 | } else { 24 | $agg_id = null; 25 | } 26 | 27 | $success = !is_null($agg_id); 28 | 29 | $res = array('success' => $success, 'aggregate_id' => $agg_id); 30 | echo json_encode($res); 31 | -------------------------------------------------------------------------------- /config.php.sample: -------------------------------------------------------------------------------- 1 | array("prettyname" => "yourswitchname", "enabled" => true, "showoninterface" => true, "ip" => "yourswitchname.yourcompany.com", "snmpcommunity" => "public", "graphtypes" => array('bits','ucastpkts','errors')), 12 | # more hosts go here 13 | ); 14 | 15 | # Verbosity. Choose your level of logging here. This affects everything that logs, which at the minute is 16 | # just the poller and it's child processes. 17 | # 18 | # 0 for almost nothing, 1 for some information, 2 for lots of information. 19 | $verbose = 1; 20 | 21 | # The path to your desired rrdtool binary. 22 | $path_rrdtool = "/usr/bin/rrdtool"; 23 | 24 | # The path where you want to store your RRD files. The default is the "rrds" directory beneath this file. 25 | $path_rrd = dirname(__FILE__) . "/rrds/"; 26 | 27 | 28 | # Feel free to adjust these RRA definitions. Default stores 1 month at 1 minute resolution, and 1 year at 60 minutes 29 | $RRA_average = "RRA:AVERAGE:0.5:1:44640 RRA:AVERAGE:0.5:60:8760 "; 30 | $RRA_max = "RRA:MAX:0.5:1:44640 RRA:MAX:0.5:60:8760"; 31 | 32 | # Also send data to graphite. 33 | # $carbon_host = "graphite.example.org"; 34 | # $carbon_port = 2009; 35 | # $graphite_prefix = "network"; 36 | # $graphite_metrics = array("inerrors","outdiscards"); 37 | # $graphite_datacenter = "dc1"; 38 | 39 | # Database connection parameters 40 | $mysql_host = "localhost"; 41 | $mysql_user = "fitbuser"; 42 | $mysql_pass = "f1tbP4ss"; 43 | $mysql_db = "fitb"; 44 | 45 | # Old age: 46 | # Mark graphs older than this many seconds as stale 47 | $staleage = 1800; 48 | 49 | # DELETE graphs that have been stale for this many seconds 50 | # WARNING! You will lose your data if you don't set this correctly! 51 | # You have been warned! 52 | # Set to 0 to disable deletion of ports that have since been downed. 53 | $purgeage = 2592000; 54 | 55 | # Time periods: 56 | # An array of time periods that you wish to be selectable from the dropdown at the top of every page. 57 | # E.g. 300 seconds = 5 minutes 58 | $configtimeperiods = array (-300 => '5 minutes', -3600 => '1 hour', -7200 => '2 hours', -14400 => '4 hours', -43200 => '12 hours', -86400 => '1 day', -172800 => '2 days', -604800 => '7 days', -1209600 => '14 days', -2678400 => '1 month'); 59 | 60 | $default_duration = "-86400"; 61 | 62 | # PHP Error reporting 63 | # Incase you want to debug my crappy code, change this 64 | error_reporting(E_ERROR | E_WARNING | E_PARSE); 65 | 66 | # Uncomment if you are using PHP version 5.3.0 or newer. PHP 5.3.0+ require timezone to be set or it 67 | # will produce a line of warning output for every port the poller calls date functions. Adjust to match 68 | # your system time configuration, such as 'America/Los_Angeles', etc) 69 | # date_default_timezone_set('UTC'); 70 | 71 | # This file ends WITHOUT the normal php closing tag. Do not change. 72 | -------------------------------------------------------------------------------- /fitb.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | font-family: "Lucida Grande"; 4 | margin: 0px; 5 | } 6 | 7 | .graphcaption { 8 | font-size: 10px; 9 | } 10 | 11 | .headerinfo { 12 | font-size: 10px; 13 | margin-bottom: 5px; 14 | text-align: right; 15 | } 16 | 17 | #wrap { 18 | margin:0 auto; 19 | margin-top: -15px; 20 | } 21 | 22 | #side { 23 | float: left; 24 | margin-left: 5px; 25 | margin-top: 0px; 26 | padding-top: 0px; 27 | width: 200px; 28 | font-size: 0.9em; 29 | } 30 | 31 | #side ul { 32 | padding-left: 1em; 33 | margin-left: 1em; 34 | } 35 | 36 | #main { 37 | margin-left: 210px; 38 | } 39 | 40 | h1 { 41 | margin: 2px; 42 | } 43 | 44 | h2 { 45 | margin-bottom: 10px; 46 | } 47 | 48 | #header { 49 | height: 50px; 50 | background: #D36B00 51 | } 52 | 53 | #headside { 54 | float: left; 55 | color: white; 56 | padding: 4px; 57 | padding-left: 10px; 58 | } 59 | 60 | #headside a { 61 | color: white; 62 | } 63 | 64 | #headright { 65 | float: right; 66 | margin-top: 5px; 67 | margin-right: 5px; 68 | color: white; 69 | } 70 | 71 | form { 72 | display: inline; 73 | } 74 | 75 | img { 76 | border: none 77 | } 78 | 79 | .red { 80 | color: red; 81 | } 82 | 83 | .green { 84 | color: green; 85 | } 86 | 87 | .small { 88 | font-size: 0.8em; 89 | margin: 4px; 90 | } 91 | 92 | p.help-text { 93 | color: #999; 94 | font-size: .8em; 95 | } 96 | 97 | #host-graphs, #aggregate-graphs { 98 | list-style-type: none; 99 | } 100 | #host-graphs li, #aggregate-graphs li { 101 | display: inline; 102 | margin: 2px; 103 | display: inline-block; 104 | vertical-align: top; 105 | } 106 | .sortable-placeholder { 107 | background-color: #FBF9EE; 108 | 109 | } 110 | 111 | #aggregate-builder-status { 112 | position: fixed; 113 | bottom: 2; 114 | right: 20; 115 | width: 350px; 116 | padding: 5px; 117 | background-color: rgba(231, 144, 64, 0.90); 118 | border: 1px solid #FF7000; 119 | display: none; 120 | font-size: .9em; 121 | color: #000000; 122 | } 123 | #aggregate-builder-status a { 124 | color: #033999; 125 | text-decoration: none; 126 | } 127 | #aggregate-builder-status a:hover { 128 | text-decoration: underline; 129 | } 130 | 131 | #aggregate-builder-status .agg-builder-error { 132 | color: #AA0000; 133 | } 134 | 135 | #aggregate-builder-status.updated { 136 | border: 3px solid red; 137 | } 138 | 139 | #aggregate-graph-overlay { 140 | position: fixed; 141 | top: 70px; 142 | height: 460px; 143 | width: 650px; 144 | left: 50%; 145 | margin-left: -300px; 146 | padding: 20px; 147 | background-color: #DEDEDE; 148 | border: 1px solid #999; 149 | display: none; 150 | overflow-y: auto; 151 | } 152 | 153 | #aggregate-graph-overlay img { 154 | margin: auto; 155 | display: block; 156 | } 157 | 158 | #aggregate-graph-overlay #aggregate-img { 159 | min-height: 200px; 160 | } 161 | 162 | #aggregate-graph-overlay .close-icon { 163 | position: absolute; 164 | top: 6px; 165 | right: 6px; 166 | padding: 0 5px 2px; 167 | cursor: pointer; 168 | background-color: #eee; 169 | color: #999; 170 | font-size: 0.8em; 171 | } 172 | 173 | #aggregate-graph-overlay .close-icon:hover { 174 | background-color: #ccc; 175 | color: #333; 176 | } 177 | 178 | 179 | #edit-aggregate-tools { 180 | font-size: .8em; 181 | } 182 | 183 | #edit-aggregate-tools #add-aggregate-link { 184 | cursor: pointer; 185 | font-size: .9em; 186 | display: block; 187 | margin-top: 5px; 188 | color: #D87900; 189 | } 190 | #edit-aggregate-tools #add-aggregate-link:before { 191 | content: '+ '; 192 | } 193 | 194 | #edit-aggregate-tools #add-aggregate-link:hover { 195 | text-decoration: underline; 196 | } 197 | 198 | #edit-aggregate-tools .edit-graph-form { 199 | margin-top: 5px; 200 | } 201 | 202 | #edit-aggregate-tools .edit-graph-form input, #edit-aggregate-tools .edit-graph-form select { 203 | margin-right: 3px; 204 | } 205 | #edit-aggregate-tools .edit-graph-form input.edit-graph-form-host { 206 | width: 80px; 207 | } 208 | #edit-aggregate-tools .edit-graph-form input.edit-graph-form-rrdname { 209 | width: 180px; 210 | } 211 | 212 | #edit-aggregate-tools .edit-graph-form input.edit-graph-form-custom-label { 213 | width: 140px; 214 | } 215 | 216 | 217 | #edit-aggregate-tools .edit-graph-form input.edit-graph-form-color { 218 | width: 50px; 219 | vertical-align: bottom; 220 | padding: 0; 221 | } 222 | 223 | #edit-aggregate-tools .edit-graph-form .remove-link { 224 | margin-right: 3px; 225 | cursor: pointer; 226 | } 227 | 228 | #edit-aggregate-tools .edit-graph-form .remove-link:hover { 229 | color: #ff0000; 230 | } 231 | 232 | #edit-aggregate-tools input#aggregate-title{ 233 | width: 200px; 234 | } 235 | 236 | #edit-aggregate-tools input.meta-field { 237 | margin-right: 5px; 238 | } 239 | 240 | 241 | /* some styles from jqueryui for autocomplete menus */ 242 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 243 | .ui-autocomplete { 244 | position: absolute; 245 | top: 0; 246 | left: 0; 247 | cursor: default; 248 | font-size: .9em; 249 | } 250 | 251 | .ui-menu { list-style:none; padding: 2px; margin: 0; display:block; outline: none; } 252 | .ui-menu .ui-menu { margin-top: -3px; position: absolute; } 253 | .ui-menu .ui-menu-item { margin: 0; padding: 0; zoom: 1; width: 100%; } 254 | .ui-menu .ui-menu-divider { margin: 5px -2px 5px -2px; height: 0; font-size: 0; line-height: 0; border-width: 1px 0 0 0; } 255 | .ui-menu .ui-menu-item a { text-decoration: none; display: block; padding: 2px .4em; line-height: 1.5; zoom: 1; font-weight: normal; } 256 | .ui-menu .ui-menu-item a.ui-state-focus, 257 | .ui-menu .ui-menu-item a.ui-state-active { font-weight: normal; margin: -1px; } 258 | 259 | .ui-menu .ui-state-disabled { font-weight: normal; margin: .4em 0 .2em; line-height: 1.5; } 260 | .ui-menu .ui-state-disabled a { cursor: default; } 261 | 262 | /* Component containers 263 | ----------------------------------*/ 264 | .ui-widget-content { border: 1px solid #dddddd; background: #eeeeee 50% top repeat-x; color: #333333; } 265 | .ui-widget-content a { color: #333333; } 266 | .ui-widget-header { border: 1px solid #e78f08; background: #f6a828 50% 50% repeat-x; color: #ffffff; font-weight: bold; } 267 | .ui-widget-header a { color: #ffffff; } 268 | 269 | /* Interaction states 270 | ----------------------------------*/ 271 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #cccccc; background: #f6f6f6 50% 50% repeat-x; font-weight: bold; color: #1c94c4; } 272 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #1c94c4; text-decoration: none; } 273 | .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 #fbcb09; background: #fdf5ce 50% 50% repeat-x; font-weight: bold; color: #c77405; } 274 | .ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #c77405; text-decoration: none; } 275 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #fbd850; background: #ffffff 50% 50% repeat-x; font-weight: bold; color: #eb8f00; } 276 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #eb8f00; text-decoration: none; } 277 | 278 | /* Interaction Cues 279 | ----------------------------------*/ 280 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fed22f; background: #ffe45c 50% top repeat-x; color: #363636; } 281 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } 282 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a; background: #b81900 50% 50% repeat; color: #ffffff; } 283 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #ffffff; } 284 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #ffffff; } 285 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 286 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 287 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 288 | .ui-state-disabled .ui-icon { filter:Alpha(Opacity=35); } /* For IE8 - See #6059 * 289 | 290 | /* Corner radius */ 291 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; -khtml-border-top-left-radius: 4px; border-top-left-radius: 4px; } 292 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; -khtml-border-top-right-radius: 4px; border-top-right-radius: 4px; } 293 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; -khtml-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; } 294 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; -khtml-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; } -------------------------------------------------------------------------------- /fitb.js: -------------------------------------------------------------------------------- 1 | var GraphManager = function() {}; 2 | 3 | GraphManager.prototype = { 4 | graphs: null, 5 | 6 | init: function(graph_img_selector) { 7 | this.status_el = $('#aggregate-builder-status'); 8 | this.initAggregateBuilder(graph_img_selector); 9 | 10 | this.status_el.on('click', 'a.show-agg-builder', (function(event) { 11 | event.preventDefault(); 12 | this.aggregateBuilder.show(); 13 | }).bind(this)); 14 | 15 | this.status_el.on('click', 'a.reset-agg-builder', (function(event) { 16 | event.preventDefault(); 17 | this.aggregateBuilder.reset(); 18 | this.status_el.hide(); 19 | }).bind(this)); 20 | }, 21 | 22 | getAggregateGraphData: function(aggParts, meta) { 23 | meta = meta || {}; 24 | var data = {}; 25 | meta.height = meta.height || 200; 26 | meta.width = meta.width || 500; 27 | $.extend(data, meta); 28 | data.host = aggParts.map(function(g) { return g.host; } ).join('|'); 29 | data.rrdname = aggParts.map(function(g) { return g.rrdname; } ).join('|'); 30 | data.subtype = aggParts.map(function(g) { return g.subtype; } ).join('|'); 31 | data.color = aggParts.map(function(g) { return g.color; } ).join('|'); 32 | data.graphing_method = aggParts.map(function(g) { return g.graphing_method; } ).join('|'); 33 | data.custom_label = aggParts.map(function(g) { return g.custom_label; } ).join('|'); 34 | data.count = aggParts.length; 35 | return data; 36 | }, 37 | 38 | getDeserializedQueryString: function() { 39 | var data = {}; 40 | var querystring = window.location.search; 41 | if (querystring && querystring != '') { 42 | var kvs = querystring.substring(1).split('&'); 43 | var duration = '' 44 | kvs.map(function(e) { 45 | var kv = e.split('='); 46 | data[kv[0]] = kv.length ? kv[1] : ''; 47 | }); 48 | } 49 | return data; 50 | }, 51 | 52 | getAggregateGraphUrl: function(aggParts, meta) { 53 | var data = this.getAggregateGraphData(aggParts, meta); 54 | 55 | var qs = this.getDeserializedQueryString(); 56 | if (qs.duration) { 57 | data.duration = qs.duration; 58 | } 59 | return 'graph.php?' + Object.keys(data).map(function(k) { return [k, encodeURIComponent(data[k])].join('='); } ).join('&'); 60 | }, 61 | 62 | saveAggregate: function(aggParts, meta) { 63 | if (!aggParts) return false; 64 | var agg = { 65 | aggParts: aggParts, 66 | meta: meta 67 | }; 68 | $.post('ajax/saveAggregate.php', this.getAggregateGraphData(aggParts, meta)) 69 | .done(this.saveSuccess.bind(this)) 70 | .fail(this.saveError.bind(this)); 71 | this.aggregates.push(agg); 72 | }, 73 | 74 | saveSuccess: function(data) { 75 | if (data.success) { 76 | this.aggregateBuilder.reset(); 77 | this.aggregateBuilder.hide(); 78 | var agg_id = data.aggregate_id; 79 | this.showStatusMessage('aggregate saved! view aggregate | view all', 8000); 80 | } else { 81 | this.saveError.apply(this, arguments); 82 | } 83 | }, 84 | 85 | saveError: function() { 86 | console.error(arguments); 87 | }, 88 | 89 | initAggregateBuilder: function(graph_img_selector) { 90 | var aggParts = null; 91 | if ('localStorage' in window && window['localStorage'] !== null) { 92 | aggParts = JSON.parse(localStorage.getItem("aggregateGraphParts")); 93 | } 94 | this.aggregateBuilder = new GraphManager.AggregateBuilder(this, aggParts, graph_img_selector); 95 | this.aggregateBuilder.init(); 96 | }, 97 | 98 | showStatusMessage: function(msg, duration) { 99 | this.status_el.find('.agg-builder-message').html(msg).show(); 100 | this.status_el.find('.agg-builder-error').hide(); 101 | this.status_el.removeClass('error').show(); 102 | if (this.status_msg_timeout) { 103 | clearTimeout(this.status_msg_timeout); 104 | this.status_msg_timeout = null; 105 | } 106 | if (duration) { 107 | this.status_msg_timeout = setTimeout((function() { 108 | this.status_el.fadeOut(); 109 | }).bind(this), duration); 110 | } 111 | }, 112 | 113 | showStatusError: function(msg, duration) { 114 | this.status_el.find('.agg-builder-message').hide(); 115 | this.status_el.find('.agg-builder-error').html(msg).show(); 116 | this.status_el.addClass('error').show(); 117 | if (this.status_msg_timeout) { 118 | clearTimeout(this.status_msg_timeout); 119 | this.status_msg_timeout = null; 120 | } 121 | if (duration) { 122 | this.status_msg_timeout = setTimeout((function() { 123 | this.status_el.fadeOut((function() { 124 | this.onAggregateBuilderUpdate(this.aggregateBuilder.aggParts.length); 125 | }).bind(this)); 126 | }).bind(this), duration); 127 | } 128 | }, 129 | 130 | onAggregateBuilderUpdate: function(numAggParts) { 131 | if (numAggParts > 0) { 132 | this.showStatusMessage( 133 | '' + 134 | 'show aggregate builder (' + numAggParts + ' graph' + (numAggParts == 1 ? '' : 's') + ')' + 135 | ' | ' + 136 | 'reset' 137 | ); 138 | } 139 | this.status_el.toggle(numAggParts > 0); 140 | }, 141 | 142 | onAggregateBuilderError: function(errorMsg) { 143 | this.showStatusError(errorMsg, 8000); 144 | } 145 | 146 | }; 147 | 148 | GraphManager.AggregateBuilder = function(graphManager, aggParts, graph_img_selector) { 149 | this.graphManager = graphManager; 150 | this.aggParts = aggParts || []; 151 | this.inited = false; 152 | this.graph_img_selector = graph_img_selector; 153 | }; 154 | 155 | GraphManager.AggregateBuilder.prototype = { 156 | init: function() { 157 | $(this.graph_img_selector).click((function(event) { 158 | if (event.shiftKey) { 159 | event.preventDefault(); 160 | this.addToAggregate($(event.target)); 161 | } 162 | }).bind(this)) 163 | .attr('title', 'shift-click to add to aggregate'); 164 | 165 | this.overlay = $('#aggregate-graph-overlay'); 166 | this.overlay.on('click', '.close-icon', this.hide.bind(this)); 167 | this.overlay.on('click', '#add-aggregate-link', this.addGraphForm.bind(this)); 168 | 169 | this.overlay.on('click', '#do-update-aggregate', this.update.bind(this)); 170 | this.overlay.on('click', '#do-save-aggregate', this.save.bind(this)); 171 | this.overlay.on('click', '#do-reset-aggregate', this.reset.bind(this)); 172 | 173 | this.overlay.on('change', '#aggregate-type-selector input', this.changeType.bind(this)); 174 | 175 | this.forms = []; 176 | this.type = this.aggParts.length > 0 ? this.aggParts[0].type : null; 177 | 178 | $.each(this.aggParts, (function (i, aggPart) { 179 | this.addGraphForm(aggPart); 180 | }).bind(this)); 181 | this.inited = true; 182 | this.updateUI(); 183 | this.graphManager.onAggregateBuilderUpdate(this.aggParts.length); 184 | }, 185 | 186 | addToAggregate: function(img) { 187 | var data = this.extractGraphData(img.attr('src')); 188 | if (data.type != null && (!this.type || data.type == this.type)) { 189 | this.type = data.type; 190 | this.aggParts.push(data); 191 | this.saveAggParts(); 192 | this.addGraphForm(data); 193 | this.updateUI(); 194 | this.graphManager.onAggregateBuilderUpdate(this.aggParts.length); 195 | } else { 196 | this.graphManager.onAggregateBuilderError('please select a graph of the same type as the others in this aggregate (' + this.type + ')'); 197 | } 198 | }, 199 | 200 | extractGraphData: function(url) { 201 | var kvs = url.replace('graph.php?', '').split('&'); 202 | var data = {}; 203 | $.each(kvs, function(i, kvp) { 204 | var kv = kvp.split('='); 205 | data[kv[0]] = kv.length > 0 ? kv[1] : ''; 206 | }); 207 | return data; 208 | }, 209 | 210 | addGraphForm: function(data) { 211 | var form = new GraphManager.GraphEditForm(this.type, data, this.forms.length); 212 | this.overlay.find('#graph-forms').append(form.getForm()); 213 | this.forms.push(form); 214 | this.updateUI(false); 215 | return form; 216 | }, 217 | 218 | update: function() { 219 | this.aggParts = this.forms.filter(function(el) { return el.enabled; } ).map(function(f) { 220 | return f.getData(); 221 | }); 222 | this.saveAggParts(); 223 | this.updateUI(); 224 | this.graphManager.onAggregateBuilderUpdate(this.aggParts.length); 225 | }, 226 | 227 | show: function() { 228 | this.overlay.show(); 229 | }, 230 | 231 | hide: function() { 232 | this.overlay.hide(); 233 | }, 234 | 235 | updateUI: function(redraw) { 236 | if (!this.inited) return; 237 | var hasAggParts = this.aggParts.length > 0; 238 | var hasType = !!this.type; 239 | var hasAggForms = this.forms.filter(function(f) { return f.enabled; }).length > 0; 240 | if (redraw !== false) { 241 | if (hasAggParts) { 242 | var imgUrl = this.graphManager.getAggregateGraphUrl(this.aggParts, this.getMeta()); 243 | this.overlay.find('#aggregate-img').html($('').error(this.imgError.bind(this)).attr('src', imgUrl)).show(); 244 | } else { 245 | this.overlay.find('#aggregate-img').empty().hide(); 246 | } 247 | } 248 | 249 | var aggTypeSelector = this.overlay.find('#aggregate-type-selector'); 250 | 251 | aggTypeSelector.find('input').attr('disabled', hasAggParts || hasAggForms ? 'disabled' : null).attr('checked', null); 252 | if (hasType) { 253 | aggTypeSelector.find('input[value="' + this.type + '"]').attr('checked', 'checked'); 254 | } 255 | 256 | this.overlay.find('#add-aggregate-link').toggle(hasType); 257 | this.overlay.find('.meta-field').toggle(hasAggForms); 258 | 259 | $('#aggregate-builder-status').toggle(hasAggParts); 260 | }, 261 | 262 | getMeta: function() { 263 | var meta = {}; 264 | meta.friendlytitle = $.trim(this.overlay.find('#edit-aggregate-tools input#aggregate-title').val()); 265 | meta.stack = this.overlay.find('#edit-aggregate-tools input#aggregate-stack').is(':checked'); 266 | meta.type = this.type; 267 | return meta; 268 | }, 269 | 270 | imgError: function() { 271 | this.overlay.find('#aggregate-img').html($('There was a problem loading this graph.')); 272 | }, 273 | 274 | saveAggParts: function() { 275 | if ('localStorage' in window && window['localStorage'] !== null) { 276 | localStorage.setItem("aggregateGraphParts", JSON.stringify(this.aggParts)); 277 | } 278 | }, 279 | 280 | save: function() { 281 | this.graphManager.saveAggregate(this.aggParts, this.getMeta()); 282 | }, 283 | 284 | changeType: function() { 285 | var selectedType = this.overlay.find('#aggregate-type-selector input:checked').val(); 286 | this.type = selectedType; 287 | this.updateUI(); 288 | }, 289 | 290 | reset: function() { 291 | $.each(this.forms, function(i, f) { 292 | f.destroy(); 293 | }); 294 | this.forms = []; 295 | this.aggParts = []; 296 | this.type = null; 297 | this.overlay.find('#edit-aggregate-tools input#aggregate-title').val(''); 298 | this.overlay.find('#edit-aggregate-tools input#aggregate-stack').attr('checked', 'checked'); 299 | this.update(); 300 | } 301 | }; 302 | 303 | GraphManager.GraphEditForm = function(type, data, idx) { 304 | this.type = type; 305 | this.data = data || {}; 306 | this.idx = idx; 307 | this.portsByHost = {}; 308 | this.form = this.createForm(); 309 | this.form.find('.edit-graph-form-host').on("autocompletechange change", (function() { 310 | this.form.find('.edit-graph-form-rrdname').val(''); 311 | }).bind(this)); 312 | this.form.find('.remove-link').on("click", (function() { 313 | this.destroy(); 314 | }).bind(this)); 315 | this.enabled = true; 316 | }; 317 | 318 | GraphManager.GraphEditForm.prototype = { 319 | getHosts: function() { 320 | if (GraphManager.HOSTS) { 321 | return Object.keys(GraphManager.HOSTS); 322 | } 323 | return []; 324 | }, 325 | 326 | getRRDNames: function(request, response) { 327 | var host = this.getData().host; 328 | if (host && host != '' && this.type) { 329 | if (this.portsByHost[host]) { 330 | response(this.portsByHost[host].filter(function(el) { return el.match(new RegExp(request.term, "i")); })); 331 | } else { 332 | $.getJSON('ajax/getRRDNamesForHost.php', {host: host, type: this.type}) 333 | .done((function(data) { 334 | this.portsByHost[host] = data; 335 | response(this.portsByHost[host].filter(function(el) { return el.match(new RegExp(request.term, "i")); })); 336 | }.bind(this))) 337 | .fail(function() { 338 | console.error(arguments); 339 | response([]); 340 | }); 341 | } 342 | } else { 343 | response([]); 344 | } 345 | }, 346 | 347 | getSubtypes: function() { 348 | if (this.type != null) { 349 | return { 350 | bits: ['bits_in', 'bits_out'], 351 | ucastpkts: ['ucastpkts_in', 'ucastpkts_out'], 352 | errors: ['discards_in', 'errors_in', 'discards_out', 'errors_out'], 353 | mcastpkts: ['mcastpkts_in', 'mcastpkts_out'], 354 | bcastpkts: ['bcastpkts_in', 'bcastpkts_out'] 355 | }[this.type]; 356 | } 357 | return []; 358 | }, 359 | 360 | getData: function() { 361 | var data = {}; 362 | data.host = this.form.find('.edit-graph-form-host').val(); 363 | data.rrdname = this.form.find('.edit-graph-form-rrdname').val(); 364 | data.subtype = this.form.find('.edit-graph-form-subtype').val(); 365 | data.color = this.form.find('.edit-graph-form-color').val(); 366 | data.graphing_method = this.form.find('.edit-graph-form-graphing-method').val(); 367 | data.custom_label = this.form.find('.edit-graph-form-custom-label').val(); 368 | data.type = this.type; 369 | return data; 370 | }, 371 | 372 | getDefaultColor: function() { 373 | // these should match the colors in functions.php 374 | var colors = { 375 | 'bits': ['#0A2868', '#FFCA00', '#EC4890', '#517CD7', '#00C169', '#D1F94C'], 376 | 'ucastpkts': ['#2008E6', '#D401E2', '#00DFD6', '#08004E'], 377 | 'errors': ['#30B6C9', '#FFFE39', '#AD34CF', '#09616D'], 378 | 'mcastpkts': ['#53B0B8', '#7E65C7', '#C2F2C6', '#FFB472'], 379 | 'bcastpkts': ['#FFEF9F', '#C17AC3', '#7FA1C3', '#AA9737'] 380 | }[this.type]; 381 | return colors[this.idx % colors.length]; 382 | }, 383 | 384 | createForm: function() { 385 | var form = $('
'); 386 | form.append($('x')); 387 | form.append($('').autocomplete({ source: this.getHosts() }).val(this.data.host || '').attr('placeholder', 'host')); 388 | form.append($('').autocomplete({ source: (function (request, response) { this.getRRDNames(request, response) }).bind(this) }).val(this.data.rrdname || '').attr('placeholder', 'rrdname')); 389 | var subtype_select = $(''); 397 | graphing_method_select.append($('