DIY Uptime Monitoring

 •  Filed under uptime, monitor, monitoring, diy, python, bash

Uptime monitoring software is a must have in an IT production environment, and there are hundreds of solutions available for that specific job. But what if you are just looking for a simple yet functional uptime monitor? A DIY solution might be the perfect way to go for you. Why? Because of multiple reasons, namely:

  • Full control over the software: your can add or remove functionality and design the software to your likings. You are the developer!
  • It's just what you need: why run a big bloated application for such a small task again? You can make your software as lightweight as you would like.
  • What's more awesome than designing and developing your own software?

So, I wrote a complete solution that exists of a client script written in Bash and a server application written in Python.

The database

A monitoring system would be nowhere without a database. I decided to use SQLite because of its ease of use. This is the SQL code to create the needed tables for this project:

CREATE TABLE `DeviceStatuses` (  
    `id` INTEGER NOT NULL UNIQUE,
    `MachineIdentifier` TEXT NOT NULL,
    `EventDate` TEXT NOT NULL,
    `Up` INTEGER NOT NULL,
    PRIMARY KEY(`id`)
);

CREATE TABLE `RegisteredDevices` (  
    `id` INTEGER NOT NULL UNIQUE,
    `MachineIdentifier` TEXT NOT NULL UNIQUE,
    `PublicAddress` TEXT NOT NULL,
    `MachineGuid` TEXT NOT NULL,
    PRIMARY KEY(`id`)
);

Save your database as StatusData.db.

The server

The server application that I made is written in Python. More specifically, it uses sqlite as the database engine, and web.py to listen as an API for clients. This is what the public API currently supports:

  • Registering clients
  • Acknowledge clients (is the client up?)

This is what I yet have to do an/or add:

  • Removing clients via the API

Oh well, you can still remove clients manually by editing the database 🙂

Here's the code of my monitoring server:

# server.py
# written and developed by Bart Simons, 2016

import ConfigParser  
import datetime  
import os  
import re  
import sqlite3  
import time  
import threading  
import uuid  
import web

web.config.debug = False

config = ConfigParser.RawConfigParser()

if not os.path.isfile('config.xml'):  
    config.read('config.xml')
    config.add_section('NetworkConfiguration')
    config.set('NetworkConfiguration', 'Address', '127.0.0.1')
    config.set('NetworkConfiguration', 'Port', '4086')
    config.add_section('SecurityConfiguration')
    config.set('SecurityConfiguration', 'AccessKey', 'AccessKeyGoesHere')
    config.add_section('IntervalConfiguration')
    config.set('IntervalConfiguration', 'UpdateCheckInterval', '3')
    with open('config.xml', 'wb') as CfgFile:
        config.write(CfgFile)
else:  
    config.read('config.xml')

accesskey = config.get('SecurityConfiguration', 'AccessKey')  
listen_address = config.get('NetworkConfiguration', 'Address')  
listen_port = config.get('NetworkConfiguration', 'Port')  
check_interval = config.get('IntervalConfiguration', 'UpdateCheckInterval')

db = sqlite3.connect('StatusData.db', check_same_thread=False)  
dbcursor = db.cursor()

urls = (  
    '/register', 'register',
    '/acknowledge', 'acknowledge'
)

client_objects = []

def checkClient(client_identifier):  
    while True:
        for client_object in client_objects:
            if (client_identifier == client_object[0]):
                try:
                    db.execute("INSERT INTO `DeviceStatuses` (id,MachineIdentifier,EventDate,Up) VALUES (NULL, ?, ?, ?)", (str(client_object[0]), str(datetime.datetime.now()), str(client_object[3])))
                    db.commit()
                    client_objects[client_objects.index(client_object)][3] = str(False)
                except:
                    print("Something went wrong while committing to the database!")
        time.sleep(int(check_interval))

for client in db.execute("SELECT * FROM `RegisteredDevices`").fetchall():  
    client_objects.append([str(client[1]), str(client[2]), str(client[3]), False])
    threading.Thread(target=checkClient, args=(str(client[1]),)).start()

validate_uuid = re.compile('[0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15}\Z', re.I)

app = web.application(urls, globals())

