Compare commits
8 commits
f28f291a49
...
b09798cada
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b09798cada | ||
|
|
e52703fe44 | ||
|
|
1b52ceb881 | ||
|
|
88b678da3b | ||
| 93fb4f93d0 | |||
| b6792931dc | |||
|
|
e68739966a | ||
|
|
0afffee697 |
3 changed files with 235 additions and 122 deletions
2
.authorized_users
Normal file
2
.authorized_users
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
AUTHORIZED_USER_ID_1
|
||||||
|
AUTHORIZED_USER_ID_2
|
||||||
109
README.md
109
README.md
|
|
@ -1,53 +1,61 @@
|
||||||
# lainmonitor
|
# lainmonitor
|
||||||
|
|
||||||
lainmonitor is a Telegram bot designed to monitor your system by providing real-time information about the system's status, services, and disk usage. It can also check connectivity to a specific Tailscale IP address.
|
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:
|
||||||
- Retrieve system hostname, uptime, and status of essential services such as:
|
|
||||||
- Zerotier
|
|
||||||
- Prosody
|
|
||||||
- PostgreSQL
|
|
||||||
- Tailscale
|
|
||||||
- Check disk usage
|
|
||||||
- Ping a Tailscale IP to verify connectivity
|
|
||||||
- Use via Telegram commands like `/status`, `/ping`, and `/help`
|
|
||||||
|
|
||||||
## Dependencies
|
Retrieve system information:
|
||||||
- [Telebot](https://github.com/eternnoir/pyTelegramBotAPI) - A Python library for Telegram bot API.
|
Hostname
|
||||||
|
Uptime
|
||||||
|
Status of critical services:
|
||||||
|
Zerotier
|
||||||
|
Prosody
|
||||||
|
PostgreSQL
|
||||||
|
Tailscale
|
||||||
|
Check disk usage
|
||||||
|
Ping a Tailscale IP for connectivity verification
|
||||||
|
Accessible via Telegram commands such as /status, /ping, and /help
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
|
||||||
|
Python 3
|
||||||
|
Telebot — Python library for interacting with the Telegram bot API.
|
||||||
|
|
||||||
|
### Installation Guide:
|
||||||
|
|
||||||
|
Clone the repository:
|
||||||
|
|
||||||
## Installation
|
|
||||||
1. Clone this repository:
|
|
||||||
```bash
|
|
||||||
git clone https://git.lainlounge.xyz/hornet/lainmonitor.git
|
git clone https://git.lainlounge.xyz/hornet/lainmonitor.git
|
||||||
cd lainmonitor
|
cd lainmonitor
|
||||||
```
|
|
||||||
2. Install the required Python library:
|
|
||||||
```bash
|
|
||||||
pip install pyTelegramBotAPI
|
|
||||||
```
|
|
||||||
3. Replace the placeholder in the .env file with your Telegram bot token
|
|
||||||
|
|
||||||
4. Add the .env file to .gitignore to prevent token overwriting
|
Install dependencies:
|
||||||
|
|
||||||
4. Set up permissions for the bot to check system services (run as a user with `sudo` access).
|
pip3 install pyTelegramBotAPI
|
||||||
|
|
||||||
## Usage
|
Configure your bot token: Open the lainmonitor.py file and replace the placeholder with your Telegram bot token:
|
||||||
|
|
||||||
### Running Directly
|
TOKEN = 'YOUR_BOT_TOKEN'
|
||||||
You can run the bot directly using Python:
|
|
||||||
|
|
||||||
```bash
|
Set up service access: Ensure the bot can check system services by running it with sudo or appropriate permissions.
|
||||||
python3 lainmonitor.py
|
|
||||||
```
|
### Usage:
|
||||||
|
#### Running the Bot Manually:
|
||||||
|
|
||||||
|
You can run LainMonitor directly from the command line:
|
||||||
|
|
||||||
|
python3 lainmonitor.py
|
||||||
|
|
||||||
|
#### Running as a Systemd Service:
|
||||||
|
|
||||||
|
To run the bot as a systemd service, follow these steps:
|
||||||
|
|
||||||
|
Create a service file:
|
||||||
|
|
||||||
### Running as a Service
|
|
||||||
To run LainMonitor as a service, follow these steps:
|
|
||||||
1. Create a systemd service file:
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/systemd/system/lainmonitor.service
|
sudo nano /etc/systemd/system/lainmonitor.service
|
||||||
```
|
|
||||||
2. Add the following configuration:
|
Add the following configuration:
|
||||||
```ini
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=LainMonitor Telegram Bot
|
Description=LainMonitor Telegram Bot
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
@ -58,21 +66,24 @@ To run LainMonitor as a service, follow these steps:
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
```
|
|
||||||
3. Enable and start the service:
|
Enable and start the service:
|
||||||
```bash
|
|
||||||
sudo systemctl enable lainmonitor
|
sudo systemctl enable lainmonitor
|
||||||
sudo systemctl start lainmonitor
|
sudo systemctl start lainmonitor
|
||||||
```
|
|
||||||
|
|
||||||
## Telegram Bot Commands
|
### Available Commands:
|
||||||
- `/start`: Initialize the bot and receive a welcome message.
|
|
||||||
- `/help`: Display available commands.
|
|
||||||
- `/status`: Get the system hostname, status, uptime, and the status of monitored services.
|
|
||||||
- `/ping`: Ping a Tailscale IP subnet and return the connectivity status for each peer.
|
|
||||||
- `/reboot`: (Work in progress) Placeholder for a reboot command.
|
|
||||||
|
|
||||||
## Author
|
/start — Initialize the bot and receive a welcome message.
|
||||||
Created by **hornet** and **hax**
|
/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.
|
||||||
|
|
||||||
Feel free to contribute or suggest features!
|
### Contributions:
|
||||||
|
|
||||||
|
Created by hornetmaidan.
|
||||||
|
With Contributions from h@x.
|
||||||
|
|
||||||
|
Any new features and suggestions are welcome!
|
||||||
246
lainmonitor.py
246
lainmonitor.py
|
|
@ -1,97 +1,197 @@
|
||||||
#description: telegram bot for monitoring the system
|
# --/usr/bin/env python3 -- #
|
||||||
#dependencies: telebot
|
# description: telegram bot for monitoring the system
|
||||||
#usage: python3 lainmonitor.py | or run it as a service
|
# dependencies: telebot
|
||||||
#author: hornetmaidan
|
# usage: python3 lainmonitor.py | or run it as a service
|
||||||
|
# author: hornetmaidan
|
||||||
|
# contributors: h@x
|
||||||
|
# version: 1.2
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
|
import queue
|
||||||
|
from time import sleep
|
||||||
import telebot
|
import telebot
|
||||||
|
import logging
|
||||||
|
|
||||||
#define the variables
|
# Setup logging
|
||||||
status, hostname, uptime, zerotier, prosody, postgres, tailscale, disk, ping = 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', 'unknown'
|
logging.basicConfig(filename='lainmonitor.log', level=logging.INFO,
|
||||||
nodes, hostnames, reach, threads = [], [], [], []
|
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
#load the token
|
# Load environment variables and config files securely
|
||||||
token = open('.env', 'r').read().strip()
|
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 init
|
# 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)
|
||||||
|
|
||||||
|
# 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)
|
bot = telebot.TeleBot(token)
|
||||||
|
|
||||||
#get system info
|
# Define status variables
|
||||||
def getinfo():
|
status, hostname, uptime = 'unknown', 'unknown', 'unknown'
|
||||||
global status, hostname, uptime, zerotier, prosody, postgres, tailscale, disk
|
zerotier, prosody, postgres, tailscale, nginx, disk = ['unknown'] * 6
|
||||||
hostname = subprocess.check_output(['hostname']).decode().strip()
|
nodes, hostnames, threads = [], [], []
|
||||||
uptime = subprocess.check_output(['uptime', '-p']).decode().strip()
|
reach_queue = queue.Queue()
|
||||||
#systemd-only services
|
|
||||||
zerotier = subprocess.Popen("sudo systemctl status zerotier-one | grep 'Active'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
# Get basic system info
|
||||||
prosody = subprocess.Popen("sudo systemctl status prosody | grep 'Active'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
def get_system_info():
|
||||||
postgres = subprocess.Popen("sudo systemctl status postgresql | grep 'Active'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
global hostname, uptime, zerotier, prosody, postgres, tailscale, nginx, disk
|
||||||
tailscale = subprocess.Popen("sudo systemctl status tailscaled | grep 'Active'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
try:
|
||||||
disk = subprocess.check_output(['df', '-h']).decode().strip()
|
hostname = subprocess.check_output(['hostname']).decode().strip()
|
||||||
if hostname == 'unknown':
|
uptime = subprocess.check_output(['uptime', '-p']).decode().strip()
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
disk = subprocess.check_output(['df', '-h']).decode().strip()
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
logging.error(f"Error fetching system info: {e}")
|
||||||
status = 'offline'
|
status = 'offline'
|
||||||
else:
|
else:
|
||||||
status = 'online'
|
status = 'online'
|
||||||
return hostname, uptime, zerotier, prosody, postgres, tailscale, disk
|
|
||||||
|
|
||||||
#function to ping tailscale nodes
|
# Helper function to get service status
|
||||||
|
def get_service_status(service):
|
||||||
|
try:
|
||||||
|
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):
|
def ping_node(node, hostname):
|
||||||
ping = subprocess.Popen(f"ping {node} -c 1 | grep '1 packets'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
try:
|
||||||
if '1 received' in ping:
|
ping = subprocess.run(['ping', '-c', '1', node], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||||
reach.append(f'{node}/{hostname} is reachable')
|
reach_queue.put(f'{node}/{hostname} is reachable')
|
||||||
else:
|
except subprocess.CalledProcessError:
|
||||||
reach.append(f'{node}/{hostname} is unreachable')
|
reach_queue.put(f'{node}/{hostname} is unreachable')
|
||||||
|
|
||||||
#ping tailscale nodes
|
# Check Tailscale nodes
|
||||||
def check_tailscale():
|
def check_tailscale_nodes():
|
||||||
global nodes, hostnames, reach, threads, ping
|
global nodes, hostnames, threads
|
||||||
nodes_output = subprocess.Popen("tailscale status | grep '100'", shell=True, stdout=subprocess.PIPE).stdout.read().decode().strip()
|
try:
|
||||||
nodes = [line.split()[0] for line in nodes_output.split('\n') if line]
|
nodes_output = subprocess.check_output("tailscale status | grep '100'", shell=True).decode().strip()
|
||||||
hostnames = [line.split()[1] for line in nodes_output.split('\n') if line]
|
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):
|
for node, hostname in zip(nodes, hostnames):
|
||||||
thread = threading.Thread(target=ping_node, args=(node, hostname))
|
thread = threading.Thread(target=ping_node, args=(node, hostname))
|
||||||
threads.append(thread)
|
threads.append(thread)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
for thread in threads:
|
for thread in threads:
|
||||||
thread.join()
|
thread.join()
|
||||||
|
|
||||||
return reach
|
reach = []
|
||||||
|
while not reach_queue.empty():
|
||||||
|
reach.append(reach_queue.get())
|
||||||
|
|
||||||
#debug handler
|
return reach
|
||||||
def check():
|
except subprocess.CalledProcessError as e:
|
||||||
global status, hostname, uptime, zerotier, prosody, postgres, tailscale, disk
|
logging.error(f"Error checking Tailscale status: {e}")
|
||||||
getinfo()
|
return ['Error checking Tailscale status']
|
||||||
print('system status:', status)
|
|
||||||
print('hostname:', hostname)
|
|
||||||
print('uptime:', uptime)
|
|
||||||
print('zerotier:', zerotier)
|
|
||||||
print('prosody:', prosody)
|
|
||||||
print('postgres:', postgres)
|
|
||||||
print('tailscale:', tailscale)
|
|
||||||
print('disk:', disk)
|
|
||||||
return status, hostname, uptime, zerotier, prosody, postgres, tailscale, disk
|
|
||||||
|
|
||||||
#message handling
|
# Function to restart a service
|
||||||
@bot.message_handler(commands=['start', 'help', 'status', 'reboot', 'ping'])
|
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):
|
def handle(message):
|
||||||
if message.text == '/start':
|
user_id = str(message.from_user.id)
|
||||||
bot.reply_to(message, 'lainmonitor v1.0 --- standing by...')
|
if user_id not in authorized_users:
|
||||||
elif message.text == '/help':
|
bot.reply_to(message, 'You are not authorized for this action')
|
||||||
bot.reply_to(message, 'commands: /start, /help, /status, /reboot, /ping')
|
else:
|
||||||
elif message.text == '/status':
|
if message.text == '/start':
|
||||||
check()
|
bot.reply_to(message, 'lainmonitor v1.2 --- standing by...')
|
||||||
status_message = f'hostname: {hostname}\nsystem status: {status}\nuptime: {uptime}\nzerotier: {zerotier}\nprosody: {prosody}\npostgres: {postgres}\ntailscale: {tailscale}'
|
elif message.text == '/help':
|
||||||
bot.reply_to(message, status_message)
|
bot.reply_to(message, 'commands: /start, /help, /status, /restart, /reboot, /ping')
|
||||||
bot.reply_to(message, f'filesystem info for {hostname}: \n\n{disk}')
|
bot.reply_to(message, 'commands: /start, /help, /status, /restart, /reboot, /ping')
|
||||||
elif message.text == '/reboot':
|
elif message.text == '/status':
|
||||||
bot.reply_to(message, 'work in progress...')
|
get_system_info()
|
||||||
elif message.text == '/ping':
|
status_message = (
|
||||||
check_tailscale()
|
f'hostname: {hostname}\n'
|
||||||
ping_status = '\n'.join(reach)
|
f'system status: {status}\n'
|
||||||
bot.reply_to(message, f'ping status:\n\n{ping_status}')
|
f'uptime: {uptime}\n'
|
||||||
ping_status = ''
|
f'zerotier: {zerotier}\n'
|
||||||
reach.clear()
|
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}')
|
||||||
|
|
||||||
#polling
|
|
||||||
bot.polling()
|
|
||||||
Loading…
Add table
Reference in a new issue