ICYMI: Customizing Polling Intervals With the Orion SDK

(“In Case You Missed It Monday” is my chance to showcase something that I wrote and published in another venue, but is still relevant. This week’s post originally appeared on THWACK.com)

This script was featured on the TWHACKcamp 2018 session “There’s an API for That,” which you can watch in its entirety here. The two scripts I used are attached to this post.

My goal here is to explain the idea behind the script as well as point out any major design points within the code. But there are a few things I need to clear up before I begin:

First, you will probably notice that this script is written in Perl, a language with a long, illustrious, and glorious history as a text processing tool, and yet not a language which lends itself to process-oriented tasks. I selected the language because:

  1. I am old, and writing in Perl brings back memories of my youthful glory days.
  2. I am stubborn, and they said Perl wasn’t the right tool for this job so I had to prove them wrong.
  3. It underscores the flexibility of the Orion API—if your programming language supports REST, then you can use it.

Second, you’ll notice that I’ve had to load a few different packages at the start of the script to make this all work. I’ll describe them in detail below, but should you decide to follow in my footsteps, you will need to get comfortable loading CPAN modules before any of this will run.

Third, I need to take a moment and express my extreme gratitude and deepest thanks to @tdanner@kmsigma@Zackm, and @ironman84 for their help, encouragement, support, and patience.

However, the biggest thank-you goes to Steven Klassen (a.k.a. @mrxinu), who took significant time out of his day(s) to help clean up the spaghetti that was my original attempt at this script. The best thing you could say about my original was “it ran.” After his assistance, you can say “it ran with a bit of grace and style.”

Background: What Problem Are We Solving?

When you load up a bunch of nodes in Orion® (whether you’re using Network Performance MonitorServer & Application Monitor, or some other module) you “get” a certain level of monitoring for “free.” (God as my witness, the legal team insisted I use those air quotes because they’re afraid someone will really think it’s free.) That monitoring includes availability (up/down of the node, interfaces, disks, etc.) and statistic collection (CPU, RAM, disk space, etc.). Generally speaking, the availability data is collected by default every 120 seconds, and the statistic data is collected every 5 to 10 minutes. Which is fine, IN GENERAL.

BUT… there are some devices in your environment that need to be monitored, but not that aggressively. Like the under-the-desk server in the training room. Or the media server that plays HR training videos for new hires. Or the internet-connected coffee pot. At the other end of the spectrum are business-critical systems that need much more aggressive monitoring.

The good news is that Orion allows you to set a global standard for polling frequency, and also adjust the that frequency on a per-machine basis. The bad news is that this is, by necessity, a manual process. You have to pick the machine(s) you want to adjust and adjust them. And generally speaking, this is the right choice.

But what if you wanted to base polling frequency on a variable? What if you had a custom property that indicated the criticality of the system, and you wanted Orion to just naturally adjust polling based on that field? In that way, you could monitor a system that was in the “build” stage very lightly just to get baselines, but turn it up to 11 when you moved to “burn-in” stage, and then ratchet the polling frequency down to normal levels when the criticality was “prod-sev-3.”

That’s what this script does. It reads the value of a database field for each node, and adjusts the polling values accordingly.

The Values

For the sake of example, here’s what I have set up:

  • I have a custom property called “Polling_Level”
  • The “Polling_Level” values and their availability (in seconds) / statistics (in minutes) frequency are:
    • Decommissioned (600/60)
    • Training (300/30)
    • Build (300/15)
    • Dev (120/15)
    • Prod-Sev-5 (300/10)
    • Prod-Sev-4 (240/5)
    • Prod-Sev-3 (120/5)
    • Prod-Sev-2 (60/2)
    • Prod-Sev-1 (30/2)
    • Scrutiny (10/1)

The Nitty Gritty (that one line of code)

The main line of code—the one that does all the heavy lifting—is here:

$response = $client->POST('/SolarWinds/InformationService/v3/Json/'.$clienturi, $newpolljson, {"Content-Type"=>"application/json"} );  

But to understand it, you need a little bit of background.

Syntax of “That One Line of Code”

