#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ GLPI Agent Task: netconfig_backup Recupera configurazione da apparati di rete e invia a GLPI plugin netconfig Requisiti: netmiko, requests, pyyaml Install: pip3 install netmiko requests pyyaml """ import os import sys import json import hashlib import logging import requests from datetime import datetime from typing import Optional, Dict, List from netmiko import ConnectHandler, NetmikoTimeoutException, NetmikoAuthenticationException import yaml # Configurazione logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('/var/log/glpi_agent_netconfig.log'), logging.StreamHandler(sys.stdout) ] ) logger = logging.getLogger('netconfig_backup') # Configurazione da file YAML esterno (sicuro, non hardcodato) CONFIG_FILE = os.getenv('NETCONFIG_CONFIG', '/etc/glpi-agent/netconfig_devices.yaml') GLPI_URL = os.getenv('GLPI_URL', 'http://localhost/glpi') GLPI_TOKEN = os.getenv('GLPI_APP_TOKEN', '') AGENT_TOKEN = os.getenv('NETCONFIG_AGENT_TOKEN', 'change_me_in_production') TIMEOUT = int(os.getenv('NETCONFIG_TIMEOUT', '30')) RETRIES = int(os.getenv('NETCONFIG_RETRIES', '2')) def load_devices_config(path: str) -> List[Dict]: """Carica configurazione dispositivi da YAML""" if not os.path.exists(path): logger.error(f"Config file not found: {path}") return [] try: with open(path, 'r') as f: data = yaml.safe_load(f) return data.get('devices', []) except Exception as e: logger.error(f"Error loading config: {e}") return [] def get_device_connection_params(device: Dict) -> Dict: """Prepara parametri di connessione per netmiko""" device_type_map = { 'cisco_ios': 'cisco_ios', 'cisco_nxos': 'cisco_nxos', 'hp_comware': 'hp_comware', 'juniper_junos': 'juniper_junos', 'arista_eos': 'arista_eos', 'paloalto_panos': 'paloalto_panos', } return { 'device_type': device_type_map.get(device['platform'], 'cisco_ios'), 'host': device['ip'], 'username': device.get('username', os.getenv('NETCONFIG_DEFAULT_USER')), 'password': device.get('password', os.getenv('NETCONFIG_DEFAULT_PASS')), 'secret': device.get('enable_password', os.getenv('NETCONFIG_ENABLE_PASS')), 'port': device.get('port', 22), 'timeout': TIMEOUT, 'session_log': None, 'global_delay_factor': device.get('delay_factor', 1), } def fetch_config(device: Dict) -> Optional[str]: """Recupera configurazione via SSH con netmiko""" params = get_device_connection_params(device) for attempt in range(RETRIES + 1): try: logger.info(f"Connecting to {device['name']} ({params['host']}) [attempt {attempt+1}]") connection = ConnectHandler(**params) # Abilita modalità enable se necessaria if params.get('secret'): connection.enable() # Comando di show config (personalizzabile per piattaforma) command = device.get('command', 'show running-config') config = connection.send_command(command, expect_string=r'#|\$', max_loops=150) connection.disconnect() # Pulizia output da caratteri di controllo config = config.strip() config = '\n'.join(line.rstrip() for line in config.splitlines()) logger.info(f"Config fetched successfully for {device['name']}") return config except NetmikoAuthenticationException as e: logger.error(f"Auth failed for {device['name']}: {e}") break # Non ritentare se auth fallisce except NetmikoTimeoutException as e: logger.warning(f"Timeout for {device['name']}: {e}") except Exception as e: logger.error(f"Unexpected error for {device['name']}: {e}") return None def send_to_glpi(device_id: int, config: str) -> bool: """Invia configurazione a GLPI plugin endpoint""" url = f"{GLPI_URL}/plugins/netconfig/ajax/agent_receive.php" payload = { 'device_id': device_id, 'config': config, 'token': AGENT_TOKEN, 'timestamp': datetime.utcnow().isoformat(), 'agent_version': '1.0.0' } headers = { 'Content-Type': 'application/json', 'App-Token': GLPI_TOKEN, # Opzionale: autenticazione API GLPI } try: response = requests.post( url, json=payload, headers=headers, timeout=30, verify=os.getenv('SSL_VERIFY', 'true').lower() == 'true' ) if response.status_code == 200: result = response.json() logger.info(f"GLPI response: {result.get('message', 'OK')}") return result.get('status') == 'ok' else: logger.error(f"GLPI HTTP {response.status_code}: {response.text}") return False except requests.RequestException as e: logger.error(f"Request to GLPI failed: {e}") return False def calculate_hash(config: str) -> str: """Calcola SHA256 della configurazione""" return hashlib.sha256(config.encode('utf-8')).hexdigest() def run_task(): """Entry point della task""" logger.info("=== NetConfig Backup Task Started ===") devices = load_devices_config(CONFIG_FILE) if not devices: logger.warning("No devices configured, exiting") return success_count = 0 for device in devices: try: device_name = device.get('name', 'unknown') device_id = device.get('glpi_id') # ID in glpi_networkdevices if not device_id: logger.warning(f"Skipping {device_name}: missing glpi_id") continue config = fetch_config(device) if not config: logger.warning(f"Failed to fetch config for {device_name}") continue # Invia a GLPI solo se diverso dall'ultimo (controllo lato server, ma ottimizzazione client) if send_to_glpi(device_id, config): success_count += 1 logger.info(f"✓ {device_name} - Config saved") else: logger.error(f"✗ {device_name} - Failed to save to GLPI") except Exception as e: logger.exception(f"Unhandled error for device {device.get('name')}") logger.info(f"=== Task Completed: {success_count}/{len(devices)} devices processed ===") if __name__ == '__main__': run_task()