#!/usr/bin/env perl
#
# Check an Ookla Speedtest server with a specified URL and/or host is present
# on the list of servers.
#
# Author: Tom Ryder <tom@sanctum.geek.nz>
# Copyright: 2018 Tom Ryder
#
package Monitoring::Plugin::Speedtest::Servers;
# Force me to write this properly
use strict;
use warnings;
use utf8;
# Require at least this Perl version
# Should work even on very old Perls
use 5.006;
# Import required modules
use English '-no_match_vars';
use LWP::UserAgent ();
use Monitoring::Plugin qw(%ERRORS);
use XML::LibXML;
# Decree package version
our $VERSION = '0.04';
# Add description and license package variables
our $DESCRIPTION = <<'EOF';
This plugin retrieves the list of speedtest servers from speedtest.net and
checks for the presence of at least one server with the given host and/or URL.
EOF
our $LICENSE = <<'EOF';
This plugin is distributed under an MIT license. See LICENSE, or visit
<https://opensource.org/licenses/MIT>. Thanks to Inspire Net Ltd for allowing
this open-source fork.
EOF
# Define custom options
our @OPTS = (
{
spec => 'host|h=s',
label => 'HOSTNAME:PORT',
help => 'Hostname:port pair to find in list, usually *:8080',
},
{
spec => 'url|u=s',
label => 'URL',
help => 'URL to find in list, usually ends in /upload.php',
},
);
# URL from which the server list should be retrieved
our $SERVERS_LIST_URL = 'https://www.speedtest.net/speedtest-servers.php';
# Build Monitoring::Plugin object
my $mp = Monitoring::Plugin->new(
usage => 'Usage: %s --host|-H HOSTNAME:PORT --url|-u URL',
version => $VERSION,
blurb => $DESCRIPTION,
license => $LICENSE,
) or die "Failed plugin construct\n";
# Anything that dies in here will raise ->plugin_die()
eval {
# Define and parse custom options
for my $opt (@OPTS) {
$mp->add_arg( %{$opt} );
}
$mp->getopts();
# At least one of --host and --url must be specified
length $mp->opts->host
or length $mp->opts->url
or die "One or both --host or --url must be specified\n";
# Build a user agent that accepts only XML
my $ua = LWP::UserAgent->new();
# Attempt to retrieve the server list
my $headers = HTTP::Headers->new(
'Accept' => 'application/xml;text/xml',
'Accept-Encoding' => scalar HTTP::Message::decodable(),
);
my $request = HTTP::Request->new( 'GET', $SERVERS_LIST_URL, $headers );
my $response = $ua->request($request);
$response->is_success
or die "$response->status_line\n";
# Parse the server list as an XML document
my $lxml = XML::LibXML->new();
my $doc = $lxml->load_xml( string => $response->decoded_content )
or die "Failed to parse response XML\n";
# Build an XPath query object
my $xpc = XML::LibXML::XPathContext->new($doc)
or die "Failed to build XPath query object on response XML\n";
# Build a query depending on which options we were provided
## no critic (RequireInterpolationOfMetachars)
my $query = '/settings/servers/server';
if ( $mp->opts->url ) {
$query .= sprintf q{[@url='%s']}, $mp->opts->url;
}
if ( $mp->opts->host ) {
$query .= sprintf q{[@host='%s']}, $mp->opts->host;
}
# Check that we have at least one matching server
my @servers = $xpc->findnodes($query);
my $code = $mp->check_threshold(
check => scalar @servers,
critical => '1:',
);
my $message = sprintf '%u matching servers', scalar @servers;
$mp->add_message( $code, $message );
# Add OK-level messages showing the conditions we applied
# If we find an actual problem, "OK" will get replaced by ->check_messages()
if ( $mp->opts->host ) {
$mp->add_message( $ERRORS{OK}, sprintf 'host=%s', $mp->opts->host );
}
if ( $mp->opts->url ) {
$mp->add_message( $ERRORS{OK}, sprintf 'url=%s', $mp->opts->url );
}
# Exit with determined code and messages
$mp->plugin_exit(
$mp->check_messages(
join => q{, },
join_all => q{, },
),
);
} or $mp->plugin_die($EVAL_ERROR);