first commit

main
pporcheret 2023-06-30 13:40:48 +02:00
commit f3cb525fed
6 changed files with 485 additions and 0 deletions

146
README.md 100644
View File

@ -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 <home_user>/.ssh/id_rsa.pub <user>@<client>
```
## Configuration on application
You have to report these info on the application :
```bash
class ssh_conf:
port = 22
username = '<user>'
password ='<home_user>/.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 = '<user>'
password ='<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
```

129
agent.py 100755
View File

@ -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)

44
app.py 100755
View File

@ -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/<node>", 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)

27
config.py 100644
View File

@ -0,0 +1,27 @@
import os
#Client configuration
client = ['client 1','client 2']
#SSH configuration
class ssh_conf:
port = 22
username = '<user>'
password ='/<user_home>/.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'

9
monit.service 100644
View File

@ -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

View File

@ -0,0 +1,130 @@
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ titre }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js" integrity="sha384-oBqDVmMz9ATKxIep9tiCxS/Z9fNfEXiDAYTujMAeBAsjFuCZSmKbSSUnQlmh/jp3" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN" crossorigin="anonymous"></script>
</head>
<body>
<center>
<nav class="navbar navbar-expand-lg bg-body-tertiary shadow">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Nodes
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/">127.0.0.1</a></li>
<li><hr class="dropdown-divider"></li>
{% for nav_item in client %}
<li><a class="dropdown-item" href="/n/{{ nav_item }}">{{ nav_item }}</a></li>
{% endfor %}
</ul>
</li>
</ul>
</div>
</div>
</nav>
<br>
<div class="container">
<div class="row">
<div class="col-4">
<div class="card">
<div class="card-header">
Environnement
</div>
<div class="card-body">
<h5 class="display-6">{{ full['hostname'] }}</h5>
<br> CPU : {{ full['cpu_number'] }}
<br> vCPU : {{ full['vcpu'] }}
<br> Memory : {{ full['mem_max']}}
</div></div>
</div>
<div class="col-8">
<div class="card">
<div class="card-header">
Usage
</div>
<div class="card-body">
CPU
<div class="progress">
<div class="progress-bar progress-bar-striped" role="progressbar" style="width: {{ full['cpu']['percent'] }}%;" aria-valuenow={{ full['cpu']['percent'] }} aria-valuemin="0" aria-valuemax="100">{{ full['cpu']['percent'] }}%</div>
</div>
<small class="form-text text-muted">
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'] }}
</small>
<br>
MEM
<div class="progress">
<div class="progress-bar bg-warning progress-bar-striped" role="progressbar" style="width: {{ full['mem']['percent'] }}%;" aria-valuenow={{ full['mem']['percent'] }} aria-valuemin="0" aria-valuemax="100">{{ full['mem']['percent'] }}%</div>
</div>
<small class="form-text text-muted">
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'] }}
</small>
<br>
SWAP
<div class="progress">
<div class="progress-bar bg-danger progress-bar-striped" role="progressbar" style="width: {{ full['swap']['percent'] }}%;" aria-valuenow={{ full['swap']['percent'] }} aria-valuemin="0" aria-valuemax="100">{{ full['swap']['percent'] }}%</div>
</div>
<small class="form-text text-muted">
total : {{ full['swap']['total'] }} / used : {{ full['swap']['used'] }} / free : {{ full['swap']['free'] }} / sin : {{ full['swap']['sin'] }} / sout : {{ full['swap']['sout'] }}
</small>
</div>
</div>
</div>
</div>
<br>
<div class="card">
<div class="card-header">
Disks
</div>
<div class="card-body">
{% for key in full %}
{% if key.startswith('disk_') %}
{{ full[key]['mountpoint'] }}
<div class="progress">
<div class="progress-bar bg-info" role="progressbar" style="width: {{ full[key]['size_percent'] }}%;" aria-valuenow={{ full[key]['size_percent'] }} aria-valuemin="0" aria-valuemax="100">{{ full[key]['size_percent'] }}%</div>
</div>
<small class="form-text text-muted">
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'] }}
</small>
</br>
{% endif %}
{% endfor %}
</div>
</div>
<br>
<div class="card">
<div class="card-header">
Network
</div>
<div class="card-body">
<table class="table">
<tr><td>Name</td><td>IPv4</td><td>Netmask v4</td><td>IPv6</td><td>Netmask v6</td></tr>
{% for key in full %}
{% if key.startswith('net_') %}
<tr><td>{{ full[key]['name'] }}</td><td>{{ full[key]['address_v4'] }}</td><td>{{ full[key]['netmask_v4'] }}</td><td>{{ full[key]['address_v6'] }}</td><td>{{ full[key]['netmask_v6'] }}</td><tr>
{% endif %}
{% endfor %}
</table>
</div>
</div>
</body>
</html>