#!/usr/bin/perl # -*- perl -*- =encoding utf8 =head1 NAME unifi_api - Munin plugin to display device and network information from the Ubiquiti unifi API =head1 APPLICABLE SYSTEMS Unifi controllors with direct API access Controller version 5+ required (tested with 5.8.x) WebRTC is not supported at this time =head1 CONFIGURATION This script uses the multigraph functionality to generate many graphs. As such, there are a significant amount of available configuration options =head2 API Details You will need to supply your API login details: [unifi_api] # User name to login to unifi controller API. Default is "ubnt". Ideally, this should # point to a read-only account. env.user Controller_Username # Password to login to unifi controller API. Default is "ubnt" env.pass Controller_Password # URL of the API, with port if needed. No trailing slash. # Default is https://localhost:8443 env.api_url https://unifi.fqdn.com:8443 # Verify SSL certificate name against host. # Note: if using a default cloudkey certificate, this will fail unless you manually add it # to the local keystore. # Default is "yes" env.ssl_verify_host no # Verify Peer's SSL vertiicate. # Note: if using a default cloudkey certificate, this will fail # Default is "yes" env.ssl_verify_peer no # The human readable name of the unifi site - used for graph titles env.name Site Name # "Site" string - the internal unifi API site identifier. # default is "default" - found when you connect to the web interface # it's the term in the URL - /manage/site/site_string/dashboard env.site site_string =head2 Graph Categories / Host Management Sometimes, you need more control over where the unifi graphs appear. env.force_category 0 # By default, Use standard munin well know categories - # system: cpu, mem, load, & uptime # network: clients, transfer statistics. # To use this feature, set "force_category" to a text string (i.e. "unifi"). This is very helpful if your graphs are going to appear inside another host - for instance if your munin graphs for that host are monitoring the host the controller is running on, and the unifi API instance. Sometimes however, you want to monitor either an offsite API, or a cloudkey which, at least by default, does not run munin-node. In that case, you can actually create a "virtual" munin host to display only these graphs (or any combination you like). This is documented in the main munin docs, but in a nutshell: In your munin-node plugin configuration: (Something like: /etc/munin/plugin-conf.d/munin-node) [unifi_api] host_name hostname.whatever.youlike env.force_category unifi And, in your munin master configuration: (Something like: /etc/munin/munin.conf) [hostname.whatever.youlike] address ip.of.munin.node Make sure you do *not* set "use_node_name" on this new host. It may be necessary to define "host_name" in your munin-node configuration as well, if you have not already (Likely, on a multi-homed host, this has been done to keep munin-node from advertising itself as localhost) More information: * L =head2 Toggling of graphs / Individual options You can turn off individual graphs. A few graphs have extra configuration options. By default, everything is enabled. Set to "no" to disable [unifi_api] # Show device CPU utilization env.enable_device_cpu yes # Show device memory usage env.enable_device_mem yes # Show device load average (switches and APs only) env.enable_device_load yes # Show device uptime env.enable_device_uptime yes # Show number of clients connected to each device env.enable_clients_device yes # Show detailed graphs for each device (per device graphs) env.enable_detail_clients_device yes # Show number of clients connected to each network type env.enable_clients_type yes # Show detailed graphs for each client type (per type graphs) env.enable_detail_clients_type yes # Show unauthorized / authorized client list # if you are not using the guest portal, this is useless env.show_authorized_clients_type yes # Show transfer statistics on switch ports env.enable_xfer_port yes # Show detailed graphs per switch port env.enable_detail_xfer_port yes # Hide ports that have no link (When set to no, unplugged ports will transfer 0, not be undefined) env.hide_empty_xfer_port yes # Show transfer statistics per device env.enable_xfer_device yes # Show detailed graphs for each device env.enable_detail_xfer_device yes # Show transfer statistics per named network env.enable_xfer_network yes # Show detailed graphs for each named network env.enable_detail_xfer_network yes # Show transfer statistics per radio env.enable_xfer_radio yes # Show detailed graphs for each radio env.enable_detail_xfer_radio yes =head1 CAPABILITIES This plugin supports L =head1 DEPENDENCIES This plugin requires munin-multiugraph. =over =item WWW::Curl::Easy Perl extension interface for libcurl =item JSON JSON (JavaScript Object Notation) encoder/decoder =back =head1 PERFORMANCE CONCERNS The main performance concern on this is the huge number of graphs that may be generated. Using the cron version of munin-graph may hurt a lot. A bit of a case study: | My Site | UBNT Demo --------------------------------------- Devices | 8 | 126 AP's | 4 | 118 24xSwitch | 1 | 5 8xSwitch | 2 | 2 Output Bytes | 64,262 | 431,434 Output Lines | 1,761 | 14,586 Output Graphs | 77 | 530 So, just note that the growth in the amount of graphed date can be extreme. =head1 LICENSE Copyright (C) 2018 J.T.Sage (jtsage@gmail.com) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see L. =head1 MAGIC MARKERS #%# family=manual #%# capabilities= =cut use warnings; use strict; use utf8; use Munin::Plugin; # Check dependencies my @errorCode; my $me = (split '/', $0)[-1]; if (! eval {require JSON; JSON->import(); 1; } ) { push @errorCode, "JSON module not found"; } if (! eval {require WWW::Curl::Easy; 1;} ) { push @errorCode, "WWW::Curl::Easy module not found"; } # Fail on not found dependencies if ( @errorCode != 0 ) { die "FATAL:$me: Perl dependencies not installed (", join(", " => @errorCode), ")\n"; } # Multigraph cabability is required for this plugin need_multigraph(); # Somewhat (in)sane defaults for host, pass, etc my %APIconfig = ( 'user' => env_default_text('user' , 'ubnt'), 'pass' => env_default_text('pass' , 'ubnt'), 'api_url' => env_default_text('api_url' , 'https://localhost:8443'), 'site' => env_default_text('site' , 'default'), 'ssl_verify_host' => env_default_text('ssl_verify_host', 'yes'), 'ssl_verify_peer' => env_default_text('ssl_verify_peer', 'yes'), 'name' => env_default_text('name' , 'Unnamed Site'), ); # The big table of plugin options - see POD documentation for what these do. my %PluginConfig = ( 'enable_device_cpu' => env_default_bool_true('enable_device_cpu'), 'enable_device_mem' => env_default_bool_true('enable_device_mem'), 'enable_device_load' => env_default_bool_true('enable_device_load'), 'enable_device_uptime' => env_default_bool_true('enable_device_uptime'), 'enable_clients_device' => env_default_bool_true('enable_clients_device'), 'enable_clients_type' => env_default_bool_true('enable_clients_network'), 'enable_xfer_port' => env_default_bool_true('enable_xfer_port'), 'enable_xfer_device' => env_default_bool_true('enable_xfer_device'), 'enable_xfer_network' => env_default_bool_true('enable_xfer_network'), 'enable_xfer_radio' => env_default_bool_true('enable_xfer_radio'), 'enable_detail_xfer_port' => env_default_bool_true('enable_detail_xfer_port'), 'enable_detail_xfer_device' => env_default_bool_true('enable_detail_xfer_device'), 'enable_detail_xfer_network' => env_default_bool_true('enable_detail_xfer_network'), 'enable_detail_xfer_radio' => env_default_bool_true('enable_detail_xfer_radio'), 'enable_detail_clients_device' => env_default_bool_true('enable_detail_clients_device'), 'enable_detail_clients_type' => env_default_bool_true('enable_detail_clients_network'), 'hide_empty_xfer_port' => env_default_bool_true('hide_empty_xfer_port'), 'show_authorized_clients_type' => env_default_bool_true('show_authorized_clients_type'), 'force_category' => env_default_text('force_category', 0), ); # Set up needed API endpoints my %APIPoint = ( 'login' => $APIconfig{"api_url"} . "/api/login", 'device' => $APIconfig{"api_url"} . "/api/s/" . $APIconfig{"site"} . "/stat/device", 'wlan' => $APIconfig{"api_url"} . "/api/s/" . $APIconfig{"site"} . "/rest/wlanconf", 'sta' => $APIconfig{"api_url"} . "/api/s/" . $APIconfig{"site"} . "/stat/sta", ); my %APIResponse; my %APIJsonResponse; my %Data; my $retcode; # Init curl and JSON my $curl = WWW::Curl::Easy->new() or die "FATAL:$me: WWW::Curl::Easy init failed!\n"; my $jsonOBJ = JSON->new() or die "FATAL:$me: JSON init failed!\n"; ## Fetch the data from the API # The rest is a way to use local files from the local disk. Undocumented and not really supported. if ( !env_default_bool_true('USE_API') ) { if (! eval {require File::Slurp; File::Slurp->import(); 1; } ) { die "Local debug unavailable, File::Slurp CPAN module required\n"; } foreach ( "./demo-test-files/device", "./demo-test-files/sta", "./demo-test-files/wlanconf" ) { if ( ! -f $_ ) { die "File not found: $_\n"; } } $APIJsonResponse{'device'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode(read_file('./demo-test-files/device')); $APIJsonResponse{'sta'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode(read_file('./demo-test-files/sta')); $APIJsonResponse{'wlan'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode(read_file('./demo-test-files/wlanconf')); } else { fetch_data(); } ## Process the data make_data(); if ( defined($ARGV[0]) && $ARGV[0] eq "config" ) { # Do the config step for each set of graphs do_config_mem(); do_config_cpu(); do_config_load(); do_config_uptime(); do_config_xfer_by_device(); do_config_xfer_by_uplink(); do_config_xfer_by_port(); do_config_xfer_by_network(); do_config_xfer_by_radio(); do_config_clients_by_device(); do_config_clients_by_type(); # If dirtyconfig is not supported, or turned off, exit here. Otherwise, continue to fetch section if ( !defined($ENV{'MUNIN_CAP_DIRTYCONFIG'}) || !$ENV{'MUNIN_CAP_DIRTYCONFIG'} ) { exit 0; } } # Do the fetch step for each set of graphs do_values_cpu(); do_values_mem(); do_values_load(); do_values_uptime(); do_values_xfer_by_device(); do_values_xfer_by_uplink(); do_values_xfer_by_port(); do_values_xfer_by_network(); do_values_xfer_by_radio(); do_values_clients_by_device(); do_values_clients_by_type(); ####################### # SUBROUTINES CONFIG # ####################### sub do_config_clients_by_type { # Provide client count by type - CONFIG if ( !$PluginConfig{'enable_clients_type'} ) { return 0; } graph_prologue( 'unifi_clients_per_network', 'Clients Connected / Network', '-l 0 --base 1000', 'clients', 'network', 'Clients connected per type - manually summing these numbers may be meaningful, as clients are often of multiple types' ); foreach ( @{$Data{'typesOrder'}} ) { print $_ , ".label " , $Data{'types'}{$_}[0] , "\n"; } if ( ! $PluginConfig{'enable_detail_clients_type'} ) { return 1; } foreach ( @{$Data{'typesOrder'}} ) { if ( $Data{'types'}{$_}[1] == 1 ) { graph_prologue( 'unifi_clients_per_network.' . $_, 'Clients Connected : ' . $Data{'types'}{$_}[0], '-l 0 --base 1000', 'clients', 'network', 'Clients connected via that are of type: ' . $Data{'types'}{$_}[0] ); print "users.label Users\n"; print "guests.label Guests\n"; } } return 1; } sub do_config_clients_by_device { # Provide client count by device - CONFIG if ( !$PluginConfig{'enable_clients_device'} ) { return 0; } graph_prologue( 'unifi_clients_per_device', 'Clients Connected / Device', '-l 0 --base 1000', 'clients', 'network', 'Clients connected to each unifi device' ); foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".label " , $Data{'device'}{$_}->{'label'} , "\n"; } if ( ! $PluginConfig{'enable_detail_clients_device'} ) { return 1; } foreach ( sort keys %{$Data{'device'}} ) { graph_prologue( 'unifi_clients_per_device.' . $_, 'Clients / Device : ' . $Data{'device'}{$_}->{'label'}, '-l 0 --base 1000', 'clients', 'network', 'Clients connected to the ' . $Data{'device'}{$_}->{'label'} . " unifi device" ); print "users.label Users\n"; print "guests.label Guests\n"; } return 1; } sub do_config_xfer_by_radio { # Provide transfer for radios - CONFIG if ( !$PluginConfig{'enable_xfer_radio'} ) { return 0; } graph_prologue( 'unifi_xfer_per_radio', 'Transfer / radio', '--base 1000', 'Packets/${graph_period}', 'network', 'Number of packets transferred per individual radio band' ); foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "uap" ) { next; } foreach ( @{$Data{'device'}{$thisDevice}{'radio'}} ) { print $thisDevice , "_" , $_->{"name"} , "_pack.label " , $_->{"label"} , "\n"; print $thisDevice , "_" , $_->{"name"} , "_pack.type DERIVE\n"; print $thisDevice , "_" , $_->{"name"} , "_pack.min 0\n"; } } if ( ! $PluginConfig{'enable_detail_xfer_radio'} ) { return 1; } foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "uap" ) { next; } graph_prologue( 'unifi_xfer_per_radio.' . $thisDevice, 'Transfer / radio : ' . $Data{'device'}{$thisDevice}->{'name'}, '--base 1000', 'Packets/${graph_period}', 'network', 'Transfered Packets, Dropped / Retried Packets, and Error Packets for the WLAN device: ' . $Data{'device'}{$thisDevice}->{'name'} ); foreach ( @{$Data{'device'}{$thisDevice}{'radio'}} ) { print $_->{"name"} , "_pkt.label " , $_->{"type"} , " Packets\n"; print $_->{"name"} , "_pkt.type DERIVE\n"; print $_->{"name"} , "_pkt.min 0\n"; print $_->{"name"} , "_dret.label " , $_->{"type"} , " Dropped / Retries\n"; print $_->{"name"} , "_dret.type DERIVE\n"; print $_->{"name"} , "_dret.min 0\n"; print $_->{"name"} , "_err.label " , $_->{"type"} , " Errors\n"; print $_->{"name"} , "_err.type DERIVE\n"; print $_->{"name"} , "_err.min 0\n"; } } return 1; } sub do_config_xfer_by_network { # Provide transfer for named networks - CONFIG if ( !$PluginConfig{'enable_xfer_network'} ) { return 0; } graph_prologue( 'unifi_xfer_per_network', 'Transfer / named network', '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received per each named network' ); foreach my $thisNet ( sort keys %{$Data{'networks'}} ) { foreach ( "_rxbytes", "_txbytes" ) { print $thisNet , $_ , ".label " , $Data{'networks'}{$thisNet}->{"label"} . "\n"; print $thisNet , $_ , ".type DERIVE\n"; print $thisNet , $_ , ".min 0\n"; } print $thisNet , "_rxbytes.graph no\n"; print $thisNet , "_txbytes.negative " , $thisNet , "_rxbytes\n"; } if ( ! $PluginConfig{'enable_detail_xfer_network'} ) { return 1; } foreach my $thisNet ( sort keys %{$Data{'networks'}} ) { graph_prologue( 'unifi_xfer_per_network.' . $thisNet, 'Transfer / named network : ' . $Data{'networks'}{$thisNet}->{'label'}, '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received for the network named: ' . $Data{'networks'}{$thisNet}->{'label'} ); foreach ( "rxbyte", "txbyte" ) { print $_ , ".label Bytes\n"; print $_ , ".type DERIVE\n"; print $_ , ".min 0\n"; } print "rxbyte.graph no\n"; print "txbyte.negative rxbyte\n"; } return 1; } sub do_config_xfer_by_port { # Provide transfer for switch ports - CONFIG if ( !$PluginConfig{'enable_xfer_port'} ) { return 0; } foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "usw" ) { next; } graph_prologue( 'unifi_xfer_per_port_' . $thisDevice, 'Transfer / port : ' . $Data{'device'}{$thisDevice}->{'label'}, '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received per port on the switch named: ' . $Data{'device'}{$thisDevice}->{'label'} ); foreach my $thisPort ( @{$Data{'device'}{$thisDevice}{'ports'}} ) { foreach ( "_rxbytes", "_txbytes" ) { print $thisDevice , "_" , $thisPort->{"name"} , $_ , ".label " , $thisPort->{"label"} . "\n"; print $thisDevice , "_" , $thisPort->{"name"} , $_ , ".type DERIVE\n"; print $thisDevice , "_" , $thisPort->{"name"} , $_ , ".min 0\n"; } print $thisDevice , "_" , $thisPort->{"name"} , "_rxbytes.graph no\n"; print $thisDevice , "_" , $thisPort->{"name"} , "_txbytes.negative " , $thisDevice , "_" , $thisPort->{"name"} , "_rxbytes\n"; } } if ( ! $PluginConfig{'enable_detail_xfer_port'} ) { return 1; } # Extended graphs foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "usw" ) { next; } foreach my $thisPort ( @{$Data{'device'}{$thisDevice}{'ports'}} ) { graph_prologue( 'unifi_xfer_per_port_' . $thisDevice . "." . $thisPort->{'name'}, 'Transfer / port : ' . $Data{'device'}{$thisDevice}->{'label'} . " : " . $thisPort->{'label'}, '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received on port "' . $thisPort->{'label'} . '" of the switch "' . $Data{'device'}{$thisDevice}->{'label'} . '"' ); foreach ( "rxbyte", "txbyte" ) { print $_ . ".label Bytes\n"; print $_ . ".type DERIVE\n"; print $_ . ".min 0\n"; } print "rxbyte.graph no\n"; print "txbyte.negative rxbyte\n"; } } return 1; } sub do_config_xfer_by_uplink { # Provide transfer for unifi uplink - CONFIG if ( !$PluginConfig{'enable_xfer_device'} ) { return 0; } graph_prologue( 'unifi_xfer_by_uplink', 'Transfer on uplink : ' . $Data{'uplink'}{'devName'}, '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received on the WAN port of the USG, and the speedtest result of the same port' ); foreach ( "rx", "tx" ) { print $_ , "_speed.label Speedtest\n"; print $_ , "_bytes.label Transferred\n"; print $_ , "_speed.type GAUGE\n"; print $_ , "_bytes.type DERIVE\n"; print $_ , "_speed.min 0\n"; print $_ , "_bytes.min 0\n"; } print "rx_speed.graph no\n"; print "rx_bytes.graph no\n"; print "tx_speed.negative rx_speed\n"; print "tx_bytes.negative rx_bytes\n"; return 1; } sub do_config_xfer_by_device { # Provide transfer for each unifi device - CONFIG if ( !$PluginConfig{'enable_xfer_device'} ) { return 0; } graph_prologue( 'unifi_xfer_per_device', 'Transfer / device', '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received per unifi device' ); foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { foreach ( "_rxbytes", "_txbytes" ) { print $thisDevice , $_ , ".label " , $Data{'device'}{$thisDevice}->{'label'} , "\n"; print $thisDevice , $_ , ".type DERIVE\n"; print $thisDevice , $_ , ".min 0\n"; } print $thisDevice , "_rxbytes.graph no\n"; print $thisDevice , "_txbytes.negative " , $thisDevice , "_rxbytes\n"; } if ( $PluginConfig{'enable_detail_xfer_device'} ) { foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { graph_prologue( 'unifi_xfer_per_device.' . $thisDevice, 'Transfer / device : ' . $Data{'device'}{$thisDevice}->{'label'}, '--base 1000', 'Bytes/${graph_period} rcvd (-) / trans (+)', 'network', 'Bytes sent and received on the unifi device named: ' . $Data{'device'}{$thisDevice}->{'label'} ); foreach ( "rxbyte", "txbyte" ) { print $_ , ".label Bytes\n"; print $_ , ".type DERIVE\n"; print $_ , ".min 0\n"; } print "rxbyte.graph no\n"; print "txbyte.negative rxbyte\n"; } } return 1; } sub do_config_uptime { # Provide device uptime for each unifi device - CONFIG if ( !$PluginConfig{'enable_device_uptime'} ) { return 0; } graph_prologue( 'unifi_device_uptime', 'Uptime', '--base 1000 -r --lower-limit 0', 'days', 'system', 'Uptime in days for each unifi device' ); foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".label " , $Data{'device'}{$_}->{"name"} , "\n"; } return 1; } sub do_config_cpu { # Provide device CPU usage for each unifi device - CONFIG if ( !$PluginConfig{'enable_device_cpu'} ) { return 0; } graph_prologue( 'unifi_device_cpu', 'CPU Usage', '--base 1000 -r --lower-limit 0 --upper-limit 100', '%', 'system', 'CPU usage as a percentage for each unifi device' ); foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".label " , $Data{'device'}{$_}->{"name"} , "\n"; } return 1; } sub do_config_load { # Provide device load average for each unifi device - CONFIG if ( !$PluginConfig{'enable_device_load'} ) { return 0; } graph_prologue( 'unifi_device_load', 'Load Average', '-l 0 --base 1000', 'load', 'system', 'Load average for each unifi Access Point or Switch' ); foreach ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$_}->{'type'} eq 'ugw' ) { next; } print $_ , ".label " , $Data{'device'}{$_}->{"name"} , "\n"; } return 1; } sub do_config_mem { # Provide device memory usage for each unifi device - CONFIG if ( !$PluginConfig{'enable_device_mem'} ) { return 0; } graph_prologue( 'unifi_device_mem', 'Memory Usage', '--base 1000 -r --lower-limit 0 --upper-limit 100', '%', 'system', 'Memory usage as a percentage for each unifi device' ); foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".label " , $Data{'device'}{$_}->{"name"} , "\n"; } return 1; } ######################### # SUBROUTINES VALUES # ######################### sub do_values_clients_by_type { # Provide client count by type - VALUES if ( !$PluginConfig{'enable_clients_type'} ) { return 0; } print "multigraph unifi_clients_per_network\n"; foreach ( @{$Data{'typesOrder'}} ) { print $_ , ".value " , ( $Data{'types'}{$_}[2] + $Data{'types'}{$_}[3] ) , "\n"; } if ( ! $PluginConfig{'enable_detail_clients_type'} ) { return 1; } foreach ( @{$Data{'typesOrder'}} ) { if ( $Data{'types'}{$_}[1] == 1 ) { print "multigraph unifi_clients_per_network.$_\n"; print "users.value " , $Data{'types'}{$_}[2] , "\n"; print "guests.value " , $Data{'types'}{$_}[3] , "\n"; } } return 1; } sub do_values_clients_by_device { # Provide client count by device - VALUES if ( !$PluginConfig{'enable_clients_device'} ) { return 0; } print "multigraph unifi_clients_per_device\n"; foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".value " , $Data{'device'}{$_}->{'clients'} , "\n"; } if ( ! $PluginConfig{'enable_detail_clients_device'} ) { return 1; } foreach ( sort keys %{$Data{'device'}} ) { print "multigraph unifi_clients_per_device.$_\n"; print "users.value " , $Data{'device'}{$_}->{'users'} , "\n"; print "guests.value " , $Data{'device'}{$_}->{'guests'} , "\n"; } return 1; } sub do_values_xfer_by_radio { # Provide transfer for radios - VALUES if ( !$PluginConfig{'enable_xfer_radio'} ) { return 0; } print "multigraph unifi_xfer_per_radio\n"; foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "uap" ) { next; } foreach ( @{$Data{'device'}{$thisDevice}{'radio'}} ) { print $thisDevice , "_" , $_->{"name"} , "_pack.value " , ($_->{"pckt"} // 0), "\n";; } } if ( ! $PluginConfig{'enable_detail_xfer_radio'} ) { return 1; } foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "uap" ) { next; } print "multigraph unifi_xfer_per_radio.$thisDevice\n"; foreach ( @{$Data{'device'}{$thisDevice}{'radio'}} ) { print $_->{"name"} , "_pkt.value " , ($_->{"pckt"} // 0) , "\n"; print $_->{"name"} , "_dret.value " , ($_->{"dret"} // 0) , "\n"; print $_->{"name"} , "_err.value " , ($_->{"err"} // 0) , "\n"; } } return 1; } sub do_values_xfer_by_network { # Provide transfer for named networks - CONFIG if ( !$PluginConfig{'enable_xfer_network'} ) { return 0; } print "multigraph unifi_xfer_per_network\n"; foreach my $thisNet ( sort keys %{$Data{'networks'}} ) { print $thisNet , "_rxbytes.value " , ($Data{'networks'}{$thisNet}->{"rx"} // 0) , "\n"; print $thisNet , "_txbytes.value " , ($Data{'networks'}{$thisNet}->{"tx"} // 0) , "\n"; } if ( ! $PluginConfig{'enable_detail_xfer_network'} ) { return 1; } foreach my $thisNet ( sort keys %{$Data{'networks'}} ) { print "multigraph unifi_xfer_per_network.$thisNet\n"; print "rxbyte.value " , ($Data{'networks'}{$thisNet}->{"rx"} // 0) , "\n"; print "txbyte.value " , ($Data{'networks'}{$thisNet}->{"tx"} // 0) , "\n"; } return 1; } sub do_values_xfer_by_port { # Provide transfer for switch ports - VALUES if ( !$PluginConfig{'enable_xfer_port'} ) { return 0; } foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "usw" ) { next; } print "multigraph unifi_xfer_per_port_$thisDevice\n"; foreach ( @{$Data{'device'}{$thisDevice}{'ports'}} ) { print $thisDevice , "_" , $_->{"name"} , "_rxbytes.value " , $_->{"rx"} , "\n"; print $thisDevice , "_" , $_->{"name"} , "_txbytes.value " , $_->{"tx"} , "\n"; } } if ( ! $PluginConfig{'enable_detail_xfer_port'} ) { return 1; } # Extended graphs foreach my $thisDevice ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$thisDevice}->{'type'} ne "usw" ) { next; } foreach ( @{$Data{'device'}{$thisDevice}{'ports'}} ) { print 'multigraph unifi_xfer_per_port_' . $thisDevice . "." . $_->{'name'} . "\n"; print "rxbyte.value " , $_->{"rx"} , "\n"; print "txbyte.value " , $_->{"tx"} , "\n"; } } return 1; } sub do_values_xfer_by_uplink { # Provide transfer for unifi uplink - CONFIG if ( !$PluginConfig{'enable_xfer_device'} ) { return 0; } $Data{'uplink'}{"rx_speed"} = "U" unless $Data{'uplink'}{"rx_speed"}; $Data{'uplink'}{"tx_speed"} = "U" unless $Data{'uplink'}{"tx_speed"}; $Data{'uplink'}{"rx_bytes"} = "U" unless $Data{'uplink'}{"rx_bytes"}; $Data{'uplink'}{"tx_bytes"} = "U" unless $Data{'uplink'}{"tx_bytes"}; print "multigraph unifi_xfer_by_uplink\n"; print "rx_speed.value " . $Data{'uplink'}{"rx_speed"} . "\n"; print "tx_speed.value " . $Data{'uplink'}{"tx_speed"} . "\n"; print "rx_bytes.value " . $Data{'uplink'}{"rx_bytes"} . "\n"; print "tx_bytes.value " . $Data{'uplink'}{"tx_bytes"} . "\n"; return 1; } sub do_values_xfer_by_device { # Provide transfer for each unifi device - CONFIG if ( !$PluginConfig{'enable_xfer_device'} ) { return 0; } print "multigraph unifi_xfer_per_device\n"; foreach ( sort keys %{$Data{'device'}} ) { print $_ . "_rxbytes.value " . $Data{'device'}{$_}->{"rx"} , "\n"; print $_ . "_txbytes.value " . $Data{'device'}{$_}->{"tx"} , "\n"; } if ( $PluginConfig{'enable_detail_xfer_device'} ) { foreach ( sort keys %{$Data{'device'}} ) { print "multigraph unifi_xfer_per_device." , $_ , "\n"; print "rxbyte.value " , $Data{'device'}{$_}->{"rx"} , "\n"; print "txbyte.value " , $Data{'device'}{$_}->{"tx"} , "\n"; } } return 1; } sub do_values_cpu { # Provide device CPU usage for each unifi device - VALUES if ( !$PluginConfig{'enable_device_cpu'} ) { return 0; } print "multigraph unifi_device_cpu\n"; foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".value " , ( $Data{'device'}{$_}->{"cpu"} ) , "\n"; } return 1; } sub do_values_mem { # Provide device memory usage for each unifi device - VALUES if ( !$PluginConfig{'enable_device_mem'} ) { return 0; } print "multigraph unifi_device_mem\n"; foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".value " , ( $Data{'device'}{$_}->{"mem"} ) , "\n"; } return 1; } sub do_values_load { # Provide device load average for each unifi device - VALUES if ( !$PluginConfig{'enable_device_load'} ) { return 0; } print "multigraph unifi_device_load\n"; foreach ( sort keys %{$Data{'device'}} ) { if ( $Data{'device'}{$_}->{'type'} eq 'ugw' ) { next; } print $_ , ".value " , ( $Data{'device'}{$_}->{"load"} ) , "\n"; } return 1; } sub do_values_uptime { # Provide device uptime for each unifi device - VALUES if ( !$PluginConfig{'enable_device_uptime'} ) { return 0; } print "multigraph unifi_device_uptime\n"; foreach ( sort keys %{$Data{'device'}} ) { print $_ , ".value " , ( $Data{'device'}{$_}->{"uptime"} / 86400 ) , "\n"; } return 1; } ######################### # SUBROUTINES GENERAL # ######################### sub graph_prologue { # Generate graph prologues - slightly less copy-pasta, and less chance for things to go wrong my ( $id, $title, $args, $vlabel, $category, $info ) = (@_); print "multigraph $id\n"; print 'graph_title ' , $title , ' : ' , $APIconfig{"name"} , "\n"; print "graph_args $args\n"; print "graph_vlabel $vlabel\n"; if ( $PluginConfig{'force_category'} ) { print "graph_category ", $PluginConfig{'force_category'}, "\n"; } else { print "graph_category $category\n"; } if ( $info ) { print 'graph_info For the unifi site named "' , $APIconfig{"name"} , "\", $info\n"; } return 1; } # Collate all collected data into something we can use. sub make_data { foreach my $thisDevice ( @{$APIJsonResponse{'device'}->{'data'}} ) { # Grab everything we care to know about each device. $Data{'device'}{ make_safe($thisDevice->{'name'}, $thisDevice->{'serial'}) } = { 'label' => $thisDevice->{'name'}, 'users' => ($thisDevice->{'user-num_sta'} || 0), 'guests' => ($thisDevice->{'guest-num_sta'} || 0), 'clients' => ($thisDevice->{'user-num_sta'} + $thisDevice->{'guest-num_sta'} || 0), 'tx' => $thisDevice->{'rx_bytes'}, 'rx' => $thisDevice->{'tx_bytes'}, 'name' => $thisDevice->{'name'}, 'uptime' => $thisDevice->{'uptime'}, 'cpu' => $thisDevice->{'system-stats'}->{'cpu'}, 'mem' => $thisDevice->{'system-stats'}->{'mem'}, 'load' => ( $thisDevice->{'type'} eq 'ugw' ? 'U' : $thisDevice->{'sys_stats'}->{'loadavg_1'} ), 'type' => $thisDevice->{'type'} }; if ( $thisDevice->{'type'} eq 'ugw' ) { # Handle firewall specially, record uplink and networks foreach my $thisNet ( @{$thisDevice->{'network_table'}} ) { $Data{'networks'}{ make_safe($thisNet->{'name'}, $thisNet->{'_id'} ) } = { 'label' => $thisNet->{'name'}, 'tx' => $thisNet->{'tx_bytes'}, 'rx' => $thisNet->{'rx_bytes'} } } $Data{'uplink'}{'devName'} = $thisDevice->{'name'}; $Data{'uplink'}{'rx_speed'} = $thisDevice->{'speedtest-status'}->{'xput_download'} * 1000000; $Data{'uplink'}{'tx_speed'} = $thisDevice->{'speedtest-status'}->{'xput_upload'} * 1000000; foreach ( @{$thisDevice->{"port_table"}} ) { if ( $_->{name} eq "wan" ) { $Data{'uplink'}{'rx_bytes'} = $_->{'rx_bytes'}; $Data{'uplink'}{'tx_bytes'} = $_->{'tx_bytes'}; } } } if ( $thisDevice->{'type'} eq 'usw' ) { # Handle swiches specially - record port stats my @port_list; foreach my $port ( @{$thisDevice->{'port_table'}} ) { if ( !$PluginConfig{'hide_empty_xfer_port'} || $port->{'up'} ) { push @port_list , { 'name' => 'port_' . zPad($port->{'port_idx'}), 'label' => zPad($port->{'port_idx'}) . '-' . $port->{'name'}, 'rx' => $port->{'rx_bytes'}, 'tx' => $port->{'tx_bytes'} }; } } $Data{'device'}{ make_safe($thisDevice->{'name'}, $thisDevice->{'serial'}) }{'ports'} = \@port_list; } if ( $thisDevice->{'type'} eq 'uap' ) { # Handle APS specially - record radio stats my @theseRadios; foreach my $thisRadio ( @{$thisDevice->{'radio_table_stats'}} ) { my $name = make_safe( $thisRadio->{'name'}, "" ); my $label = ( $thisRadio->{'channel'} < 12 ) ? '2.4Ghz' : '5Ghz'; $_ = $thisDevice->{'stat'}->{'ap'}; push @theseRadios, { 'name' => $name, 'label' => $label . '-' . $thisDevice->{'name'}, 'pckt' => ($_->{$name . '-rx_packets'} // 0) + ($_->{$name . '-tx_packets'} // 0), 'dret' => ($_->{$name . '-rx_dropped'} // 0) + ($_->{$name . '-tx_retries'} // 0) + ($_->{$name . '-tx_dropped'} // 0), 'err' => ($_->{$name . '-rx_errors'} // 0) + ($_->{$name . '-tx_errors'} // 0), 'type' => $label }; } $Data{'device'}{ make_safe($thisDevice->{'name'}, $thisDevice->{'serial'}) }{'radio'} = \@theseRadios; } } # END PROCESSING OF DEVICE DATA # PROCESS NETWORK TYPE DATA # -> UNLESS, type graph is disabled. # # WHY: if the client list is large (huge. 10,000+), this is CPU intensive if ( !$PluginConfig{'enable_clients_type'} ) { return 1; } $Data{'types'} = { "wired" => ["Wired Connection", 1, 0, 0], "wifi" => ["Wireless Connection", 1, 0, 0], "tuser" => ["Total Users", 0, 0, 0], "tguest" => ["Total Guests", 0, 0, 0], "authed" => ["Authorized Guests", 0, 0, 0], "unauth" => ["Unauthorized Guests", 0, 0, 0], }; $Data{'typesOrder'} = ( $PluginConfig{'show_authorized_clients_type'} ) ? [ "wired", "wifi", "tuser", "tguest", "authed", "unauth"] : [ "wired", "wifi", "tuser", "tguest" ]; my @wlans; foreach my $thisNet ( @{$APIJsonResponse{'wlan'}->{'data'}} ) { $Data{'types'}{ make_safe($thisNet->{'name'}, "") } = [ $thisNet->{'name'}, 1, 0, 0 ]; push @wlans, make_safe($thisNet->{'name'}, ""); } foreach ( sort @wlans ) { push @{$Data{'typesOrder'}}, $_; } foreach my $client ( @{$APIJsonResponse{'sta'}->{'data'}} ) { if ( $client->{"is_wired"} ) { if ( $client->{"is_guest"} ) { $Data{'types'}->{'wired'}[3]++; $Data{'types'}->{'guest'}[3]++; } else { $Data{'types'}->{'wired'}[2]++; $Data{'types'}->{'user'}[2]++; } } else { if ( $client->{"is_guest"} ) { $Data{'types'}->{make_safe($client->{"essid"}, "")}[3]++; $Data{'types'}->{'wifi'}[3]++; $Data{'types'}->{'guest'}[3]++; if ( $client->{"authorized"} ) { $Data{'types'}->{'authed'}[3]++; } else { $Data{'types'}->{'unauth'}[3]++; } } else { $Data{'types'}->{make_safe($client->{"essid"}, "")}[2]++; $Data{'types'}->{'wifi'}[2]++; $Data{'types'}->{'user'}[2]++; } } } return 1; } sub fetch_data { # Set up curl, and login to API $curl->setopt($curl->CURLOPT_POST,1); $curl->setopt($curl->CURLOPT_COOKIEFILE,""); # Session only cookie $curl->setopt($curl->CURLOPT_SSL_VERIFYPEER, (( $APIconfig{"ssl_verify_peer"} =~ m/no/i ) ? 0 : 1) ); $curl->setopt($curl->CURLOPT_SSL_VERIFYHOST, (( $APIconfig{"ssl_verify_host"} =~ m/no/i ) ? 0 : 2) ); $curl->setopt($curl->CURL_SSLVERSION_TLSv1, 1); $curl->setopt($curl->CURLOPT_URL, $APIPoint{'login'}); $curl->setopt($curl->CURLOPT_POSTFIELDS, q[{"username":"] . $APIconfig{"user"} . q[", "password":"] . $APIconfig{"pass"} . q["}] ); $curl->setopt($curl->CURLOPT_WRITEDATA, \$APIResponse{'login'}); $retcode = $curl->perform; if ( $retcode != 0 ) { die "FATAL:$me: Unable to connect to API: " . $curl->strerror($retcode) . " " . $curl->errbuf . "\n"; } $APIJsonResponse{'login'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode($APIResponse{'login'}); if ( $APIJsonResponse{'login'}->{'meta'}->{'rc'} ne 'ok' ) { die "FATAL:$me: Unable to login to API - it said: " , $APIJsonResponse{'login'}->{'meta'}->{'msg'} , "\n"; } # Change method to GET $curl->setopt($curl->CURLOPT_HTTPGET,1); # Get some API data. # Device data $curl->setopt($curl->CURLOPT_WRITEDATA, \$APIResponse{'device'}); $curl->setopt($curl->CURLOPT_URL, $APIPoint{'device'}); $retcode = $curl->perform; if ( $retcode != 0 ) { die "FATAL:$me: Unable to connect to API: " . $curl->strerror($retcode) . " " . $curl->errbuf . "\n"; } $APIJsonResponse{'device'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode($APIResponse{'device'}); if ( $APIJsonResponse{'device'}->{'meta'}->{'rc'} ne 'ok' ) { die "FATAL:$me: Unable get device data from API - it said: " , $APIJsonResponse{'device'}->{'meta'}->{'msg'} , "\n"; } # STA (client) data $curl->setopt($curl->CURLOPT_WRITEDATA, \$APIResponse{'sta'}); $curl->setopt($curl->CURLOPT_URL, $APIPoint{'sta'}); $retcode = $curl->perform; if ( $retcode != 0 ) { die "FATAL:$me: Unable to connect to API: " . $curl->strerror($retcode) . " " . $curl->errbuf . "\n"; } $APIJsonResponse{'sta'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode($APIResponse{'sta'}); if ( $APIJsonResponse{'sta'}->{'meta'}->{'rc'} ne 'ok' ) { die "FATAL:$me: Unable get sta data from API - it said: " , $APIJsonResponse{'sta'}->{'meta'}->{'msg'} , "\n"; } # WLAN data $curl->setopt($curl->CURLOPT_WRITEDATA, \$APIResponse{'wlan'}); $curl->setopt($curl->CURLOPT_URL, $APIPoint{'wlan'}); $retcode = $curl->perform; if ( $retcode != 0 ) { die "FATAL:$me: Unable to connect to API: " . $curl->strerror($retcode) . " " . $curl->errbuf . "\n"; } $APIJsonResponse{'wlan'} = $jsonOBJ->allow_nonref->utf8->relaxed->decode($APIResponse{'wlan'}); if ( $APIJsonResponse{'wlan'}->{'meta'}->{'rc'} ne 'ok' ) { die "FATAL:$me: Unable get wlan data from API - it said: " , $APIJsonResponse{'wlan'}->{'meta'}->{'msg'} , "\n"; } } # Make field names safe, and lowercase. # # Typically, $extraName should be the MAC address of the unique ID identifier as the unifi # controller software does not enforce that device names or network names are unique. sub make_safe { my ( $name, $extraName ) = ( @_ ); if ( $extraName ne "" ) { return clean_fieldname(lc($name) . "_" . $extraName); } else { return lc(clean_fieldname($name)); } } # Get a default from an environmental variable - return text # # env_default(, ) sub env_default_text { my ( $env_var, $default ) = (@_); return ( ( defined $ENV{$env_var} ) ? $ENV{$env_var} : $default ), } # Get a default from an environmental variable - boolean true # # env_default_bool_true (, ) sub env_default_bool_true { my $env_var = $_[0]; return ( ( defined $ENV{$env_var} && $ENV{$env_var} =~ m/no/i ) ? 0 : 1 ); } # Quick 2 digit zero pad sub zPad { return sprintf("%02d", $_[0]); }