forked from hornet/lainmonitor
Compare commits
20 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8515e9c4a3 | ||
|
|
b09798cada | ||
|
|
e52703fe44 | ||
|
|
1b52ceb881 | ||
|
|
88b678da3b | ||
| 93fb4f93d0 | |||
| b6792931dc | |||
|
|
e68739966a | ||
|
|
0afffee697 | ||
|
|
f28f291a49 | ||
|
|
7e3acb68ce | ||
|
|
29cc8aedd4 | ||
|
|
c0d02bb69a | ||
|
|
f45bffd25e | ||
|
|
5d0ce49d0c | ||
|
|
edb0641889 | ||
|
|
da49859b9d | ||
|
|
a007405b4f | ||
|
|
7d157e5136 | ||
|
|
186134db0c |
7 changed files with 259 additions and 293 deletions
2
.authorized_users
Normal file
2
.authorized_users
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
AUTHORIZED_USER_ID_1
|
||||
AUTHORIZED_USER_ID_2
|
||||
1
.env
Normal file
1
.env
Normal file
|
|
@ -0,0 +1 @@
|
|||
YOUR_TOKEN_HERE
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
test.py
|
||||
venv/
|
||||
142
README.md
142
README.md
|
|
@ -1,105 +1,99 @@
|
|||
# LainMonitor
|
||||
# lainmonitor
|
||||
|
||||
LainMonitor is a Telegram bot designed to provide real‑time monitoring of both the local system and remote network clients (OPNsense firewalls and generic SSH hosts). It aggregates key metrics via SSH and REST APIs, and delivers concise reports through Telegram commands.
|
||||
LainMonitor is a Telegram bot designed to monitor your system, providing real-time updates on the system’s status, essential services, and disk usage. It can also verify connectivity to a specific Tailscale IP address.
|
||||
Current version: v1.2
|
||||
|
||||
## Features
|
||||
### Key Features:
|
||||
|
||||
* **Local System Monitoring**
|
||||
Retrieve system information:
|
||||
Hostname
|
||||
Uptime
|
||||
Status of critical services:
|
||||
Zerotier
|
||||
Prosody
|
||||
PostgreSQL
|
||||
Tailscale
|
||||
nginx
|
||||
Check disk usage
|
||||
Ping a Tailscale IP for connectivity verification
|
||||
Restart critical services
|
||||
Reboot the host
|
||||
Accessible via Telegram commands
|
||||
|
||||
* Hostname and overall online/offline status
|
||||
* Uptime (human‑readable)
|
||||
* Load averages (1, 5, 15 minute)
|
||||
* Memory usage (via `free -h`)
|
||||
* Disk usage (via `df -h`)
|
||||
### Prerequisites:
|
||||
|
||||
* **Remote Client Monitoring**
|
||||
Python 3
|
||||
Telebot — Python library for interacting with the Telegram bot API.
|
||||
|
||||
* **OPNsense Firewalls** (multiple hosts with per‑host trust‑on‑first‑use SSL)
|
||||
### Installation Guide:
|
||||
|
||||
* System health status
|
||||
* Uptime
|
||||
* Memory and disk statistics
|
||||
* Load averages
|
||||
* **Generic SSH Hosts**
|
||||
Clone the repository:
|
||||
|
||||
* Hostname, uptime, load, memory, and disk via SSH
|
||||
git clone https://git.lainlounge.xyz/hornet/lainmonitor.git
|
||||
cd lainmonitor
|
||||
|
||||
* **Security & Resilience**
|
||||
RECOMMENDED: Create a virtual environment for python with:
|
||||
```
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
Install dependencies:
|
||||
|
||||
* Trust‑on‑first‑use SSL: automatically fetches and caches firewall certificates
|
||||
* Concurrency: parallel polling of remote hosts with `ThreadPoolExecutor`
|
||||
* Error handling: per‑task exceptions are logged and do not interrupt overall data gathering
|
||||
* Access control: only whitelisted Telegram chat IDs can invoke commands
|
||||
* Automatic bot restart on failure with backoff retry loop
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
## Commands
|
||||
Configure your bot token: Open the .env file and replace the placeholder with your Telegram bot token.
|
||||
|
||||
* `/status` or `/ping` — Returns a combined report of local and remote metrics
|
||||
Configure authorized users: Open the .authorized_users file and replace the placeholders with Telegram user ID(s).
|
||||
|
||||
## Installation
|
||||
Set up service access: Ensure the bot can check system services by running it with sudo or appropriate permissions.
|
||||
|
||||
1. **Clone repository**
|
||||
### Usage:
|
||||
#### Running the Bot Manually:
|
||||
|
||||
```bash
|
||||
git clone https://git.lainlounge.xyz/hornet/lainmonitor.git
|
||||
cd lainmonitor
|
||||
```
|
||||
2. **Install dependencies**
|
||||
You can run LainMonitor directly from the command line:
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. **Configure**
|
||||
python3 lainmonitor.py
|
||||
|
||||
* Copy `config.py.example` to `config.py`
|
||||
* Populate `config.TOKEN` with your Telegram bot token
|
||||
* Add your Telegram chat IDs to `config.ALLOWED_CHATS`
|
||||
* Define each host under `config.HOSTS` with correct credentials and API settings
|
||||
4. **Prepare SSL directory** (created automatically at runtime):
|
||||
#### Running as a Systemd Service:
|
||||
|
||||
```bash
|
||||
mkdir certs
|
||||
```
|
||||
To run the bot as a systemd service, follow these steps:
|
||||
|
||||
## Usage
|
||||
Create a service file:
|
||||
|
||||
* **Run directly**
|
||||
sudo nano /etc/systemd/system/lainmonitor.service
|
||||
|
||||
```bash
|
||||
python3 lainmonitor.py
|
||||
```
|
||||
Add the following configuration:
|
||||
|
||||
* **Run as a service** (systemd)
|
||||
[Unit]
|
||||
Description=LainMonitor Telegram Bot
|
||||
After=network.target
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=LainMonitor Telegram Bot
|
||||
After=network.target
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /path/to/lainmonitor.py
|
||||
Restart=on-failure
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /path/to/lainmonitor.py
|
||||
Restart=on-failure
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
Enable and start the service:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable lainmonitor
|
||||
sudo systemctl start lainmonitor
|
||||
```
|
||||
sudo systemctl enable lainmonitor
|
||||
sudo systemctl start lainmonitor
|
||||
|
||||
## Dependencies
|
||||
### Available Commands:
|
||||
|
||||
* `pyTelegramBotAPI` (Telebot) — Telegram Bot API client
|
||||
* `paramiko` — SSH connectivity
|
||||
* `requests` — HTTP/REST API client
|
||||
/start — Initialize the bot and receive a welcome message.
|
||||
/help — Display a list of available commands.
|
||||
/status — Retrieve system hostname, uptime, and status of monitored services.
|
||||
/ping — Ping a Tailscale IP and return connectivity status.
|
||||
/restart hostname- Restart a specific service on a specified machine.
|
||||
/reboot hostname — Placeholder for a system reboot command.
|
||||
|
||||
## Author
|
||||
### Contributions:
|
||||
|
||||
**h@x**
|
||||
Created by hornetmaidan.
|
||||
With Contributions from h@x.
|
||||
|
||||
## Original Script written by:
|
||||
**hornetmaidan**
|
||||
|
||||
Contributions and feedback are welcome! :-)
|
||||
Any new features and suggestions are welcome!
|
||||
40
config.py
40
config.py
|
|
@ -1,40 +0,0 @@
|
|||
# Configuration for Lainmonitor
|
||||
|
||||
# Telegram bot token
|
||||
TOKEN = 'PLACE_YOUR_TOKEN_HERE'
|
||||
|
||||
# Allowed Telegram chat IDs (whitelist)
|
||||
ALLOWED_CHATS = [123456789, 987654321]
|
||||
|
||||
# Per-host configuration
|
||||
HOSTS = {
|
||||
'10.0.0.1': {
|
||||
'type': 'opnsense',
|
||||
'api_url': 'https://10.0.0.1/api',
|
||||
'api_key': 'OPN_KEY_1',
|
||||
'api_secret': 'OPN_SECRET_1'
|
||||
},
|
||||
'10.128.0.1': {
|
||||
'type': 'opnsense',
|
||||
'api_url': 'https://10.128.0.1/api',
|
||||
'api_key': 'OPN_KEY_2',
|
||||
'api_secret': 'OPN_SECRET_2'
|
||||
},
|
||||
'10.144.0.1': {
|
||||
'type': 'opnsense',
|
||||
'api_url': 'https://10.144.0.1/api',
|
||||
'api_key': 'OPN_KEY_3',
|
||||
'api_secret': 'OPN_SECRET_3'
|
||||
},
|
||||
'10.130.1.1': {
|
||||
'type': 'opnsense',
|
||||
'api_url': 'https://10.130.1.1/api',
|
||||
'api_key': 'OPN_KEY_4',
|
||||
'api_secret': 'OPN_SECRET_4'
|
||||
},
|
||||
'10.177.0.100': {
|
||||
'type': 'generic',
|
||||
'ssh_user': 'SSH_USER_100',
|
||||
'ssh_pass': 'SSH_PASS_100'
|
||||
}
|
||||
}
|
||||
363
lainmonitor.py
363
lainmonitor.py
|
|
@ -1,188 +1,197 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Description: A Telegram bot for monitoring critical infrastructur services
|
||||
# Dependencies: telebot
|
||||
# Usage: python3 lainmonitor.py | or run it as a service
|
||||
# Author: h@x
|
||||
# Version: 2.1.0
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
import subprocess
|
||||
import telebot
|
||||
import paramiko
|
||||
import requests
|
||||
import time
|
||||
import socket
|
||||
import logging
|
||||
import ssl
|
||||
# --/usr/bin/env python3 -- #
|
||||
# description: telegram bot for monitoring the system
|
||||
# dependencies: telebot
|
||||
# usage: python3 lainmonitor.py | or run it as a service
|
||||
# author: hornetmaidan
|
||||
# contributors: h@x
|
||||
# version: 1.2
|
||||
import os
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from telebot import types
|
||||
import config
|
||||
import subprocess
|
||||
import threading
|
||||
import queue
|
||||
from time import sleep
|
||||
import telebot
|
||||
import logging
|
||||
|
||||
# Configure logging
|
||||
tlogging_format = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
logging.basicConfig(level=logging.INFO, format=tlogging_format)
|
||||
logger = logging.getLogger(__name__)
|
||||
# Setup logging
|
||||
logging.basicConfig(filename='lainmonitor.log', level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Ensure certificate directory exists
|
||||
CERT_DIR = os.path.join(os.path.dirname(__file__), 'certs')
|
||||
if not os.path.isdir(CERT_DIR):
|
||||
os.makedirs(CERT_DIR, exist_ok=True)
|
||||
# Load environment variables and config files securely
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
env_path = os.path.join(script_dir, '.env')
|
||||
auth_users_path = os.path.join(script_dir, '.authorized_users')
|
||||
|
||||
bot = telebot.TeleBot(config.TOKEN)
|
||||
ALLOWED_CHATS = set(config.ALLOWED_CHATS)
|
||||
# Load the token
|
||||
try:
|
||||
with open(env_path, 'r') as f:
|
||||
token = f.read().strip()
|
||||
except FileNotFoundError:
|
||||
logging.error('Token file not found. Exiting...')
|
||||
exit(1)
|
||||
|
||||
# Utility for command execution with timeout
|
||||
def run_cmd(cmd, timeout=5):
|
||||
# Load the authorized users
|
||||
try:
|
||||
authorized_users = [str(line.strip()) for line in open(auth_users_path, 'r').readlines()]
|
||||
except FileNotFoundError:
|
||||
logging.error('Authorized users file not found. Exiting...')
|
||||
exit(1)
|
||||
|
||||
# Initialize the bot
|
||||
bot = telebot.TeleBot(token)
|
||||
|
||||
# Define status variables
|
||||
status, hostname, uptime = 'unknown', 'unknown', 'unknown'
|
||||
zerotier, prosody, postgres, tailscale, nginx, disk = ['unknown'] * 6
|
||||
nodes, hostnames, threads = [], [], []
|
||||
reach_queue = queue.Queue()
|
||||
|
||||
# Get basic system info
|
||||
def get_system_info():
|
||||
global hostname, uptime, zerotier, prosody, postgres, tailscale, nginx, disk
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
return result.stdout.strip()
|
||||
except subprocess.TimeoutExpired as e:
|
||||
logger.warning(f"Command {cmd} timed out: {e}")
|
||||
return 'timeout'
|
||||
except OSError as e:
|
||||
logger.error(f"OS error running {cmd}: {e}")
|
||||
return 'error'
|
||||
hostname = subprocess.check_output(['hostname']).decode().strip()
|
||||
uptime = subprocess.check_output(['uptime', '-p']).decode().strip()
|
||||
|
||||
# Local system info
|
||||
def get_local_info():
|
||||
hostname = run_cmd(['hostname'])
|
||||
uptime = run_cmd(['uptime', '-p'])
|
||||
load_line = run_cmd(['uptime'])
|
||||
load_avg = load_line.split('load average:')[-1].strip() if 'load average:' in load_line else 'unknown'
|
||||
memory = run_cmd(['free', '-h'])
|
||||
disk = run_cmd(['df', '-h'])
|
||||
status = 'online' if hostname not in ('', 'error', 'timeout') else 'offline'
|
||||
return {'hostname': hostname, 'uptime': uptime, 'load_avg': load_avg, 'memory': memory, 'disk': disk, 'status': status}
|
||||
services = ['zerotier-one', 'prosody', 'postgresql', 'tailscaled', 'nginx']
|
||||
status_results = []
|
||||
for service in services:
|
||||
status_results.append(get_service_status(service))
|
||||
zerotier, prosody, postgres, tailscale, nginx = status_results
|
||||
|
||||
# Fetch and store SSL certificate once
|
||||
def fetch_certificate(host, port):
|
||||
cert_path = os.path.join(CERT_DIR, f"{host}.pem")
|
||||
if os.path.isfile(cert_path):
|
||||
return cert_path
|
||||
try:
|
||||
cert = ssl.get_server_certificate((host, port))
|
||||
with open(cert_path, 'w') as f:
|
||||
f.write(cert)
|
||||
logger.info(f"Saved certificate for {host} to {cert_path}")
|
||||
return cert_path
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch certificate for {host}: {e}")
|
||||
return True
|
||||
|
||||
# SSH-based info gathering
|
||||
def get_ssh_info(ip, cfg):
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(ip, username=cfg['ssh_user'], password=cfg['ssh_pass'], timeout=5)
|
||||
info = {}
|
||||
cmds = {'hostname': 'hostname', 'uptime': 'uptime -p', 'load_avg': 'uptime', 'memory': 'free -h', 'disk': 'df -h'}
|
||||
for key, cmd in cmds.items():
|
||||
try:
|
||||
stdin, stdout, stderr = client.exec_command(cmd, timeout=5)
|
||||
out = stdout.read().decode().strip()
|
||||
if key == 'load_avg' and 'load average:' in out:
|
||||
out = out.split('load average:')[-1].strip()
|
||||
info[key] = out
|
||||
except (socket.timeout, paramiko.SSHException) as e:
|
||||
logger.error(f"SSH command {cmd} on {ip} failed: {e}")
|
||||
info[key] = 'error'
|
||||
info['status'] = 'online'
|
||||
except (paramiko.AuthenticationException, paramiko.SSHException, socket.timeout) as e:
|
||||
logger.error(f"SSH connection to {ip} failed: {e}")
|
||||
info = {'status': 'unreachable'}
|
||||
finally:
|
||||
try: client.close()
|
||||
except Exception as e: logger.warning(f"Error closing SSH to {ip}: {e}")
|
||||
return ip, info
|
||||
|
||||
# OPNsense API-based info gathering
|
||||
def get_opnsense_info(ip, cfg):
|
||||
url = cfg['api_url']
|
||||
host = url.split('//')[1].split('/')[0].split(':')[0]
|
||||
port = int(url.split('//')[1].split('/')[0].split(':')[1]) if ':' in url.split('//')[1].split('/')[0] else 443
|
||||
verify = fetch_certificate(host, port)
|
||||
try:
|
||||
resp = requests.get(f"{url}/core/get/health", auth=(cfg['api_key'], cfg['api_secret']), verify=verify, timeout=5)
|
||||
resp.raise_for_status()
|
||||
data = resp.json().get('health', {})
|
||||
return ip, {'status': data.get('health','unknown'), 'uptime': data.get('uptime','unknown'), 'memory': f"{data.get('mem_used','?')}MB/{data.get('mem_total','?')}MB", 'load_avg': data.get('load_avg','unknown'), 'disk': f"{data.get('disk_used','?')}%/{data.get('disk_total','?')}%"}
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"OPNsense API call for {ip} failed: {e}")
|
||||
return ip, {'status': 'unreachable'}
|
||||
|
||||
# Gather info for given host or all hosts
|
||||
def gather_host(ip=None):
|
||||
if ip and ip in config.HOSTS:
|
||||
cfg = config.HOSTS[ip]
|
||||
return [get_ssh_info(ip, cfg) if cfg['type']=='generic' else get_opnsense_info(ip, cfg)]
|
||||
# all hosts
|
||||
return gather_clients()
|
||||
|
||||
# Ping utility
|
||||
def ping_ip(ip):
|
||||
res = run_cmd(['ping', '-c', '1', ip], timeout=3)
|
||||
if '1 packets transmitted, 1 received' in res or '1 packets transmitted, 1 packets received' in res:
|
||||
return 'reachable'
|
||||
if res in ('timeout', 'error'):
|
||||
return res
|
||||
return 'unreachable'
|
||||
|
||||
# Access control decorator
|
||||
def restricted(func):
|
||||
def wrapper(msg, *args, **kwargs):
|
||||
if msg.chat.id not in ALLOWED_CHATS:
|
||||
bot.reply_to(msg, 'Unauthorized access')
|
||||
return
|
||||
return func(msg, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
# /status: show menu of available hosts
|
||||
@bot.message_handler(commands=['status'])
|
||||
@restricted
|
||||
def handle_status(msg):
|
||||
keyboard = types.InlineKeyboardMarkup()
|
||||
for ip in config.HOSTS.keys():
|
||||
keyboard.add(types.InlineKeyboardButton(ip, callback_data=f'status:{ip}'))
|
||||
keyboard.add(types.InlineKeyboardButton('All', callback_data='status:all'))
|
||||
bot.send_message(msg.chat.id, 'Select host for status:', reply_markup=keyboard)
|
||||
|
||||
# Callback handler for inline menu
|
||||
@bot.callback_query_handler(func=lambda c: c.data.startswith('status:'))
|
||||
@restricted
|
||||
def callback_status(call):
|
||||
_, key = call.data.split(':', 1)
|
||||
if key == 'all':
|
||||
entries = gather_clients()
|
||||
disk = subprocess.check_output(['df', '-h']).decode().strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Error fetching system info: {e}")
|
||||
status = 'offline'
|
||||
else:
|
||||
entries = dict(gather_host(key))
|
||||
lines = []
|
||||
for ip, info in entries.items():
|
||||
lines.append(f"{ip}: {info.get('status','unknown')}")
|
||||
if info.get('status')=='online':
|
||||
for field in ('uptime','load_avg','memory','disk'):
|
||||
lines.append(f" {field}: {info.get(field,'-')}")
|
||||
bot.send_message(call.message.chat.id, '\n'.join(lines))
|
||||
status = 'online'
|
||||
|
||||
# /ping <IP>
|
||||
@bot.message_handler(func=lambda m: m.text and m.text.startswith('/ping'))
|
||||
@restricted
|
||||
def handle_ping(msg):
|
||||
parts = msg.text.split()
|
||||
if len(parts) != 2:
|
||||
bot.reply_to(msg, 'Usage: /ping <IP>')
|
||||
return
|
||||
ip = parts[1]
|
||||
status = ping_ip(ip)
|
||||
bot.reply_to(msg, f"Ping {ip}: {status}")
|
||||
|
||||
# Run polling with retry
|
||||
while True:
|
||||
# Helper function to get service status
|
||||
def get_service_status(service):
|
||||
try:
|
||||
bot.polling()
|
||||
except Exception as e:
|
||||
logger.error(f"Polling error: {e}")
|
||||
time.sleep(5)
|
||||
subprocess.run(['sudo', 'systemctl', 'is-active', '--quiet', service], check=True)
|
||||
return f'{service} is active'
|
||||
except subprocess.CalledProcessError:
|
||||
return f'{service} is inactive/not present'
|
||||
|
||||
# Function to ping a Tailscale node
|
||||
def ping_node(node, hostname):
|
||||
try:
|
||||
ping = subprocess.run(['ping', '-c', '1', node], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
reach_queue.put(f'{node}/{hostname} is reachable')
|
||||
except subprocess.CalledProcessError:
|
||||
reach_queue.put(f'{node}/{hostname} is unreachable')
|
||||
|
||||
# Check Tailscale nodes
|
||||
def check_tailscale_nodes():
|
||||
global nodes, hostnames, threads
|
||||
try:
|
||||
nodes_output = subprocess.check_output("tailscale status | grep '100'", shell=True).decode().strip()
|
||||
nodes = [line.split()[0] for line in nodes_output.split('\n') if line]
|
||||
hostnames = [line.split()[1] for line in nodes_output.split('\n') if line]
|
||||
|
||||
for node, hostname in zip(nodes, hostnames):
|
||||
thread = threading.Thread(target=ping_node, args=(node, hostname))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
reach = []
|
||||
while not reach_queue.empty():
|
||||
reach.append(reach_queue.get())
|
||||
|
||||
return reach
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Error checking Tailscale status: {e}")
|
||||
return ['Error checking Tailscale status']
|
||||
|
||||
# Function to restart a service
|
||||
def restart_service(service):
|
||||
logging.info(f'Restarting {service}...')
|
||||
try:
|
||||
subprocess.run(['sudo', 'systemctl', 'restart', service], check=True)
|
||||
sleep(3)
|
||||
service_status = get_service_status(service)
|
||||
status_message = f'{service} restarted! Status: {service_status}'
|
||||
logging.info(status_message)
|
||||
return status_message
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Error restarting {service}: {e}")
|
||||
return f'Error restarting {service}'
|
||||
|
||||
# Restart services menu
|
||||
def restart_menu():
|
||||
keyboard = [
|
||||
[telebot.types.InlineKeyboardButton('zerotier-one', callback_data='zerotier-one')],
|
||||
[telebot.types.InlineKeyboardButton('prosody', callback_data='prosody')],
|
||||
[telebot.types.InlineKeyboardButton('postgresql', callback_data='postgresql')],
|
||||
[telebot.types.InlineKeyboardButton('tailscaled', callback_data='tailscaled')],
|
||||
[telebot.types.InlineKeyboardButton('nginx', callback_data='nginx')],
|
||||
[telebot.types.InlineKeyboardButton('cancel', callback_data='cancel')]
|
||||
]
|
||||
reply_markup = telebot.types.InlineKeyboardMarkup(keyboard)
|
||||
return reply_markup
|
||||
|
||||
# Callback query handler for service restart
|
||||
@bot.callback_query_handler(func=lambda call: True)
|
||||
def callback_query(call):
|
||||
service = call.data
|
||||
if service != 'cancel':
|
||||
status_message = restart_service(service)
|
||||
bot.send_message(call.message.chat.id, status_message)
|
||||
else:
|
||||
bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=None)
|
||||
bot.send_message(call.message.chat.id, 'Canceled')
|
||||
|
||||
# Reboot system function
|
||||
def reboot():
|
||||
logging.info('Rebooting system...')
|
||||
subprocess.run(['sudo', 'reboot'], check=True)
|
||||
|
||||
# Populate teh variables on first start
|
||||
get_system_info()
|
||||
|
||||
# Message handlers
|
||||
@bot.message_handler(commands=['start', 'help', 'status', 'restart', 'reboot', 'ping'])
|
||||
def handle(message):
|
||||
user_id = str(message.from_user.id)
|
||||
if user_id not in authorized_users:
|
||||
bot.reply_to(message, 'You are not authorized for this action')
|
||||
else:
|
||||
if message.text == '/start':
|
||||
bot.reply_to(message, 'lainmonitor v1.2 --- standing by...')
|
||||
elif message.text == '/help':
|
||||
bot.reply_to(message, 'commands: /start, /help, /status, /restart, /reboot, /ping')
|
||||
bot.reply_to(message, 'commands: /start, /help, /status, /restart, /reboot, /ping')
|
||||
elif message.text == '/status':
|
||||
get_system_info()
|
||||
status_message = (
|
||||
f'hostname: {hostname}\n'
|
||||
f'system status: {status}\n'
|
||||
f'uptime: {uptime}\n'
|
||||
f'zerotier: {zerotier}\n'
|
||||
f'prosody: {prosody}\n'
|
||||
f'postgres: {postgres}\n'
|
||||
f'tailscale: {tailscale}\n'
|
||||
f'nginx: {nginx}'
|
||||
)
|
||||
bot.reply_to(message, status_message)
|
||||
bot.reply_to(message, f'Filesystem info for {hostname}:\n\n{disk}')
|
||||
elif message.text == f'/restart {hostname}':
|
||||
bot.send_message(message.chat.id, 'Select a service to restart:', reply_markup=restart_menu())
|
||||
elif message.text == f'/reboot {hostname}':
|
||||
bot.reply_to(message, f'Rebooting {hostname}...')
|
||||
reboot()
|
||||
elif message.text == '/ping':
|
||||
reach = check_tailscale_nodes()
|
||||
bot.reply_to(message, f'Ping status:\n\n{"\n".join(reach)}')
|
||||
else:
|
||||
pass
|
||||
# Polling with timeout and error handling
|
||||
try:
|
||||
bot.polling(none_stop=True, timeout=60, long_polling_timeout=60)
|
||||
except Exception as e:
|
||||
logging.error(f'Polling error: {e}')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1 @@
|
|||
telebot
|
||||
paramiko
|
||||
requests
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue