#!/usr/bin/env python3 """ IPWatch MQTT Agent Agent à installer sur chaque machine pour recevoir les commandes shutdown/reboot via MQTT Installation: pip install paho-mqtt psutil netifaces Configuration: Créer /etc/ipwatch/mqtt-agent.conf avec: [mqtt] broker = localhost port = 1883 username = password = [agent] hostname = auto check_interval = 30 """ import paho.mqtt.client as mqtt import platform import os import sys import subprocess import json import time import socket import configparser import logging from datetime import datetime from pathlib import Path # Configuration du logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/var/log/ipwatch-mqtt-agent.log'), logging.StreamHandler() ] ) logger = logging.getLogger('ipwatch-mqtt-agent') class IPWatchMQTTAgent: """Agent MQTT pour recevoir les commandes de contrôle système""" def __init__(self, config_file='/etc/ipwatch/mqtt-agent.conf'): self.config = self.load_config(config_file) self.hostname = self.get_hostname() self.ip_address = self.get_ip_address() # Topics MQTT self.base_topic = f"ipwatch/device/{self.ip_address}" self.command_topic = f"{self.base_topic}/command" self.status_topic = f"{self.base_topic}/status" self.availability_topic = f"{self.base_topic}/availability" # Client MQTT self.client = mqtt.Client(client_id=f"ipwatch-agent-{self.hostname}") self.client.on_connect = self.on_connect self.client.on_message = self.on_message self.client.on_disconnect = self.on_disconnect # Will message (si l'agent se déconnecte brutalement) self.client.will_set( self.availability_topic, payload="offline", qos=1, retain=True ) def load_config(self, config_file): """Charge la configuration depuis le fichier""" config = configparser.ConfigParser() if not Path(config_file).exists(): logger.warning(f"Fichier de configuration {config_file} introuvable, utilisation des valeurs par défaut") return { 'broker': 'localhost', 'port': 1883, 'username': None, 'password': None, 'check_interval': 30 } config.read(config_file) return { 'broker': config.get('mqtt', 'broker', fallback='localhost'), 'port': config.getint('mqtt', 'port', fallback=1883), 'username': config.get('mqtt', 'username', fallback=None), 'password': config.get('mqtt', 'password', fallback=None), 'check_interval': config.getint('agent', 'check_interval', fallback=30) } def get_hostname(self): """Récupère le hostname de la machine""" return platform.node() def get_ip_address(self): """Récupère l'adresse IP principale de la machine""" try: # Créer une socket pour déterminer l'IP locale s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) ip = s.getsockname()[0] s.close() return ip except Exception as e: logger.error(f"Erreur récupération IP: {e}") return "127.0.0.1" def on_connect(self, client, userdata, flags, rc): """Callback lors de la connexion au broker MQTT""" if rc == 0: logger.info(f"✓ Connecté au broker MQTT {self.config['broker']}:{self.config['port']}") # S'abonner au topic des commandes client.subscribe(self.command_topic) client.subscribe(f"ipwatch/device/+/command") # Pour broadcast logger.info(f"✓ Abonné à {self.command_topic}") # Publier la disponibilité client.publish(self.availability_topic, "online", qos=1, retain=True) # Publier le statut initial self.publish_status() else: logger.error(f"✗ Échec connexion MQTT, code: {rc}") def on_disconnect(self, client, userdata, rc): """Callback lors de la déconnexion""" if rc != 0: logger.warning(f"⚠ Déconnexion inattendue du broker MQTT (code {rc})") def on_message(self, client, userdata, msg): """Callback lors de la réception d'un message""" try: payload = msg.payload.decode('utf-8') logger.info(f"→ Message reçu sur {msg.topic}: {payload}") # Parser le message JSON try: command_data = json.loads(payload) command = command_data.get('command', payload) # Support format simple ou JSON except json.JSONDecodeError: command = payload # Format texte simple # Exécuter la commande self.execute_command(command) except Exception as e: logger.error(f"✗ Erreur traitement message: {e}") def execute_command(self, command): """Exécute une commande système""" logger.info(f"⚙ Exécution commande: {command}") try: if command == "shutdown": self.shutdown() elif command == "reboot": self.reboot() elif command == "status": self.publish_status() else: logger.warning(f"⚠ Commande inconnue: {command}") self.publish_response(f"Commande inconnue: {command}", success=False) except Exception as e: logger.error(f"✗ Erreur exécution commande: {e}") self.publish_response(str(e), success=False) def shutdown(self): """Éteint la machine""" logger.warning("🔴 Shutdown demandé, arrêt dans 5 secondes...") self.publish_response("Shutdown en cours...", success=True) time.sleep(1) # Publier offline avant l'arrêt self.client.publish(self.availability_topic, "offline", qos=1, retain=True) time.sleep(1) # Commande d'arrêt selon l'OS if platform.system() == "Windows": subprocess.run(["shutdown", "/s", "/t", "5"]) else: subprocess.run(["sudo", "shutdown", "-h", "+0"]) def reboot(self): """Redémarre la machine""" logger.warning("🔄 Reboot demandé, redémarrage dans 5 secondes...") self.publish_response("Reboot en cours...", success=True) time.sleep(1) # Publier offline avant le redémarrage self.client.publish(self.availability_topic, "offline", qos=1, retain=True) time.sleep(1) # Commande de redémarrage selon l'OS if platform.system() == "Windows": subprocess.run(["shutdown", "/r", "/t", "5"]) else: subprocess.run(["sudo", "reboot"]) def publish_status(self): """Publie le statut de la machine""" try: import psutil status = { "hostname": self.hostname, "ip": self.ip_address, "platform": platform.system(), "platform_version": platform.version(), "uptime": time.time() - psutil.boot_time(), "cpu_percent": psutil.cpu_percent(interval=1), "memory_percent": psutil.virtual_memory().percent, "disk_percent": psutil.disk_usage('/').percent, "timestamp": datetime.now().isoformat() } self.client.publish( self.status_topic, json.dumps(status), qos=1, retain=False ) logger.info(f"✓ Statut publié") except ImportError: logger.warning("psutil non installé, statut limité") status = { "hostname": self.hostname, "ip": self.ip_address, "timestamp": datetime.now().isoformat() } self.client.publish(self.status_topic, json.dumps(status), qos=1) def publish_response(self, message, success=True): """Publie une réponse sur le topic de statut""" response = { "success": success, "message": message, "timestamp": datetime.now().isoformat() } self.client.publish( f"{self.base_topic}/response", json.dumps(response), qos=1 ) def run(self): """Démarre l'agent""" try: # Authentification si configurée if self.config['username'] and self.config['password']: self.client.username_pw_set( self.config['username'], self.config['password'] ) # Connexion au broker logger.info(f"→ Connexion au broker MQTT {self.config['broker']}:{self.config['port']}...") self.client.connect( self.config['broker'], self.config['port'], keepalive=60 ) # Démarrer la boucle MQTT self.client.loop_start() # Publier le statut périodiquement logger.info(f"✓ Agent IPWatch MQTT démarré sur {self.ip_address}") logger.info(f" Topics: {self.command_topic} | {self.status_topic}") while True: time.sleep(self.config['check_interval']) self.publish_status() except KeyboardInterrupt: logger.info("\n→ Arrêt demandé par l'utilisateur") self.stop() except Exception as e: logger.error(f"✗ Erreur fatale: {e}") self.stop() sys.exit(1) def stop(self): """Arrête l'agent proprement""" logger.info("→ Arrêt de l'agent...") self.client.publish(self.availability_topic, "offline", qos=1, retain=True) self.client.loop_stop() self.client.disconnect() logger.info("✓ Agent arrêté") def main(): """Point d'entrée principal""" import argparse parser = argparse.ArgumentParser(description='IPWatch MQTT Agent') parser.add_argument( '-c', '--config', default='/etc/ipwatch/mqtt-agent.conf', help='Fichier de configuration (défaut: /etc/ipwatch/mqtt-agent.conf)' ) parser.add_argument( '--test', action='store_true', help='Mode test (affiche la config et quitte)' ) args = parser.parse_args() agent = IPWatchMQTTAgent(config_file=args.config) if args.test: print(f"Configuration:") print(f" Broker: {agent.config['broker']}:{agent.config['port']}") print(f" Hostname: {agent.hostname}") print(f" IP: {agent.ip_address}") print(f" Command topic: {agent.command_topic}") print(f" Status topic: {agent.status_topic}") return agent.run() if __name__ == "__main__": main()