There are actually two syntaxes you need to understand: the syntax of the Orion API and the syntax of Perl as it relates to the API.

Regarding the API, you need to understand that you’re doing an update operation (https://github.com/solarwinds/OrionSDK/wiki/REST#update-request), which requires 3 things:

  1. The URI of the object you want to update, including the SolarWinds Information Service (SWIS) prefix
  2. The value(s) you want to set the object to
  3. The content-type

In this example, my values are going to be set to:

  1. “/SolarWinds/InformationService/v3/Json/” plus the URI of the node (which is the variable $clienturi in my sample code)
  2. The field StatCollection set to a certain number of minutes, and the field PollInterval set to a certain number of seconds
  3. The content type is “application/json.” Period. No decisions here, it just has to be included for the command to work.

Meanwhile, the Perl syntax happening here includes:

  1. ‘/SolarWinds/InformationService/v3/Json/’.$clienturi combines (concatenates) the $clienturi variable with the SWIS prefix elements
  2. To create an array of values to update, the format in Perl is:
    StatCollection’=>”5″, “PollInterval”=>”60”
  3. (SEE NOTE BELOW)
  4. The content type gets enclosed in French brackets like this:

    1. {Content-Type”=>”application/json”)

NOTE: that’s how you would manually set the variables. In my code, I’m creating a hash earlier, looking up the matching value in the “Polling Level” custom field, and taking the resulting values.

Other Important Elements for “That One Line of Code”

As I just mentioned, I’m looking up the custom property value, finding it in my hash array, and extracting those values. If you were to assign it manually, it would look like this:

%newpollvalue = ('StatCollection'=>"5", "PollInterval"=>"60");  

The next thing I want to point out is converting the hashed values into JSON-compatible data. The following like does just that, using the encode_json function that comes in the “JSON” package.

my $newpolljson = encode_json \%newpollvalue;  

All of that being said, if this main line was running in a complete vacuum, this this is all you would need. Of course, it’s NOT running in a vacuum and I need to explain a few other dependencies.

Packages and Dependencies

At the top of the script, you’ll find a few packages being invoked. Here’s where each of them comes into play.

use REST::Client;

This sets up the initial REST-full connection to the SolarWinds® server, which you see in the following line.

my $client = REST::Client->new();  

use MIME::Base64;

You should never keep username/passwords in clear text in your script. That’s just lazy (and also dumb, but I’m trying not to judge). Even though my sample script DOES give you the option (commented out) just in case you are having problems you think are due to authentication, it really uses an encrypted password. (More about that in a separate section of this post.) That’s what this module and the following line of code is for.

encode_base64("$username:$password", ''));  

use Crypt::RC4;

This is used to decrypt the password when using secure (i.e., CORRECT) password handling to connect to the SolarWinds server.

use URI::Encode qw(uri_encode);

When you pass a query via REST, it needs to be encapsulated as a universal resource indicator (URI). This module facilitates that behavior, as in:

uri_encode($query));  

use JSON::Parse ‘:all’;

When you get a JSON response back (as when you get the record containing node details), you need to split it up into something readable. That’s how the parse command works.

parse_json $client->responseContent();  

use JSON; 

Finally, as mentioned in the previous section, the values you want to push to the server need to be JSON-ified, and that’s what this module does.

my $newpolljson = encode_json \%newpollvalue;  

Connecting to Orion

NOTE: This section is a complete aside, but also a pet peeve. Connecting correctly (i.e., safely) to your Orion instance is one of those good habits you should have, like eating right, getting enough sleep, and avoiding sweets before bedtime. Which is to say: (almost) everyone understands it’s a good idea, but many will find excuses why they don’t. So skip this section if you must, but understand that it *is* important to your overall Orion API health and welfare.

There are two basic options to connect your script to Orion:

  1. The quick and dirty way that makes security professionals weep and likely gets you in trouble with the auditors.
  1. The safe and correct way that will let you (and your company) sleep easier at night knowing you haven’t thrown the gates of security wide open to the ravenous hordes of hackers that lurk just outside.

The Quick and Dirty Way

