Compare commits

...

8 commits

Author SHA1 Message Date
hornet
b09798cada updoot readme and version labels in the main file 2024-12-27 21:46:34 +05:00
hornet
e52703fe44 fixed authorization issue 2024-12-27 21:34:32 +05:00
hornet
1b52ceb881 Merge branch 'hax-dev' into dev 2024-10-25 18:20:39 +05:00
hornet
88b678da3b Merge branch 'hax-readme_updoot' into dev 2024-10-25 18:00:55 +05:00
hax
93fb4f93d0 Updoot README V1.1.0 :) 2024-10-25 00:43:54 +00:00
hax
b6792931dc Updoot secure system monitor with service management
Key Improvements:

Error Handling:
Used try-except blocks to catch errors from subprocesses and file operations, logging issues.

Thread Safety:
Introduced queue.Queue for thread-safe operations when handling ping results.

Subprocess Optimizations:
Used subprocess.run() for cleaner, more modern handling of subprocesses.
Avoided shell=True for security reasons unless absolutely necessary.

Service Management:
Improved service status checking by using systemctl is-active and using exit statuses for reliability.

User Authorization:
Checked user authorization in relevant commands like /restart, /reboot, and /ping.

Logging:
Introduced logging for all major operations to track activity and errors.

Polling Timeout:
Added timeouts and error handling to prevent the bot from hanging during long polling.

This updoot is bring more security, robustness, and scalability, ready to handle various edge cases that might occur in our system monitoring.

Signed-off-by: hax <hax@lainlounge.xyz>
2024-10-25 00:29:37 +00:00
hornet
e68739966a finished /reboot host and /restart host functions, added authorization for actions by telegram ID, added nginx to default services 2024-10-18 18:06:53 +05:00
hornet
0afffee697 new functions(WIP) - /reboot and /restart service 2024-10-17 15:51:21 +05:00
3 changed files with 235 additions and 122 deletions

2
.authorized_users Normal file
View file

@ -0,0 +1,2 @@
AUTHORIZED_USER_ID_1
AUTHORIZED_USER_ID_2

109
README.md
View file

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

View file

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