250 lines
7.5 KiB
Python
250 lines
7.5 KiB
Python
#!/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())
|
|
|