first commit
commit
f3cb525fed
|
@ -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
|
||||
```
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
||||
|
|
@ -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
|
|
@ -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>
|
Loading…
Reference in New Issue