# -*- coding: utf-8 -*- # pylint: disable=import-error from odoo import http, _ # IDE peut signaler une erreur, mais fonctionne dans l'environnement Odoo from odoo.http import request # IDE peut signaler une erreur, mais fonctionne dans l'environnement Odoo # pylint: enable=import-error import logging import json # Nécessaire pour parser les réponses API et utilisé dans les méthodes de traitement import requests from requests.exceptions import RequestException, ConnectionError from urllib3.exceptions import InsecureRequestWarning from datetime import datetime # Supprimer les avertissements pour les connexions non sécurisées try: # Pour les versions plus récentes de requests import urllib3 urllib3.disable_warnings(category=InsecureRequestWarning) except ImportError: # Pour les versions plus anciennes de requests try: # Désactiver l'avertissement de sécurité de pylint pour cette ligne spécifique # pylint: disable=no-member requests.packages.urllib3.disable_warnings() # pylint: enable=no-member except AttributeError: # Si ni l'un ni l'autre ne fonctionne, nous ignorons silencieusement pass _logger = logging.getLogger(__name__) class UdmProController(http.Controller): """Contrôleur pour les fonctionnalités liées à UDM Pro dans Odoo""" @http.route('/udm_pro/advanced_options', type='http', auth='user', website=True) def advanced_options_form(self): """Affiche le formulaire des options avancées pour l'API UDM Pro""" return request.render('udm_pro_docs.advanced_options_form', { 'default_site': 'default', 'fixed_only': True, 'lowercase_hostnames': True, }) @http.route('/udm_pro/restart_device', type='http', auth='user', website=True) def restart_device_form(self): """Affiche le formulaire pour redémarrer un appareil UDM Pro""" if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'): return request.render('udm_pro_docs.access_denied', { 'error_message': _("You don't have permission to restart devices.") }) # Récupérer les configurations UDM Pro enregistrées pour le formulaire configs = request.env['udm.configuration'].sudo().search([]) return request.render('udm_pro_docs.restart_device_form', { 'configs': configs }) @http.route('/udm_pro/restart_device', type='http', auth='user', website=True, methods=['POST']) def restart_device(self, **post): """Traite la demande de redémarrage d'un appareil UDM Pro""" if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'): return request.render('udm_pro_docs.access_denied', { 'error_message': _("You don't have permission to restart devices.") }) config_id = int(post.get('config_id')) mac_address = post.get('mac_address') if not mac_address: return request.render('udm_pro_docs.restart_device_form', { 'error_message': _("Please provide the MAC address of the device to restart."), 'configs': request.env['udm.configuration'].sudo().search([]) }) try: # Récupérer la configuration config = request.env['udm.configuration'].sudo().browse(config_id) if not config.exists(): raise ValueError(_("Configuration not found")) # Initialiser le client UDM Pro client = UdmProClient( host=config.host, username=config.username, password=config.password, port=config.port or 443 ) # Authentifier le client if not client.login(): return request.render('udm_pro_docs.restart_device_form', { 'error_message': _("Authentication failed. Please check the configuration credentials."), 'configs': request.env['udm.configuration'].sudo().search([]) }) # Redémarrer l'appareil success = client.restart_device(mac_address) if not success: return request.render('udm_pro_docs.restart_device_form', { 'error_message': _("Failed to restart the device. Please check logs for details."), 'configs': request.env['udm.configuration'].sudo().search([]) }) return request.render('udm_pro_docs.restart_success', { 'mac_address': mac_address }) except (ConnectionError, RequestException) as e: _logger.error("Error during UDM Pro device restart: %s", str(e)) return request.render('udm_pro_docs.restart_device_form', { 'error_message': _("Error connecting to UDM Pro: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]) }) except Exception as e: # pylint: disable=broad-except _logger.exception("Unexpected error during UDM Pro device restart") return request.render('udm_pro_docs.restart_device_form', { 'error_message': _("An unexpected error occurred: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]) }) @http.route('/udm_pro/generate_hosts', type='http', auth='user', website=True) def generate_hosts_form(self): """Affiche le formulaire pour générer un fichier hosts depuis UDM Pro""" # Récupérer les configurations UDM Pro enregistrées pour le formulaire configs = request.env['udm.configuration'].sudo().search([]) return request.render('udm_pro_docs.generate_hosts_form', { 'configs': configs, 'fixed_only': True, 'lowercase_hostnames': True }) @http.route('/udm_pro/generate_hosts', type='http', auth='user', website=True, methods=['POST']) def generate_hosts(self, **post): """Génère un fichier hosts à partir des clients réseau UDM Pro""" config_id = int(post.get('config_id')) fixed_only = post.get('fixed_only') == 'on' lowercase_hostnames = post.get('lowercase_hostnames') == 'on' try: # Récupérer la configuration config = request.env['udm.configuration'].sudo().browse(config_id) if not config.exists(): raise ValueError(_("Configuration not found")) # Initialiser le client UDM Pro avec les options avancées client = UdmProClient( host=config.host, username=config.username, password=config.password, port=config.port or 443, fixed_only=fixed_only, lowercase_hostnames=lowercase_hostnames ) # Authentifier le client if not client.login(): return request.render('udm_pro_docs.generate_hosts_form', { 'error_message': _("Authentication failed. Please check the configuration credentials."), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) # Générer le fichier hosts hosts_content = client.generate_hosts_file() # Retourner le contenu sous forme de fichier à télécharger response = request.make_response(hosts_content) response.headers['Content-Type'] = 'text/plain' response.headers['Content-Disposition'] = 'attachment; filename=udm_hosts.txt' return response except (ConnectionError, RequestException) as e: _logger.error("Error during UDM Pro hosts file generation: %s", str(e)) return request.render('udm_pro_docs.generate_hosts_form', { 'error_message': _("Error connecting to UDM Pro: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) except Exception as e: # pylint: disable=broad-except _logger.exception("Unexpected error during UDM Pro hosts file generation") return request.render('udm_pro_docs.generate_hosts_form', { 'error_message': _("An unexpected error occurred: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) @http.route('/udm_pro/network_clients', type='http', auth='user', website=True) def network_clients_form(self): """Affiche le formulaire pour consulter les clients réseau UDM Pro""" # Récupérer les configurations UDM Pro enregistrées pour le formulaire configs = request.env['udm.configuration'].sudo().search([]) return request.render('udm_pro_docs.network_clients_form', { 'configs': configs, 'fixed_only': False, 'lowercase_hostnames': True }) @http.route('/udm_pro/network_clients', type='http', auth='user', website=True, methods=['POST']) def get_network_clients(self, **post): """Récupère et affiche la liste des clients réseau UDM Pro""" config_id = int(post.get('config_id')) fixed_only = post.get('fixed_only') == 'on' lowercase_hostnames = post.get('lowercase_hostnames') == 'on' try: # Récupérer la configuration config = request.env['udm.configuration'].sudo().browse(config_id) if not config.exists(): raise ValueError(_("Configuration not found")) # Initialiser le client UDM Pro avec les options avancées client = UdmProClient( host=config.host, username=config.username, password=config.password, port=config.port or 443, fixed_only=fixed_only, lowercase_hostnames=lowercase_hostnames ) # Authentifier le client if not client.login(): return request.render('udm_pro_docs.network_clients_form', { 'error_message': _("Authentication failed. Please check the configuration credentials."), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) # Récupérer les clients réseau network_clients = client.get_network_clients() return request.render('udm_pro_docs.network_clients_result', { 'clients': network_clients, 'config': config }) except (ConnectionError, RequestException) as e: _logger.error("Error retrieving UDM Pro network clients: %s", str(e)) return request.render('udm_pro_docs.network_clients_form', { 'error_message': _("Error connecting to UDM Pro: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) except ValueError as e: _logger.error("Value error retrieving UDM Pro network clients: %s", str(e)) return request.render('udm_pro_docs.network_clients_form', { 'error_message': _("Configuration error: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) except (AttributeError, KeyError) as e: _logger.error("Data format error retrieving UDM Pro network clients: %s", str(e)) return request.render('udm_pro_docs.network_clients_form', { 'error_message': _("Data error: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) except Exception as e: # pylint: disable=broad-except # Conserver cette exception générique comme dernier recours, avec un avertissement explicite pour pylint _logger.exception("Unexpected error retrieving UDM Pro network clients") return request.render('udm_pro_docs.network_clients_form', { 'error_message': _("An unexpected error occurred: %s") % str(e), 'configs': request.env['udm.configuration'].sudo().search([]), 'fixed_only': fixed_only, 'lowercase_hostnames': lowercase_hostnames }) @http.route('/udm_pro/import_config', type='http', auth='user', website=True) def import_config_form(self): """Affiche le formulaire d'importation de configuration UDM Pro""" return request.render('udm_pro_docs.import_config_form', {}) @http.route('/udm_pro/import_config', type='http', auth='user', website=True, methods=['POST']) def import_config(self, **post): """Importe une configuration UDM Pro depuis l'appareil""" if not request.env.user.has_group('udm_pro_docs.group_udm_pro_manager'): return request.render('udm_pro_docs.access_denied', { 'error_message': _("You don't have permission to import configurations.") }) host = post.get('host') username = post.get('username') password = post.get('password') port = int(post.get('port') or 443) if not all([host, username, password]): return request.render('udm_pro_docs.import_config_form', { 'error_message': _("Please provide all required fields."), 'host': host, 'username': username, 'port': port }) try: # Utiliser le client API pour récupérer la configuration client = UdmProClient(host, username, password, port) if not client.login(): return request.render('udm_pro_docs.import_config_form', { 'error_message': _("Authentication failed. Please check your credentials."), 'host': host, 'username': username, 'port': port }) config_data = client.get_full_configuration() # Importer la configuration dans Odoo config_id = request.env['udm.configuration'].sudo().import_configuration(config_data) return request.redirect('/web#id=%s&model=udm.configuration&view_type=form' % config_id) except (ConnectionError, RequestException) as e: _logger.error("Error during UDM Pro configuration import: %s", str(e)) return request.render('udm_pro_docs.import_config_form', { 'error_message': _("Error connecting to UDM Pro: %s") % str(e), 'host': host, 'username': username, 'port': port }) except (ValueError, TypeError, AttributeError) as e: _logger.error("Data processing error during UDM Pro configuration import: %s", str(e)) return request.render('udm_pro_docs.import_config_form', { 'error_message': _("Error processing data: %s") % str(e), 'host': host, 'username': username, 'port': port }) except Exception as e: # pylint: disable=broad-except _logger.exception("Unexpected error during UDM Pro configuration import") return request.render('udm_pro_docs.import_config_form', { 'error_message': _("An unexpected error occurred. Please check server logs."), 'host': host, 'username': username, 'port': port }) class UdmProClient: """Client pour interagir avec l'API UDM Pro.""" # Points d'accès de l'API API_LOGIN_ENDPOINT = '/api/auth/login' API_SYSTEM_INFO_ENDPOINT = '/api/system' API_NETWORK_ENDPOINT = '/api/networks' API_DEVICES_ENDPOINT = '/api/devices' API_USERS_ENDPOINT = '/api/users' API_SETTINGS_ENDPOINT = '/api/settings' API_FIREWALL_ENDPOINT = '/api/firewall' # Nouveaux endpoints inspirés du client Go API_ACTIVE_CLIENTS_ENDPOINT = '/proxy/network/api/s/{site}/stat/sta' API_CONFIGURED_CLIENTS_ENDPOINT = '/proxy/network/api/s/{site}/list/user' API_DEVICE_RESTART_ENDPOINT = '/proxy/network/api/s/{site}/cmd/devmgr' def __init__(self, host, username, password, port=443, verify_ssl=False, site='default', fixed_only=True, lowercase_hostnames=True, debug=False): """ Initialise le client API UDM Pro. Args: host (str): Adresse IP ou nom d'hôte de l'UDM Pro username (str): Nom d'utilisateur pour l'API password (str): Mot de passe pour l'API port (int): Port pour la connexion (par défaut 443) verify_ssl (bool): Vérifier le certificat SSL (par défaut False) site (str): Identifiant du site pour l'API UniFi (par défaut 'default') fixed_only (bool): Ne considérer que les clients avec adresse IP fixe (par défaut True) lowercase_hostnames (bool): Convertir les noms d'hôtes en minuscules (par défaut True) debug (bool): Activer le mode débogage pour les requêtes HTTP (par défaut False) """ self.host = host self.username = username self.password = password self.port = port self.verify_ssl = verify_ssl self.site = site self.fixed_only = fixed_only self.lowercase_hostnames = lowercase_hostnames self.debug = debug self.base_url = f"https://{host}:{port}" self.token = None self.csrf_token = None self.session = requests.Session() self.session.verify = verify_ssl def _get_auth_headers(self): """Retourne les en-têtes d'authentification.""" if not self.token: raise ValueError("Non authentifié. Appelez login() d'abord.") headers = { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json" } # Ajouter le token CSRF si disponible if self.csrf_token: headers["X-Csrf-Token"] = self.csrf_token return headers def login(self): """ Authentifie le client auprès de l'API UDM Pro. Returns: bool: True si l'authentification a réussi, False sinon """ try: login_url = f"{self.base_url}{self.API_LOGIN_ENDPOINT}" payload = { "username": self.username, "password": self.password } _logger.debug("Tentative de connexion à %s", login_url) response = self.session.post(login_url, json=payload) response.raise_for_status() # Capture du token CSRF s'il existe csrf_token = response.headers.get('X-Csrf-Token') if csrf_token: self.csrf_token = csrf_token _logger.debug("Token CSRF capturé: %s", csrf_token) data = response.json() if not data.get('token'): _logger.error("Aucun token n'a été retourné par l'API") return False self.token = data['token'] _logger.debug("Authentification réussie") return True except RequestException as e: _logger.error("Erreur d'authentification: %s", str(e)) return False def _make_api_request(self, method, endpoint, params=None, data=None, retry=True): """ Effectue une requête API. Args: method (str): Méthode HTTP (GET, POST, etc.) endpoint (str): Point d'accès API params (dict): Paramètres de requête data (dict): Données à envoyer dans le corps de la requête retry (bool): Réessayer en cas d'erreur d'authentification Returns: dict: Réponse JSON de l'API """ if not self.token and retry: _logger.debug("Non authentifié, tentative d'authentification") if not self.login(): raise ConnectionError("Impossible de s'authentifier à l'API UDM Pro") url = f"{self.base_url}{endpoint}" try: _logger.debug("Requête %s vers %s", method, url) headers = self._get_auth_headers() response = self.session.request( method=method, url=url, headers=headers, params=params, json=data ) # Capture du token CSRF s'il existe dans la réponse csrf_token = response.headers.get('X-Csrf-Token') if csrf_token and csrf_token != self.csrf_token: self.csrf_token = csrf_token _logger.debug("Token CSRF mis à jour: %s", csrf_token) response.raise_for_status() return response.json() except RequestException as e: if response.status_code == 401 or response.status_code == 403: if retry: _logger.debug("Token expiré, nouvelle tentative d'authentification") self.token = None return self._make_api_request(method, endpoint, params, data, retry=False) _logger.error("Erreur API (%s %s): %s", method, url, str(e)) raise def get_system_info(self): """ Récupère les informations système de l'UDM Pro. Returns: dict: Informations système """ return self._make_api_request('GET', self.API_SYSTEM_INFO_ENDPOINT) def get_networks(self): """ Récupère la configuration des réseaux. Returns: dict: Configuration des réseaux """ return self._make_api_request('GET', self.API_NETWORK_ENDPOINT) def get_devices(self): """ Récupère la liste des périphériques. Returns: dict: Liste des périphériques """ return self._make_api_request('GET', self.API_DEVICES_ENDPOINT) def get_users(self): """ Récupère la liste des utilisateurs. Returns: dict: Liste des utilisateurs """ return self._make_api_request('GET', self.API_USERS_ENDPOINT) def get_settings(self): """ Récupère les paramètres généraux. Returns: dict: Paramètres généraux """ return self._make_api_request('GET', self.API_SETTINGS_ENDPOINT) def get_firewall_rules(self): """ Récupère les règles de pare-feu. Returns: dict: Règles de pare-feu """ return self._make_api_request('GET', self.API_FIREWALL_ENDPOINT) def get_active_clients(self): """ Récupère la liste des clients actuellement connectés. Returns: list: Liste des clients actifs """ endpoint = self.API_ACTIVE_CLIENTS_ENDPOINT.format(site=self.site) response = self._make_api_request('GET', endpoint) if not response or 'data' not in response: return [] return response.get('data', []) def get_configured_clients(self): """ Récupère la liste des clients configurés statiquement. Returns: list: Liste des clients configurés """ endpoint = self.API_CONFIGURED_CLIENTS_ENDPOINT.format(site=self.site) response = self._make_api_request('GET', endpoint) if not response or 'data' not in response: return [] return response.get('data', []) def restart_device(self, mac_address): """ Redémarre un appareil géré par l'UDM Pro (ex: point d'accès WiFi). Nécessite des permissions de niveau 'admin du site'. Args: mac_address (str): Adresse MAC de l'appareil à redémarrer Returns: bool: True si l'opération a réussi, False sinon """ endpoint = self.API_DEVICE_RESTART_ENDPOINT.format(site=self.site) payload = { 'mac': mac_address, 'reboot_type': 'soft', 'cmd': 'restart' } try: response = self._make_api_request('POST', endpoint, data=payload) if response and response.get('meta', {}).get('rc') == 'ok': return True return False except Exception as e: # pylint: disable=broad-except _logger.error("Erreur lors du redémarrage de l'appareil %s: %s", mac_address, str(e)) return False def get_network_clients(self): """ Récupère tous les clients réseau (actifs et/ou configurés selon les paramètres). Returns: list: Liste des clients réseau """ clients = [] # Toujours inclure les clients configurés statiquement configured_clients = self.get_configured_clients() clients.extend(configured_clients) # Inclure les clients actifs si fixed_only est False if not self.fixed_only: active_clients = self.get_active_clients() clients.extend(active_clients) # Traitement des noms d'hôtes si nécessaire if self.lowercase_hostnames: for client in clients: if client.get('hostname'): client['hostname'] = client['hostname'].lower() if client.get('name'): client['name'] = client['name'].lower() return clients def generate_hosts_file(self): """ Génère un fichier hosts à partir des clients réseau. Returns: str: Contenu du fichier hosts """ clients = self.get_network_clients() hosts_content = "# UDM Pro Generated Hosts File\n" hosts_content += "# Generated on {}\n\n".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')) for client in clients: name = client.get('name') or client.get('hostname') ip = client.get('fixed_ip') or client.get('ip') mac = client.get('mac', '') if name and ip: hosts_content += "{:16s} {:30s} # {}\n".format(ip, name, mac) return hosts_content def get_full_configuration(self): """ Récupère la configuration complète de l'UDM Pro. Returns: dict: Configuration complète de l'UDM Pro """ # S'authentifier d'abord if not self.token and not self.login(): raise ConnectionError("Échec de l'authentification pour la récupération de la configuration complète") try: # Récupérer chaque partie de la configuration system_info = self.get_system_info() networks = self.get_networks() devices = self.get_devices() users = self.get_users() settings = self.get_settings() firewall = self.get_firewall_rules() # Récupérer également les clients réseau (nouvelle fonctionnalité) network_clients = self.get_network_clients() # Combiner toutes les parties dans un seul dictionnaire return { 'system_info': system_info, 'networks': networks, 'devices': devices, 'users': users, 'settings': settings, 'firewall': firewall, 'network_clients': network_clients } except RequestException as e: _logger.error("Erreur lors de la récupération de la configuration complète: %s", str(e)) raise