200 lines
6.7 KiB
Python
200 lines
6.7 KiB
Python
#!/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()
|