nightly analytics

This commit is contained in:
notplants 2023-04-27 14:16:42 +05:30
parent 4ee870f22b
commit 3ce0d088ec
6 changed files with 299 additions and 52 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@ download.png
test.png test.png
.idea .idea
secrets.json secrets.json
__pycache__

View File

@ -3,6 +3,7 @@ script to do a basic holistic test of the cowmesh network,
testing internet connection speed using iperf, testing internet connection speed using iperf,
between all nodes in the mesh between all nodes in the mesh
""" """
import datetime
import os import os
import re import re
import time import time
@ -16,10 +17,18 @@ SECRETS_PATH = os.path.join(PROJECT_PATH, "secrets.json")
with open(SECRETS_PATH, 'r') as f: with open(SECRETS_PATH, 'r') as f:
SECRETS = json.loads(f.read()) SECRETS = json.loads(f.read())
# nodes = [
# "jaaga",
# "redcottage",
# "new-gazebo2",
# "kotemanetp",
# "guard"
# ]
nodes = [ nodes = [
"jaaga", # "jaaga",
# "new-gazebo", # "redcottage",
"redcottage", # "new-gazebo2",
"kotemanetp", "kotemanetp",
"guard" "guard"
] ]
@ -27,6 +36,8 @@ nodes = [
host_to_ip = { host_to_ip = {
"jaaga": "10.56.121.19", "jaaga": "10.56.121.19",
"redcottage": "10.56.58.194", "redcottage": "10.56.58.194",
"redcottage2": "10.56.114.42",
"new-gazebo2": "10.56.114.42",
"new-gazebo": "10.56.113.2", "new-gazebo": "10.56.113.2",
"guard": "10.56.121.73", "guard": "10.56.121.73",
"kotemanetp": "10.56.40.113" "kotemanetp": "10.56.40.113"
@ -35,73 +46,100 @@ host_to_ip = {
results = {} results = {}
def test_between_two_nodes(node_a, node_b): class CowmeshIperfTester:
print("running test from {} to {}".format(node_a, node_b))
u_name = 'root'
pswd = SECRETS["ROUTER_PASSWORD"]
myconn = paramiko.SSHClient() def __init__(self, log=None, debug=False):
myconn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) if log:
self.log = log
self.debug = debug
session = myconn.connect(node_a, username =u_name, password=pswd) async def log(self, msg):
print(msg)
ip = host_to_ip[node_b] async def debug_log(self, msg):
if self.debug:
await self.log(msg)
remote_cmd = 'iperf -c {ip} -p 5001'.format(ip=ip) async def test_between_two_nodes(self, node_a, node_b):
(stdin, stdout, stderr) = myconn.exec_command(remote_cmd) await self.log("++ running test from {} to {}".format(node_a, node_b))
output = str(stdout.read())
print("output: {}".format(output))
print("errors: {}".format(stderr.read()))
myconn.close()
match = re.search("(\S+) Mbits", output)
if match:
to_return = match.group(1)
else:
to_return = None
return to_return
async def start_iperf_servers():
for node in nodes:
print("starting iperf server on {}".format(node))
u_name = 'root' u_name = 'root'
pswd = SECRETS["ROUTER_PASSWORD"] pswd = SECRETS["ROUTER_PASSWORD"]
myconn = paramiko.SSHClient() myconn = paramiko.SSHClient()
myconn.set_missing_host_key_policy(paramiko.AutoAddPolicy()) myconn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
session = myconn.connect(node, username=u_name, password=pswd) myconn.connect(node_a, username =u_name, password=pswd)
remote_cmd = 'iperf -s &' ip = host_to_ip[node_b]
remote_cmd = 'iperf -c {ip} -p 5001'.format(ip=ip)
(stdin, stdout, stderr) = myconn.exec_command(remote_cmd) (stdin, stdout, stderr) = myconn.exec_command(remote_cmd)
print("{}".format(stdout.read())) output = str(stdout.read())
print("{}".format(type(myconn))) await self.debug_log("output: {}".format(output))
print("Options available to deal with the connectios are many like\n{}".format(dir(myconn))) await self.debug_log("errors: {}".format(stderr.read()))
myconn.close() myconn.close()
async def run_test(): match = re.search("(\S+) Mbits", output)
if match:
to_return = match.group(1)
else:
to_return = None
await start_iperf_servers() return to_return
for node_a in nodes: async def start_iperf_servers(self):
for node_b in nodes: for node in nodes:
if node_a == node_b: print("++ starting iperf server on {}".format(node))
print("skip self") u_name = 'root'
continue pswd = SECRETS["ROUTER_PASSWORD"]
r = test_between_two_nodes(node_a, node_b) myconn = paramiko.SSHClient()
result_key = "{} -> {}".format(node_a, node_b) myconn.set_missing_host_key_policy(paramiko.AutoAddPolicy())
results[result_key] = r
myconn.connect(node, username=u_name, password=pswd)
remote_cmd = 'iperf -s &'
(stdin, stdout, stderr) = myconn.exec_command(remote_cmd)
await self.debug_log("{}".format(stdout.read()))
await self.debug_log("{}".format(type(myconn)))
await self.debug_log("Options available to deal with the connections are many like\n{}".format(dir(myconn)))
myconn.close()
async def run_test(self):
await self.start_iperf_servers()
for node_a in nodes:
for node_b in nodes:
if node_a == node_b:
await self.debug_log("skip self")
continue
r = await self.test_between_two_nodes(node_a, node_b)
result_key = "{} -> {}".format(node_a, node_b)
results[result_key] = r
async def output_results(self):
results_str = ""
now = datetime.datetime.now()
date = now.date()
time = now.time()
results_str += "**** iperf results on {date:%m-%d-%Y} at {time:%H:%M}:\n\n".format(date=date, time=time)
for test_name, result in results.items():
results_str += "{}: {} mbps\n".format(test_name, result)
await self.log(results_str)
try: if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(run_test()) try:
tester = CowmeshIperfTester()
print("** final results **") async def main_fun():
for test_name, result in results.items(): await tester.run_test()
print("{}: {} mbps".format(test_name, result)) await tester.output_results()
except (OSError, asyncssh.Error) as exc: asyncio.get_event_loop().run_until_complete(main_fun())
sys.exit('SSH connection failed: ' + str(exc))
except (OSError, asyncssh.Error) as exc:
sys.exit('SSH connection failed: ' + str(exc))

121
moonlight_analytics.py Normal file
View File

@ -0,0 +1,121 @@
import asyncio
import logging
from telegram import Update
from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters
import os
import json
from cowmesh_iperf_test import CowmeshIperfTester
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
SECRETS_PATH = os.path.join(PROJECT_PATH, "secrets.json")
with open(SECRETS_PATH, 'r') as f:
SECRETS = json.loads(f.read())
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_message(chat_id=update.effective_chat.id, text="This is the moonlight analytics bot for mesh network diagnostics.\n\nType /help to see available commands.", message_thread_id=update.message.message_thread_id)
async def caps(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""
this is just a function to test that the bot is working as expected
"""
text_caps = ' '.join(context.args).upper()
print("chat_id: {}".format(update.effective_chat.id))
print("message_thread_id: {}".format(update.message.message_thread_id))
await context.bot.send_message(chat_id=update.effective_chat.id, text=text_caps, message_thread_id=update.message.message_thread_id)
async def iperf(update: Update, context: ContextTypes.DEFAULT_TYPE):
async def log(msg):
await context.bot.send_message(chat_id=update.effective_chat.id, text=msg, message_thread_id=update.message.message_thread_id)
await log("++ starting iperf test")
tester = CowmeshIperfTester(log=log)
await tester.run_test()
await tester.output_results()
async def unknown(update: Update, context: ContextTypes.DEFAULT_TYPE):
await context.bot.send_message(chat_id=update.effective_chat.id, text="Sorry, I didn't understand that, please run /help for a list of available commands.", message_thread_id=update.message.message_thread_id)
def about_message():
msg = "This is a bot designed to help measure the performance of a mesh network through active testing. " \
"Every night the bot runs a network test using iperf to measure the connectivity between all the nodes in the mesh, " \
"and logs the result to this channel. " \
"Members of this channel can also initiate a new network iperf test at any time by sending a message to this channel, " \
"with the command /iperf . " \
"Telegram users outside of this channel cannot initiate a test, to help keep the network secure from being " \
"overrun by malicious users. " \
"The network test runs at night because that is when the fewest people are using the network, and so is more likely to " \
"give consistent results with less random variability. " \
"Please be mindful of initiating too many iperf tests during the day, as it uses a lot of network resources " \
"to run the test and could interfere with the internet connections of people using the network. "
return msg
def help_message():
msg = "This bot runs an iperf test every night and logs the results here. You can also initiate a new test using the command /iperf " \
"or read a longer message explaining how this bot works by typing the command /readme."
return msg
async def about(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = about_message()
await context.bot.send_message(chat_id=update.effective_chat.id, text=text, message_thread_id=update.message.message_thread_id)
async def help_fun(update: Update, context: ContextTypes.DEFAULT_TYPE):
text = help_message()
await context.bot.send_message(chat_id=update.effective_chat.id, text=text, message_thread_id=update.message.message_thread_id)
async def nightly_iperf():
token = SECRETS["TELEGRAM_TOKEN"]
application = ApplicationBuilder().token(token).build()
chat_id = SECRETS["TELEGRAM_LOG_CHAT_ID"]
message_thread_id = SECRETS.get("TELEGRAM_LOG_MESSAGE_THREAD_ID")
bot = application.bot
async def log(msg):
await bot.send_message(chat_id=chat_id, text=msg, message_thread_id=message_thread_id)
await log("☾☾ starting nightly iperf test")
tester = CowmeshIperfTester(log=log)
await tester.run_test()
await tester.output_results()
def init_bot_listener():
token = SECRETS["TELEGRAM_TOKEN"]
application = ApplicationBuilder().token(token).build()
start_handler = CommandHandler('start', start)
application.add_handler(start_handler)
caps_handler = CommandHandler('caps', caps)
application.add_handler(caps_handler)
about_handler = CommandHandler('readme', about)
application.add_handler(about_handler)
help_handler = CommandHandler('help', help_fun)
application.add_handler(help_handler)
iperf_handler = CommandHandler('iperf', iperf)
application.add_handler(iperf_handler)
unknown_handler = MessageHandler(filters.COMMAND, unknown)
application.add_handler(unknown_handler)
application.run_polling()
if __name__ == '__main__':
init_bot_listener()

7
nightly_test.py Normal file
View File

@ -0,0 +1,7 @@
import argparse
from moonlight_analytics import nightly_iperf
import asyncio
if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(nightly_iperf())

19
server.py Normal file
View File

@ -0,0 +1,19 @@
import os
from flask import Flask, request, render_template, jsonify
app = Flask(__name__, static_folder='public', template_folder='templates')
app.config['TEMPLATES_AUTO_RELOAD'] = True
@app.route('/')
def homepage():
return render_template('index.html')
@app.route('/test')
def testpage():
return "hi! its test"
# this line start the server with the 'run()' method
if __name__ == '__main__':
app.run()

61
templates/index.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<title>🦋</title>
<meta name="description" content="moonlight analytics">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {
box-sizing: border-box;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
background-color: lightblue;
}
/* <3<3<3 write all of your loving css ABOVE this line <3<3<3 */
</style>
</head>
<body>
<!-- <3<3<3 keep all of your html BELOW this line <3<3<3 -->
<div id="button-container">
<button id="button">run network test</button>
<div class="output-wrapper"></div>
</div>
<script src="https://code.jquery.com/jquery-2.2.1.min.js"
integrity="sha256-gvQgAFzTH6trSrAWoH1iPo9Xc96QxSZ3feW6kem+O00="
crossorigin="anonymous">
// this part loads in a javascript library called jquery. you can tell when we're using jquery by the $ in front
</script>
<script>
$('#button').bind('click', function() {
// below is the code that gets run afer the button get clicked
// first we print to the console that the button was clicked
console.log("button clicked!");
// then we use ajax, to make a request to the server at the /generate route
$.get('/init-test', function(response) {
// below is the code that runs when the python server route returns,
// response is a variable which holds the response from the server (a string of text)
// and which we can use in whatever way we want
// ... first we take this response and print it to the console
console.log(response);
// and then we insert the response into the .output-wrapper element
// (this also replaces whatever was in this element before)
$('.output-wrapper').html(response);
});
return false;
});
</script>
</body>
</html>