Initial Commit
This commit is contained in:
parent
926ffa46b3
commit
51f8181b1f
15
sysctl.ini.template
Normal file
15
sysctl.ini.template
Normal file
|
@ -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
|
249
sysctl.py
Normal file
249
sysctl.py
Normal file
|
@ -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())
|
||||
|
Loading…
Reference in a new issue