class acknowledge:  
    def GET (self):
        web.ctx.status = '200 OK'
        return 'This method only supports POST requests!'

    def POST (self):
        accesskey_posted = web.ctx.env.get('HTTP_AUTHENTICATION_KEY', 'None')
        machine_identifier = web.ctx.env.get('HTTP_MACHINE_IDENTIFIER', 'None')
        machine_guid = web.ctx.env.get('HTTP_MACHINE_GUID', 'None').replace("-", "")
        public_addr = web.ctx['ip']

        if (accesskey_posted == accesskey and machine_identifier != "None" and public_addr != "None" and validate_uuid.match(machine_guid)):
            already_exists_check = db.execute("SELECT * FROM `RegisteredDevices` WHERE MachineIdentifier = ?", [machine_identifier,]).fetchall()
            if (len(already_exists_check) == 1):
                for client_object in client_objects:
                    if (machine_identifier == client_object[0] and machine_guid == client_object[2].replace("-", "")):
                        client_objects[client_objects.index(client_object)][3] = str(True)
                        web.ctx.status = '200 OK'
                        return '200 - yay!'
            else:
                web.ctx.status = '404 Not Found'
                return '404 - machine not found.'
        else:
            web.ctx.status = '401 Unauthorized'
            return '401 - Get lost!'

class register:  
    def GET (self):
        web.ctx.status = '200 OK'
        return 'This method only supports POST requests!'

    def POST (self):
        accesskey_posted = web.ctx.env.get('HTTP_AUTHENTICATION_KEY', 'None')
        machine_identifier = web.ctx.env.get('HTTP_MACHINE_IDENTIFIER', 'None')
        machine_guid = web.ctx.env.get('HTTP_MACHINE_GUID', 'None').replace("-", "")
        public_addr = web.ctx['ip']

        if (accesskey_posted == accesskey and machine_identifier != "None" and public_addr != "None" and validate_uuid.match(machine_guid)):
            already_exists_check = db.execute("SELECT * FROM `RegisteredDevices` WHERE MachineIdentifier = ?", [machine_identifier,]).fetchall()
            if (len(already_exists_check) == 1):
                for client_object in client_objects:
                    if (machine_identifier == client_object[0]):
                        client_objects[client_objects.index(client_object)][1] = str(public_addr)
                        client_objects[client_objects.index(client_object)][2] = str(machine_guid)
                db.execute("UPDATE `RegisteredDevices` SET PublicAddress=?, MachineGuid=? WHERE MachineIdentifier=?", (public_addr,machine_guid,machine_identifier))
            else:
                client_objects.append([str(machine_identifier), str(public_addr), str(machine_guid), True])
                threading.Thread(target=checkClient, args=(str(machine_identifier),)).start()
                db.execute("INSERT INTO `RegisteredDevices` (id,MachineIdentifier,PublicAddress,MachineGuid) VALUES (NULL, ?, ?, ?)", (machine_identifier, public_addr, machine_guid))
            db.commit()
            web.ctx.status = '200 OK'
            return '200 - yay!'
        else:
            print(str(web.ctx))
            web.ctx.status = '401 Unauthorized'
            return '401 - Get lost!'


if __name__ == "__main__":  
    web.httpserver.runsimple(app.wsgifunc(), (listen_address, int(listen_port)))

Save the source code as server.py. You can now start the server with this command:

python server.py  

If you are getting an error, it might be because you are missing some dependencies (like web.py for example). Also, make sure you are running the code on Python 2.


The client

The client is really basic, simple and lightweight. It should run on anything that has Bash, cURL and a working internet connection! Here's the code:

# client.sh
# written and developed by Bart Simons, 2016

MASTER_ADDR="192.168.1.70"  
MASTER_PORT="4086"  
MASTER_PROTOCOL="http"  
MASTER_ACCESSKEY="AccessKeyGoesHere"

MACHINE_IDENTIFIER="Macbook-Bart"  
MACHINE_GUID="D40145AC-AB26-409B-9374-2EA99559B70D"

UPDATE_INTERVAL="1.5"

REGISTER_DEVICE_CMD="curl -s -X POST -H 'Authentication-Key: $MASTER_ACCESSKEY' -H 'Machine-Identifier: $MACHINE_IDENTIFIER' -H 'Machine-Guid: $MACHINE_GUID' $MASTER_PROTOCOL://$MASTER_ADDR:$MASTER_PORT/register > /dev/null"  
eval $REGISTER_DEVICE_CMD

while true; do  
    sleep $UPDATE_INTERVAL
    UPDATE_DEVICE_CMD="curl -s -X POST -H 'Authentication-Key: $MASTER_ACCESSKEY' -H 'Machine-Identifier: $MACHINE_IDENTIFIER' -H 'Machine-Guid: $MACHINE_GUID' $MASTER_PROTOCOL://$MASTER_ADDR:$MASTER_PORT/acknowledge > /dev/null"
    eval $UPDATE_DEVICE_CMD
done  

Save the source code as client.sh. You can start the client with bash client.sh.

Some tips: when you run the server, it creates a config.xml file which can be edited. You can modify the listening IP address, TCP port, access key and update interval. Also, try to keep the server check interval on twice the amount of your clients.

Try it out, and have some fun with this little project. Good luck!