commit f3cb525fed0b194e58ad3976afe2d947440ce488 Author: pporcheret Date: Fri Jun 30 13:40:48 2023 +0200 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..d107cbd --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Install + +For Debian 12 (and more...) + +On server : +```bash +apt install python3-flask python3-paramiko python3-psutil +``` + +On nodes : +```bash +apt install python3-psutil +``` + +For Debian < 12 +```bash +pip3 install psutil flask paramiko +``` + +On nodes : +```bash +pip3 install psutil +``` +If you prefer (recommended), you can run in an python env + +```bash +apt install python3-venv + +python3 -m venv Pymonit +. /Pymonit/bin/activate +pip3 install psutil flask paramiko + +``` + + +# Configuration + +## Clients + +Add your client in the python list client: + +```bash +client = ['192.168.0.1','server.local','server.mydns'] +``` + +## Generate keys for connection + +Becarrefull, avoid using a root user to start application and connect to client. +Use a dedicated user. + +On server, generate a pair of key : +```bash +ssh-keygen -b 2048 -t rsa +``` + +And push the public key in your clients : + +```bash +ssh-copy-id -i /.ssh/id_rsa.pub @ +``` + +## Configuration on application + +You have to report these info on the application : + +```bash +class ssh_conf: + port = 22 + username = '' + password ='/.ssh/id_rsa.pub' + +``` + +For testing purpose, but still not recommended, you can use a password in configuration file. + +```bash +class ssh_conf: + port = 22 + username = '' + password ='' +``` + + +## Flask configuration +Configure the flask configuration according to your needs (port,host,...) +```bash +class flask_conf: + port = 8090 + host = "0.0.0.0" + thread = False + debug = False + reloader = False + ssl = 'adhoc' +``` +NB: the parameter ssl = 'adhoc' will activate the HTTPS with a self certificate. +If you get an error, you should add this package : + +```bash +pip3 install pyopenssl +``` + +## Path configuration +```bash +class paths : + basedir = os.path.abspath(os.path.dirname(__file__)) + agent_path = basedir+'/agent.py' + remote_path = '/var/tmp/script.py' +``` + +remote_path : the location where the agent will be pushed and execute. +Take care that your used is able to write and execute in this location. + +## Systemctl (optional) + +To easy start, stop and restart application, you can use the monit.service template. + +Replace the path according to your system in : + +```bash +[Unit] +Description= Monit service +After=multi-user.target +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /application/path/app.py +[Install] +WantedBy=multi-user.target +``` + +Copy the file in your systemd folder : + +```bash + +cp ./monit.service /etc/systemd/system/ + +``` + +Then you can start it : +```bash +systemctl start monit.service +``` +To get it automaticaly start with your system: +```bash +systemctl enable monit.service +``` diff --git a/agent.py b/agent.py new file mode 100755 index 0000000..bc26bc4 --- /dev/null +++ b/agent.py @@ -0,0 +1,129 @@ +#!/usr/bin/python3 +import re +import os +import psutil + +units_map = [ + (1<<50, ' PB'), + (1<<40, ' TB'), + (1<<30, ' GB'), + (1<<20, ' MB'), + (1<<10, ' KB'), + (1, (' byte', ' bytes')), +] + +def human_size(bytes, units=units_map): + for factor, suffix in units: + if bytes >= factor: + break + amount = int(bytes / factor) + if isinstance(suffix, tuple): + singular, multiple = suffix + if amount == 1: + suffix = singular + else: + suffix = multiple + return str(amount) + suffix + +class ENV: + def __init__(self): + self.hostname = psutil.os.uname().nodename + self.cpu_number = psutil.cpu_count(logical=False) + self.vcpu = psutil.cpu_count() + self.mem_max = human_size(psutil.virtual_memory().total) + +class CPU: + def __init__(self): + self.cpu = {} + self.cpu['percent'] = psutil.cpu_percent(interval=0.0, percpu=False) + self.cpu['time_user'] = psutil.cpu_times_percent().user + self.cpu['time_nice'] = psutil.cpu_times_percent().nice + self.cpu['time_system'] = psutil.cpu_times_percent().system + self.cpu['time_idle'] = psutil.cpu_times_percent().idle + self.cpu['time_iowait'] = psutil.cpu_times_percent().iowait + self.cpu['time_irq'] = psutil.cpu_times_percent().irq + self.cpu['time_softirq'] = psutil.cpu_times_percent().softirq + self.cpu['time_steal'] = psutil.cpu_times_percent().steal + self.cpu['time_guest'] = psutil.cpu_times_percent().guest + self.cpu['time_guest_nice'] = psutil.cpu_times_percent().guest_nice + +class Memory: + def __init__(self): + + self.mem = {} + self.mem['total'] = human_size(psutil.virtual_memory().total) + self.mem['available'] = human_size(psutil.virtual_memory().available) + self.mem['percent'] = psutil.virtual_memory().percent + self.mem['used'] = human_size(psutil.virtual_memory().used) + self.mem['free'] = human_size(psutil.virtual_memory().free) + self.mem['active'] = human_size(psutil.virtual_memory().active) + self.mem['inactive'] = human_size(psutil.virtual_memory().inactive) + self.mem['buffers'] = human_size(psutil.virtual_memory().buffers) + self.mem['cached'] = human_size(psutil.virtual_memory().cached) + self.mem['shared'] = human_size(psutil.virtual_memory().shared) + self.mem['slab'] = human_size(psutil.virtual_memory().slab) + +class Swap: + def __init__(self): + self.swap = {} + self.swap['total'] = human_size(psutil.swap_memory().total) + self.swap['used'] = human_size(psutil.swap_memory().used) + self.swap['free'] = human_size(psutil.swap_memory().free) + self.swap['percent'] = psutil.swap_memory().percent + self.swap['sin'] = human_size(psutil.swap_memory().sin) + self.swap['sout'] = human_size(psutil.swap_memory().sout) + +class Disks: + def __init__(self): + num=0 + for i in psutil.disk_partitions(): + exec("self.disk_"+str(num)+" = {}") + exec("self.disk_"+str(num)+"['device'] = '"+str(i.device)+"'") + exec("self.disk_"+str(num)+"['mountpoint'] = '"+str(i.mountpoint)+"'") + exec("self.disk_"+str(num)+"['fstype'] = '"+str(i.fstype)+"'") + exec("self.disk_"+str(num)+"['opts'] = '"+str(i.opts)+"'") + exec("self.disk_"+str(num)+"['maxfile'] = '"+str(i.maxfile)+"'") + exec("self.disk_"+str(num)+"['maxpath'] = '"+str(i.maxpath)+"'") + exec("self.disk_"+str(num)+"['size_total'] = '"+str(human_size(psutil.disk_usage(i.mountpoint).total))+"'") + exec("self.disk_"+str(num)+"['size_used'] = '"+str(human_size(psutil.disk_usage(i.mountpoint).used))+"'") + exec("self.disk_"+str(num)+"['size_free'] = '"+str(human_size(psutil.disk_usage(i.mountpoint).free))+"'") + exec("self.disk_"+str(num)+"['size_percent'] = '"+str(psutil.disk_usage(i.mountpoint).percent)+"'") + num+=1 + +class Network: + def __init__(self): + num=0 + for interface in psutil.net_if_addrs(): + exec("self.net_"+str(num)+" = {}") + for detail in psutil.net_if_addrs()[interface]: + exec("self.net_"+str(num)+"['name'] = '"+str(interface)+"'") + ip_pattern = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$") + if ip_pattern.match(detail.address): + exec("self.net_"+str(num)+"['address_v4'] = '"+str(detail.address)+"'") + else: + exec("self.net_"+str(num)+"['address_v6'] = '"+str(detail.address)+"'") + if detail.netmask: + if ip_pattern.match(detail.netmask): + exec("self.net_"+str(num)+"['netmask_v4'] = '"+str(detail.netmask)+"'") + else: + exec("self.net_"+str(num)+"['netmask_v6'] = '"+str(detail.netmask)+"'") + num+=1 + +class Monit: + def __init__(self): + self.env = ENV() + self.cpu = CPU() + self.mem = Memory() + self.swap = Swap() + self.disks = Disks() + self.network = Network() + +Localhost = Monit() +full = {} +full.update(vars(Localhost.env)) +full.update(vars(Localhost.cpu)) +full.update(vars(Localhost.mem)) +full.update(vars(Localhost.swap)) +full.update(vars(Localhost.disks)) +full.update(vars(Localhost.network)) +print (full) diff --git a/app.py b/app.py new file mode 100755 index 0000000..cbdd759 --- /dev/null +++ b/app.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 +import paramiko +from config import * +from flask import Flask, redirect, render_template, request, url_for, Response + +app = Flask(__name__) + + +def get_remote(hostname, port, username, password, local_script_path, remote_script_path): + ssh = paramiko.SSHClient() + try: + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname, port, username, password) + sftp = ssh.open_sftp() + sftp.put(local_script_path, remote_script_path) + sftp.close() + stdin, stdout, stderr = ssh.exec_command(f"python3 {remote_script_path}") + output = stdout.read().decode() + return(output) + + finally: + ssh.close() + +def get_full(node): + remote_info = get_remote(node, ssh_conf.port, ssh_conf.username, ssh_conf.password, paths.agent_path, paths.remote_path) + full = eval(remote_info) + return full + +@app.route("/", methods = ['GET']) +def get_info(): + node = 'localhost' + full = get_full(node) + return render_template('index.html', full = full, client = client, titre = flask_conf.title) + +@app.route("/n/", methods = ['POST', 'GET']) +def get_node_info(node): + full = get_full(node) + return render_template('index.html', full = full, client = client, titre = flask_conf.title) + + + + +if __name__ == "__main__": + app.run(host=flask_conf.host, port=flask_conf.port, use_reloader=flask_conf.reloader, threaded=flask_conf.thread, debug=flask_conf.debug, ssl_context=flask_conf.ssl) diff --git a/config.py b/config.py new file mode 100644 index 0000000..d305e5a --- /dev/null +++ b/config.py @@ -0,0 +1,27 @@ +import os + +#Client configuration +client = ['client 1','client 2'] + +#SSH configuration +class ssh_conf: + port = 22 + username = '' + password ='//.ssh/id_rsa.pub' + +#Flask configuration +class flask_conf: + port = 8090 + host = "0.0.0.0" + thread = False + debug = False + reloader = False + ssl = 'adhoc' + title = 'Pymonit' + +#Path configuration +class paths : + basedir = os.path.abspath(os.path.dirname(__file__)) + agent_path = basedir+'/agent.py' + remote_path = '/var/tmp/script.py' + diff --git a/monit.service b/monit.service new file mode 100644 index 0000000..87c9582 --- /dev/null +++ b/monit.service @@ -0,0 +1,9 @@ +[Unit] +Description= Monit service +After=multi-user.target +[Service] +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /application/path/app.py +[Install] +WantedBy=multi-user.target diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..d6f8dc4 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,130 @@ + + + + + + {{ titre }} + + + + + + + + + +
+ + + +
+
+
+
+
+
+Environnement +
+
+
{{ full['hostname'] }}
+
CPU : {{ full['cpu_number'] }} +
vCPU : {{ full['vcpu'] }} +
Memory : {{ full['mem_max']}} +
+
+
+
+
+ Usage +
+
+CPU +
+
{{ full['cpu']['percent'] }}%
+
+ +user : {{ full['cpu']['time_user'] }} / nice : {{ full['cpu']['time_nice'] }} / system : {{ full['cpu']['time_system'] }} / idle : {{ full['cpu']['time_idle'] }} / iowait : {{ full['cpu']['time_iowait'] }} / irq : {{ full['cpu']['time_irq'] }} / softirq : {{ full['cpu']['time_softirq'] }} / steal : {{ full['cpu']['time_steal'] }} / guest : {{ full['cpu']['time_guest'] }} / guest nice : {{ full['cpu']['time_guest_nice'] }} + +
+MEM +
+
{{ full['mem']['percent'] }}%
+
+ +total : {{ full['mem']['total'] }} / available : {{ full['mem']['available'] }} / used : {{ full['mem']['used'] }} / free : {{ full['mem']['free'] }} / active : {{ full['mem']['active'] }} / inactive : {{ full['mem']['inactive'] }} / buffers : {{ full['mem']['buffers'] }} / cached : {{ full['mem']['cached'] }} / shared : {{ full['mem']['shared'] }} / slab : {{ full['mem']['slab'] }} + +
+SWAP +
+
{{ full['swap']['percent'] }}%
+
+ +total : {{ full['swap']['total'] }} / used : {{ full['swap']['used'] }} / free : {{ full['swap']['free'] }} / sin : {{ full['swap']['sin'] }} / sout : {{ full['swap']['sout'] }} + +
+
+
+
+ +
+ +
+
+ Disks +
+
+{% for key in full %} +{% if key.startswith('disk_') %} +{{ full[key]['mountpoint'] }} +
+
{{ full[key]['size_percent'] }}%
+
+ +total : {{ full[key]['size_total'] }} / used : {{ full[key]['size_used'] }} / free : {{ full[key]['size_free'] }} / device : {{ full[key]['device'] }} / fstype : {{ full[key]['fstype'] }} / opt: {{ full[key]['opts'] }} / maxfile : {{ full[key]['maxfile'] }} / maxpath : {{ full[key]['maxpath'] }} + +
+{% endif %} +{% endfor %} +
+
+
+ +
+
+ Network +
+
+ + +{% for key in full %} +{% if key.startswith('net_') %} + +{% endif %} +{% endfor %} +
NameIPv4Netmask v4IPv6Netmask v6
{{ full[key]['name'] }}{{ full[key]['address_v4'] }}{{ full[key]['netmask_v4'] }}{{ full[key]['address_v6'] }}{{ full[key]['netmask_v6'] }}
+
+
+ +