From 51f8181b1f161d959361c518c2aa3909f363757a Mon Sep 17 00:00:00 2001 From: Innovation Date: Wed, 13 Jul 2022 08:15:53 +0100 Subject: [PATCH] Initial Commit --- sysctl.ini.template | 15 +++ sysctl.py | 249 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 sysctl.ini.template create mode 100644 sysctl.py diff --git a/sysctl.ini.template b/sysctl.ini.template new file mode 100644 index 0000000..92aeb55 --- /dev/null +++ b/sysctl.ini.template @@ -0,0 +1,15 @@ +[LOGIN] +nodename = The NAMES of your node - this is what it will respond to when, for example, running a command - [COMMAND>MyNode] +username = The USERNAME of your node's Matrix account - username, NOT @username:example.not +password = The PASSWORD of your node's Matrix account + +[MATRIX] +sysctlChannel = The CHANNEL ID of the Sysctl room +serverAddr = The SERVER ADDRESS of the Matrix server (i.e. matrix.org) +serverWebAddr = The WEB ADDRESS of the Matrix server (usually the same as serverAddr. I use this because I don't have hairpinning.) +admin = The username of the ADMINISTRATOR ACCOUNT - admin, NOT @admin:example.net + +[NODES] +nodes = IP addresses of your nodes, separated with commas - 192.168.1.5,192.168.1.6,192.168.1.7 - THIS INCLUDES THE ONE THIS SCRIPT IS RUNNING ON +nodeNames = The NAMES of your nodes, separated with commas - NodeOne,NodeTwo,NodeThree - THIS INCLUDES THE ONE THIS SCRIPT IS RUNNING ON +nodeUsernames = The USERNAMES of your nodes, separated with commas - nodeone,nodetwo,nodethree - THIS INCLUDES THE ONE THIS SCRIPT IS RUNNING ON - nodeone NOT @nodeone:example.net diff --git a/sysctl.py b/sysctl.py new file mode 100644 index 0000000..1008ccd --- /dev/null +++ b/sysctl.py @@ -0,0 +1,249 @@ +#!/bin/python3.7 + +# (C) Innovation Science, Katie Martin, 2022 + +import asyncio +from nio import (AsyncClient, RoomMessageText) +#import os +#import time +from ping3 import ping +import urllib3 +import configparser + + +nodename="" +username="" +password="" +sysctlChannel="" +serverAddr="" +serverWebAddr="" +admin="" +loglevels=["Info", "Note", "Caution", "Warning", "Error", "FATAL", "EMERGENCY", "FAILURE"] + +nodes=[] +nodeNames=[] +nodeUsernames=[] +nodeStatus=[] +# Acknowledgements supresses the parser from sending the same warning over and over again. +# For example, if node 1 is down, the first flag in the second array (counting from zero) is changed to true. This prevents parseStatus from sending the same warning. +# 0 - Offline +# 1 - Cannot get daemons +# 2 - +# 10-19 - Each daemon (controlled by internal servers) +acknowledgements=[] +ContinueFlag=True +FirstRun=True + +def loadConfig(): # TODO: Proper error checking here + global nodename + global username + global password + global sysctlChannel + global serverAddr + global serverWebAddr + global admin + global nodes + global nodeNames + global nodeUsernames + global nodeStatus + global acknowledgements + + blankAcknowledgements = [False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False] + + config = configparser.ConfigParser() + config.read('sysctl.ini') + + try: + # Load login info + nodename=config['LOGIN']['nodename'] + username=config['LOGIN']['username'] + password=config['LOGIN']['password'] + + # Load Matrix server and room info + sysctlChannel=config['MATRIX']['sysctlChannel'] + serverAddr=config['MATRIX']['serverAddr'] + serverWebAddr=config['MATRIX']['serverWebAddr'] + admin=config['MATRIX']['admin'] + + # Load node info + tempnodes=config['NODES']['nodes'] + tempnodeNames=config['NODES']['nodeNames'] + tempnodeUsernames=config['NODES']['nodeUsernames'] + + nodes=tempnodes.split(',') + nodeNames=tempnodeNames.split(',') + nodeUsernames=tempnodeUsernames.split(',') + + # Prepare node online status and acknowledgements + for x in range(len(nodes)): + nodeStatus.append(False) + acknowledgements.append(blankAcknowledgements) + except KeyError: + print("KeyError encountered. This likely means your config.ini is missing or misconfigured.") + print("Please read the docs.") + exit(1) + + print("Config loaded") + +# NOTE: This assumes that the user comes from the same address as the bot. +async def fullUsername(user): + return "@" + user + ":" + serverAddr + +async def sendMessage(loglevel, message): + await client.room_send( + room_id=sysctlChannel + ":" + serverAddr, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": "[" + loglevels[loglevel] + "] " + message + } + ) + +# NOTE: This can be inherently insecure. This assumes that the room is set +# to have a required power level that only Admins and the Sysctl bot(s) +# have. What I have below should be relatively fine, but I wouldn't trust +# it with my car keys. +async def parseMessage(room, event): + doParse = False + if (room.room_id == (sysctlChannel + ":" + serverAddr)): + for x in range(len(nodeUsernames)): + if(event.sender != await fullUsername(username)): # We don't want it to respond to itself. + if((event.sender == await fullUsername(admin)) or (event.sender == await fullUsername(nodeUsernames[x]))): + doParse = True + break + + if doParse: + # I hate you Python. + bodyPreparse = (event.body).split(']') + if (bodyPreparse[0] == ('[COMMAND>' + nodename)): + await runCommand(event.sender, bodyPreparse[1]) + +async def sendCommand(reciever, command): + await client.room_send( + room_id=sysctlChannel + ":" + serverAddr, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": "[COMMAND>" + reciever + "] " + command + } + ) + +async def sendComm(reciever, command): + await client.room_send( + room_id=sysctlChannel + ":" + serverAddr, + message_type="m.room.message", + content={ + "msgtype": "m.text", + "body": "[COMM>" + reciever + "] " + command + } + ) + +async def runCommand(sender, command): + if (command[0] == " "): # Usually the command variable will have a space at the beginning. + command=command[1:] + + # I hate Python again. + if (command == "test"): + await sendMessage(0, (sender + ", test recieved.")) + #elif (command == ""): + # await sendMessage(0, (sender + "")) + else: + await sendMessage(0, (sender + ", bad command.\nAvailable commands:\ntest")) + #await sendMessage(0, (sender + ", running ")) + +async def hostIsOnline(ip): + response = ping(ip) + + if response != False and response != None: + return True + return False + +async def getOnlineStatus(): + global nodeStatus + global FirstRun + global acknowledgements + + for x in range(len(nodes)): + if await hostIsOnline(nodes[x]): + nodeStatus[x]=True + if acknowledgements[x][0] or FirstRun: + await sendMessage(0, nodeNames[x] + " online.") + acknowledgements[x][0]=False + else: + nodeStatus[x]=False + if acknowledgements[x][0]==False: + await sendMessage(7, nodeNames[x] + " OFFLINE!") + acknowledgements[x][0]=True + + + +async def parseStatus(): + global acknowledgements + x=0 + +async def getDaemonStatus(): + global acknowledgements + + http = urllib3.PoolManager(retries=1, timeout=2.0) + for x in range(len(nodes)): + if nodeStatus[x]: + try: + r = http.request("GET", "http://" + nodes[x] + ":9050/servers/") + except urllib3.exceptions.ConnectTimeoutError: + o=0 # Do nothing. This is handled later. + if r.status == 200: + nodeDaemons = (r.data.decode('utf-8')).split('\n') + + for y in range(len(nodeDaemons)): + s = http.request("GET", "http://" + nodes[x] + ":9050/servers/" + nodeDaemons[y]) + if s.data.decode('utf-8') == "1": + if acknowledgements[x][10+y]: + await sendMessage(0, "Daemon '" + nodeDaemons[y] + "' online on " + nodeNames[x]) + acknowledgements[x][10+y]=False + else: + if acknowledgements[x][10+y]==False: + await sendMessage(4, "Daemon '" + nodeDaemons[y] + "' offline on " + nodeNames[x]) + acknowledgements[x][10+y]=True + + if acknowledgements[x][1]: + await sendMessage(0, "Now able to get daemons - " + nodeNames[x]) + acknowledgements[x][1]=False + else: + if acknowledgements[x][1]==False: + await sendMessage(4, "Could not get daemons - " + nodeNames[x]) + acknowledgements[x][1]=True + +async def main(): + global FirstRun + global ContinueFlag + + await client.login(password) + + await sendMessage(1, "Sysctl process for " + nodename + " started.") + await sendMessage(1, "Prepare startup checklist...") + await sendMessage(1, "Starting command listener...") + await client.sync() + + client.add_event_callback(parseMessage, RoomMessageText) + + while ContinueFlag: + try: + await client.sync(timeout=30000) + await getOnlineStatus() + await getDaemonStatus() + #await parseStatus() # This might do something someday. + + if FirstRun: + await sendMessage(1, "Startup complete.") + FirstRun=False + except KeyboardInterrupt: # This makes it so CTRL+C begins the program exiting sequence, and so the program exits gracefully at any point during execution. + ContinueFlag = False + await sendMessage(3, "Sysctl main loop interrupted - " + nodename + " sysctl stop") + break + + await client.close() + +loadConfig() +client = AsyncClient("http://" + serverWebAddr, "@" + username + ":" + serverAddr) +asyncio.get_event_loop().run_until_complete(main()) +