#!/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())