This is what I use when I am first throwing a script together and don’t want to worry that something about the way I’m passing credentials is slowing me down.

  1. my $username = ‘knightswhosayni’;  
my $password = 'icky-icky-ptang-ptang'
my $hostname = '10.110.69.72';  
my $client = REST::Client->new();  
$client->getUseragent()->ssl_opts(verify_hostname => 0);  
$client->getUseragent()->ssl_opts(SSL_verify_mode => 'SSL_VERIFY_NONE');  
$client->setHost('https://' . $hostname . ':17778');  
$client->addHeader('Authorization', 'Basic ' . encode_base64("$username:$password", ''));  

The Right Way

This process uses an encrypted password which can be decrypted with a key. The key is in one file on your system (“swkeyfile.txt”) and the encrypted password in another (“$swhashfile.txt”). While this script makes it easy by putting everything in the same directory as the script itself, we’re going to remind you that both files should be in protected areas and should only be read-able by the account the script is running under. Otherwise you have no security at all.

Step 1: Get the Key:

$keyfile = "swkeyfile.txt"; 
open($fh1, '<', $keyfile) or die "Could not open file '$keyfile' $!"; 
while ($row = <$fh1>) {  
   chomp $row;  
   $key=$row;  
}  
close $fh1;  

Step 2: Get the Encrypted Password:

$hashfile = "swhashfile.txt";  
open($fh2, '<', $hashfile) or die "Could not open file '$hashfile' $!";  
while ($row = <$fh2>) {  
  chomp $row;  
   $enc_hash=$row;  
}  
close $fh2;  

Step 3: Unencrypt the Password

$decoded = decode_base64($enc_hash);  
$swpassword = RC4($key, $decoded);

Step 4: Use the Password to Connect to the Orion Server

#Set up the REST connection to the server  
$client = REST::Client->new();  
$client->getUseragent()->ssl_opts(verify_hostname => 0);  
$client->getUseragent()->ssl_opts(SSL_verify_mode => 'SSL_VERIFY_NONE');  
$client->setHost('https://' . $hostname . ':17778');  
$client->addHeader('Authorization', 'Basic ' . encode_base64("$username:$swpassword", ''));  

Step Zero: Create the Key and Hashed Password Files in the First Place

Yes, step zero. Before you can use the safe technique I describe in steps 1-4, you have to create those two files. This is a one-time process, but if you don’t do it, none of the rest of it will work. This is the script you’ll need.

#!/usr/bin/perl  
use strict;  
use warnings;  
use Crypt::RC4;  
use MIME::Base64;  
use Bytes::Random::Secure qw(random_bytes random_bytes_base64 random_bytes_hex);  
    #see https://metacpan.org/pod/release/DAVIDO/Bytes-Random-Secure-0.29/lib/Bytes/Random/Secure.pm for full usage  
#Generate random key and store it in a file  
my $keyfile = "swkeyfile.txt";  
my $key = random_bytes(32); # A string of 32 random bytes.  
open(my $fh1, '>', $keyfile) or die "Could not open file '$keyfile' $!";  
print $fh1 $key;  
close $fh1;   
#Now take the SolarWinds user password and encrypt it using the key
my $swpassword = "YOUR PASSWORD GOES HERE";  
my $encrypted = RC4($key, $swpassword);  
my $encoded = encode_base64($encrypted);  
my $hashfile = "swhashfile.txt";  
open(my $fh2, '>', $hashfile) or die "Could not open file '$hashfile' $!";  
print $fh2 $encoded;  
close $fh2;  

Additional Resources

If you want to learn more about about the Orion API, then you ought to check out the following:

If you, for some insane reason, need to know about using the Orion API via Perl, then this sample offers a lot of examples:

https://github.com/solarwinds/OrionSDK/blob/master/Samples/Perl/query.pl

The Mostly Un-Necessary Summary

The rest of the script is just your usual jumble of error checking, table lookups, etc. I’ve tried to comment the code so that it’s easier to follow, but I welcome your questions (@adatole on thwack.com or @leonadatoon Twitter).

%d bloggers like this: