From 5bfcaa37c26f640288ea2193a167a985e75f139f Mon Sep 17 00:00:00 2001 From: xtremxpert Date: Mon, 10 Mar 2025 10:09:39 -0400 Subject: [PATCH] unifi to test --- unifi_integration/__init__.py | 4 + unifi_integration/__manifest__.py | 46 ++ unifi_integration/controllers/__init__.py | 4 + unifi_integration/controllers/main.py | 691 ++++++++++++++++++ unifi_integration/models/__init__.py | 9 + unifi_integration/models/udm_config.py | 542 ++++++++++++++ unifi_integration/models/udm_device.py | 42 ++ unifi_integration/models/udm_firewall.py | 56 ++ unifi_integration/models/udm_network.py | 64 ++ unifi_integration/models/udm_settings.py | 27 + unifi_integration/models/udm_system_info.py | 41 ++ unifi_integration/models/udm_user.py | 23 + .../security/ir.model.access.csv | 22 + .../security/udm_pro_security.xml | 54 ++ unifi_integration/static/description/icon.png | 1 + unifi_integration/static/src/css/udm_pro.css | 70 ++ unifi_integration/views/templates.xml | 389 ++++++++++ unifi_integration/views/udm_config_views.xml | 418 +++++++++++ .../views/udm_configuration_views.xml | 138 ++++ .../views/udm_dashboard_metric_views.xml | 79 ++ unifi_integration/views/udm_device_views.xml | 37 + .../views/udm_firewall_views.xml | 41 ++ unifi_integration/views/udm_menu_views.xml | 171 +++++ unifi_integration/views/udm_network_views.xml | 38 + .../views/udm_settings_views.xml | 29 + unifi_integration/views/udm_site_views.xml | 281 +++++++ .../views/udm_system_info_views.xml | 33 + unifi_integration/views/udm_user_views.xml | 35 + unifi_integration/views/udm_vlan_views.xml | 39 + 29 files changed, 3424 insertions(+) create mode 100644 unifi_integration/__init__.py create mode 100644 unifi_integration/__manifest__.py create mode 100644 unifi_integration/controllers/__init__.py create mode 100644 unifi_integration/controllers/main.py create mode 100644 unifi_integration/models/__init__.py create mode 100644 unifi_integration/models/udm_config.py create mode 100644 unifi_integration/models/udm_device.py create mode 100644 unifi_integration/models/udm_firewall.py create mode 100644 unifi_integration/models/udm_network.py create mode 100644 unifi_integration/models/udm_settings.py create mode 100644 unifi_integration/models/udm_system_info.py create mode 100644 unifi_integration/models/udm_user.py create mode 100644 unifi_integration/security/ir.model.access.csv create mode 100644 unifi_integration/security/udm_pro_security.xml create mode 100644 unifi_integration/static/description/icon.png create mode 100644 unifi_integration/static/src/css/udm_pro.css create mode 100644 unifi_integration/views/templates.xml create mode 100644 unifi_integration/views/udm_config_views.xml create mode 100644 unifi_integration/views/udm_configuration_views.xml create mode 100644 unifi_integration/views/udm_dashboard_metric_views.xml create mode 100644 unifi_integration/views/udm_device_views.xml create mode 100644 unifi_integration/views/udm_firewall_views.xml create mode 100644 unifi_integration/views/udm_menu_views.xml create mode 100644 unifi_integration/views/udm_network_views.xml create mode 100644 unifi_integration/views/udm_settings_views.xml create mode 100644 unifi_integration/views/udm_site_views.xml create mode 100644 unifi_integration/views/udm_system_info_views.xml create mode 100644 unifi_integration/views/udm_user_views.xml create mode 100644 unifi_integration/views/udm_vlan_views.xml diff --git a/unifi_integration/__init__.py b/unifi_integration/__init__.py new file mode 100644 index 0000000..c3d410e --- /dev/null +++ b/unifi_integration/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import controllers diff --git a/unifi_integration/__manifest__.py b/unifi_integration/__manifest__.py new file mode 100644 index 0000000..fe04d72 --- /dev/null +++ b/unifi_integration/__manifest__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'Unifi Integration', + 'version': '1.0', + 'category': 'Network/Documentation', + 'summary': 'Store and manage Unifi configurations', + 'description': """ +Unifi Integration +================= +This module allows you to: +* Connect to Unifi devices and retrieve configurations +* Store configuration history in the database +* Generate documentation of network setup +* Compare configurations over time +""", + 'author': 'Your Company', + 'website': 'https://www.bemade.org', + 'depends': ['base', 'web', 'website'], + 'data': [ + 'security/udm_pro_security.xml', + 'security/ir.model.access.csv', + 'views/udm_configuration_views.xml', + 'views/udm_system_info_views.xml', + 'views/udm_network_views.xml', + 'views/udm_vlan_views.xml', + 'views/udm_device_views.xml', + 'views/udm_user_views.xml', + 'views/udm_settings_views.xml', + 'views/udm_firewall_views.xml', + 'views/udm_menu_views.xml', + 'views/templates.xml', + ], + 'demo': [], + 'installable': True, + 'application': True, + 'auto_install': False, + 'license': 'LGPL-3', + 'external_dependencies': { + 'python': ['requests'], + }, + 'assets': { + 'web.assets_backend': [ + 'udm_pro_docs/static/src/css/udm_pro.css', + ], + }, +} diff --git a/unifi_integration/controllers/__init__.py b/unifi_integration/controllers/__init__.py new file mode 100644 index 0000000..a8b0a8d --- /dev/null +++ b/unifi_integration/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +# Import du module principal des contrôleurs +from . import main # pylint: disable=relative-beyond-top-level diff --git a/unifi_integration/controllers/main.py b/unifi_integration/controllers/main.py new file mode 100644 index 0000000..3e8c16a --- /dev/null +++ b/unifi_integration/controllers/main.py @@ -0,0 +1,691 @@ +# -*- 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 diff --git a/unifi_integration/models/__init__.py b/unifi_integration/models/__init__.py new file mode 100644 index 0000000..39c9d38 --- /dev/null +++ b/unifi_integration/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from . import udm_config +from . import udm_system_info +from . import udm_network +from . import udm_device +from . import udm_user +from . import udm_settings +from . import udm_firewall diff --git a/unifi_integration/models/udm_config.py b/unifi_integration/models/udm_config.py new file mode 100644 index 0000000..7cd2c8b --- /dev/null +++ b/unifi_integration/models/udm_config.py @@ -0,0 +1,542 @@ +# -*- coding: utf-8 -*- + +# Ces importations fonctionneront dans un environnement Odoo, même si votre IDE les signale comme non trouvées +# pylint: disable=import-error +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +# pylint: enable=import-error +import logging +import json +from datetime import datetime +import random # Pour générer des données de test/démonstration + +_logger = logging.getLogger(__name__) + +class UdmSite(models.Model): + """Représente un site UniFi géré par un ou plusieurs UDM Pro""" + _name = 'udm.site' + _description = 'UDM Site' + _order = 'name' + + name = fields.Char(string='Name', required=True) + site_id = fields.Char(string='Site ID', help="Identifiant du site dans UniFi (généralement 'default' sauf si configuré autrement)", default='default') + description = fields.Text(string='Description') + address = fields.Text(string='Physical Address') + active = fields.Boolean(string='Active', default=True) + + # Relations + configuration_ids = fields.One2many('udm.configuration', 'site_id', string='Configurations') + dashboard_ids = fields.One2many('udm.dashboard.metric', 'site_id', string='Dashboard Metrics') + + # Compteurs + config_count = fields.Integer(compute='_compute_counts', string='Configuration Count') + device_count = fields.Integer(compute='_compute_device_count', string='Total Devices') + client_count = fields.Integer(compute='_compute_client_count', string='Connected Clients') + + @api.depends('configuration_ids') + def _compute_counts(self): + for record in self: + record.config_count = len(record.configuration_ids) + + @api.depends('configuration_ids.device_ids') + def _compute_device_count(self): + for record in self: + count = 0 + for config in record.configuration_ids: + count += len(config.device_ids) + record.device_count = count + + @api.depends('dashboard_ids') + def _compute_client_count(self): + for record in self: + client_metric = record.dashboard_ids.filtered(lambda m: m.metric_type == 'clients_count') + if client_metric and len(client_metric) > 0: + record.client_count = int(client_metric[0].current_value) + else: + record.client_count = 0 + + def action_view_configurations(self): + self.ensure_one() + return { + 'name': _('Configurations'), + 'view_mode': 'tree,form', + 'res_model': 'udm.configuration', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + } + + def action_view_dashboard(self): + self.ensure_one() + return { + 'name': _('Site Dashboard'), + 'view_mode': 'dashboard,form', + 'res_model': 'udm.site', + 'res_id': self.id, + 'type': 'ir.actions.act_window', + } + + def action_refresh_metrics(self): + """Rafraîchit les métriques du tableau de bord pour ce site""" + self.ensure_one() + configs = self.configuration_ids.filtered(lambda c: c.active) + if not configs: + raise UserError(_('No active UDM Pro configuration found for this site')) + + # Dans une implémentation réelle, vous appelleriez l'API UDM Pro ici + # Pour le moment, nous simulons les métriques + self._generate_sample_metrics() + + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } + + def _generate_sample_metrics(self): + """Génère des métriques de démonstration pour le tableau de bord""" + # Supprimer les anciennes métriques + self.dashboard_ids.unlink() + + # Types de métriques à générer + metric_types = [ + 'bandwidth_usage', 'cpu_usage', 'memory_usage', 'clients_count', + 'wan_status', 'threat_count', 'device_status' + ] + + # Générer les nouvelles métriques + metrics_vals = [] + now = fields.Datetime.now() + + for metric_type in metric_types: + # Générer la valeur actuelle + current_value = '' + max_value = '' + history = '' + + if metric_type == 'bandwidth_usage': + current_value = str(random.uniform(5, 200)) # Mbps + max_value = '1000' + history = self._generate_history_data(50, 250, 24) + elif metric_type in ['cpu_usage', 'memory_usage']: + current_value = str(random.uniform(10, 90)) # Pourcentage + max_value = '100' + history = self._generate_history_data(10, 90, 24) + elif metric_type == 'clients_count': + current_value = str(random.randint(5, 50)) + max_value = '100' + history = self._generate_history_data(5, 60, 24, integer=True) + elif metric_type == 'threat_count': + current_value = str(random.randint(0, 10)) + max_value = '100' + history = self._generate_history_data(0, 15, 24, integer=True) + elif metric_type == 'wan_status': + current_value = random.choice(['up', 'up']) # Principalement en ligne + max_value = '' + history = '' + elif metric_type == 'device_status': + total = random.randint(5, 20) + offline = random.randint(0, 2) + current_value = f"{total-offline}/{total}" + max_value = str(total) + history = '' + + metrics_vals.append({ + 'site_id': self.id, + 'metric_type': metric_type, + 'current_value': current_value, + 'max_value': max_value, + 'history_data': history, + 'last_update': now, + }) + + # Créer les métriques + for vals in metrics_vals: + self.env['udm.dashboard.metric'].create(vals) + + def _generate_history_data(self, min_val, max_val, points, integer=False): + """Génère des données historiques pour les graphiques""" + data = [] + for _ in range(points): # Utilisation de _ pour indiquer une variable non utilisée + if integer: + value = random.randint(min_val, max_val) + else: + value = random.uniform(min_val, max_val) + value = round(value, 2) + data.append(value) + return json.dumps(data) + + +class UdmDashboardMetric(models.Model): + """Stocke les métriques pour le tableau de bord UDM Pro""" + _name = 'udm.dashboard.metric' + _description = 'UDM Dashboard Metric' + _order = 'last_update desc' + + site_id = fields.Many2one('udm.site', string='Site', required=True, ondelete='cascade') + metric_type = fields.Selection([ + ('bandwidth_usage', 'Bandwidth Usage'), + ('cpu_usage', 'CPU Usage'), + ('memory_usage', 'Memory Usage'), + ('clients_count', 'Connected Clients'), + ('wan_status', 'WAN Status'), + ('threat_count', 'Security Threats'), + ('device_status', 'Device Status'), + ], string='Metric Type', required=True) + + current_value = fields.Char(string='Current Value', required=True) + max_value = fields.Char(string='Maximum Value') + history_data = fields.Text(string='Historical Data', help="Données JSON pour les graphiques d'historique") + last_update = fields.Datetime(string='Last Update', default=fields.Datetime.now) + + # Champs calculés pour l'affichage + formatted_value = fields.Char(compute='_compute_formatted_value', string='Formatted Value') + status = fields.Selection([ + ('normal', 'Normal'), + ('warning', 'Warning'), + ('critical', 'Critical'), + ], compute='_compute_status', string='Status') + + @api.depends('current_value', 'metric_type') + def _compute_formatted_value(self): + for record in self: + if record.metric_type == 'bandwidth_usage': + try: + value = float(record.current_value) + if value >= 1000: + record.formatted_value = f"{value/1000:.2f} Gbps" + else: + record.formatted_value = f"{value:.2f} Mbps" + except (ValueError, TypeError): + record.formatted_value = record.current_value + elif record.metric_type in ['cpu_usage', 'memory_usage']: + try: + value = float(record.current_value) + record.formatted_value = f"{value:.1f}%" + except (ValueError, TypeError): + record.formatted_value = record.current_value + elif record.metric_type == 'wan_status': + if record.current_value == 'up': + record.formatted_value = _('Online') + else: + record.formatted_value = _('Offline') + else: + record.formatted_value = record.current_value + + @api.depends('current_value', 'max_value', 'metric_type') + def _compute_status(self): + for record in self: + if record.metric_type in ['cpu_usage', 'memory_usage']: + try: + value = float(record.current_value) + if value > 90: + record.status = 'critical' + elif value > 70: + record.status = 'warning' + else: + record.status = 'normal' + except (ValueError, TypeError): + record.status = 'normal' + elif record.metric_type == 'bandwidth_usage': + try: + value = float(record.current_value) + max_value = float(record.max_value) if record.max_value else 1000 + if value > max_value * 0.9: + record.status = 'critical' + elif value > max_value * 0.7: + record.status = 'warning' + else: + record.status = 'normal' + except (ValueError, TypeError): + record.status = 'normal' + elif record.metric_type == 'threat_count': + try: + value = int(record.current_value) + if value > 5: + record.status = 'critical' + elif value > 0: + record.status = 'warning' + else: + record.status = 'normal' + except (ValueError, TypeError): + record.status = 'normal' + elif record.metric_type == 'wan_status': + if record.current_value == 'up': + record.status = 'normal' + else: + record.status = 'critical' + else: + record.status = 'normal' + + +class UdmConfiguration(models.Model): + """Configuration complète d'un UDM Pro stockée dans Odoo""" + _name = 'udm.configuration' + _description = 'UDM Pro Configuration' + _order = 'timestamp desc' + + name = fields.Char(string='Name', compute='_compute_name', store=True) + timestamp = fields.Datetime(string='Timestamp', default=fields.Datetime.now, required=True) + raw_data = fields.Text(string='Raw Data', help="Les données brutes de configuration en format JSON") + active = fields.Boolean(string='Active', default=True, help="Indique si cette configuration est actuellement active") + + # Connexion à l'UDM Pro + host = fields.Char(string='Host', help="Adresse IP ou nom d'hôte de l'UDM Pro") + port = fields.Integer(string='Port', default=443) + username = fields.Char(string='Username') + password = fields.Char(string='Password') + + # Relations + site_id = fields.Many2one('udm.site', string='Site', ondelete='restrict') + system_info_id = fields.Many2one('udm.system.info', string='System Info', ondelete='cascade') + network_ids = fields.One2many('udm.network', 'config_id', string='Networks') + vlan_ids = fields.One2many('udm.vlan', 'config_id', string='VLANs') + device_ids = fields.One2many('udm.device', 'config_id', string='Devices') + user_ids = fields.One2many('udm.user', 'config_id', string='Users') + settings_id = fields.Many2one('udm.settings', string='Settings', ondelete='cascade') + firewall_rule_ids = fields.One2many('udm.firewall.rule', 'config_id', string='Firewall Rules') + + # Statistiques + network_count = fields.Integer(compute='_compute_counts', string='Network Count') + device_count = fields.Integer(compute='_compute_counts', string='Device Count') + user_count = fields.Integer(compute='_compute_counts', string='User Count') + firewall_rule_count = fields.Integer(compute='_compute_counts', string='Firewall Rule Count') + + @api.depends('timestamp', 'system_info_id.hostname') + def _compute_name(self): + for record in self: + hostname = record.system_info_id.hostname or 'Unknown' + timestamp = record.timestamp.strftime('%Y-%m-%d %H:%M:%S') if record.timestamp else '' + record.name = f"{hostname} ({timestamp})" + + @api.depends('network_ids', 'device_ids', 'user_ids', 'firewall_rule_ids') + def _compute_counts(self): + for record in self: + record.network_count = len(record.network_ids) + record.device_count = len(record.device_ids) + record.user_count = len(record.user_ids) + record.firewall_rule_count = len(record.firewall_rule_ids) + + def action_view_networks(self): + self.ensure_one() + return { + 'name': _('Networks'), + 'view_mode': 'tree,form', + 'res_model': 'udm.network', + 'domain': [('config_id', '=', self.id)], + 'type': 'ir.actions.act_window', + } + + def action_view_devices(self): + self.ensure_one() + return { + 'name': _('Devices'), + 'view_mode': 'tree,form', + 'res_model': 'udm.device', + 'domain': [('config_id', '=', self.id)], + 'type': 'ir.actions.act_window', + } + + def action_view_users(self): + self.ensure_one() + return { + 'name': _('Users'), + 'view_mode': 'tree,form', + 'res_model': 'udm.user', + 'domain': [('config_id', '=', self.id)], + 'type': 'ir.actions.act_window', + } + + def action_view_firewall_rules(self): + self.ensure_one() + return { + 'name': _('Firewall Rules'), + 'view_mode': 'tree,form', + 'res_model': 'udm.firewall.rule', + 'domain': [('config_id', '=', self.id)], + 'type': 'ir.actions.act_window', + } + + @api.model + def import_configuration(self, config_data): + """ + Importe une configuration UDM Pro complète dans Odoo + + Args: + config_data (dict): Données de configuration brutes de l'API + + Returns: + int: ID de la configuration créée + """ + if not config_data: + raise UserError(_("No configuration data provided")) + + # Créer la configuration principale + vals = { + 'timestamp': datetime.now(), + 'raw_data': json.dumps(config_data, indent=2, ensure_ascii=False), + } + + config = self.create(vals) + + # Créer les informations système + system_info_data = config_data.get('system_info', {}) + if system_info_data: + system_info = self.env['udm.system.info'].create({ + 'config_id': config.id, + 'hostname': system_info_data.get('hostname', ''), + 'version': system_info_data.get('version', ''), + 'model': system_info_data.get('model', ''), + 'uptime': system_info_data.get('uptime', 0), + 'serial': system_info_data.get('serialNumber', ''), + 'mac_address': system_info_data.get('macAddress', ''), + 'raw_data': json.dumps(system_info_data, indent=2, ensure_ascii=False), + }) + config.system_info_id = system_info.id + + # Créer les réseaux + networks_data = config_data.get('networks', {}).get('networks', []) + for network_data in networks_data: + if isinstance(network_data, dict): + self.env['udm.network'].create({ + 'config_id': config.id, + 'name': network_data.get('name', ''), + 'purpose': network_data.get('purpose', ''), + 'subnet': network_data.get('subnet', ''), + 'vlan_id_number': network_data.get('vlanId'), + 'dhcp_enabled': network_data.get('dhcpEnabled', False), + 'dhcp_start': network_data.get('dhcpStart', ''), + 'dhcp_stop': network_data.get('dhcpStop', ''), + 'domain_name': network_data.get('domainName', ''), + 'raw_data': json.dumps(network_data, indent=2, ensure_ascii=False), + }) + + # Créer les VLANs + vlans_data = config_data.get('networks', {}).get('vlans', []) + for vlan_data in vlans_data: + if isinstance(vlan_data, dict): + self.env['udm.vlan'].create({ + 'config_id': config.id, + 'vlan_id': vlan_data.get('id', 0), + 'name': vlan_data.get('name', ''), + 'raw_data': json.dumps(vlan_data, indent=2, ensure_ascii=False), + }) + + # Créer les périphériques + devices_data = config_data.get('devices', {}).get('devices', []) + for device_data in devices_data: + if isinstance(device_data, dict): + self.env['udm.device'].create({ + 'config_id': config.id, + 'name': device_data.get('name', ''), + 'mac': device_data.get('mac', ''), + 'ip': device_data.get('ip', ''), + 'device_type': device_data.get('type', ''), + 'model': device_data.get('model', ''), + 'last_seen': datetime.fromtimestamp(device_data.get('lastSeen', 0)), + 'raw_data': json.dumps(device_data, indent=2, ensure_ascii=False), + }) + + # Créer les utilisateurs + users_data = config_data.get('users', {}).get('users', []) + for user_data in users_data: + if isinstance(user_data, dict): + self.env['udm.user'].create({ + 'config_id': config.id, + 'name': user_data.get('name', ''), + 'email': user_data.get('email', ''), + 'role': user_data.get('role', ''), + 'enabled': user_data.get('enabled', True), + 'raw_data': json.dumps(user_data, indent=2, ensure_ascii=False), + }) + + # Créer les paramètres + settings_data = config_data.get('settings', {}) + if settings_data: + settings = self.env['udm.settings'].create({ + 'config_id': config.id, + 'timezone': settings_data.get('timezone', ''), + 'ntp_servers': ','.join(settings_data.get('ntpServers', [])), + 'dns_servers': ','.join(settings_data.get('dnsServers', [])), + 'raw_data': json.dumps(settings_data, indent=2, ensure_ascii=False), + }) + config.settings_id = settings.id + + # Créer les règles de pare-feu + firewall_data = config_data.get('firewall', {}).get('rules', []) + for rule_data in firewall_data: + if isinstance(rule_data, dict): + self.env['udm.firewall.rule'].create({ + 'config_id': config.id, + 'name': rule_data.get('name', ''), + 'description': rule_data.get('description', ''), + 'action': rule_data.get('action', 'drop'), + 'protocol': rule_data.get('protocol', ''), + 'source': rule_data.get('source', ''), + 'destination': rule_data.get('destination', ''), + 'enabled': rule_data.get('enabled', True), + 'raw_data': json.dumps(rule_data, indent=2, ensure_ascii=False), + }) + + # Journaliser l'importation réussie + _logger.info('Successfully imported UDM Pro configuration: %s', config.name) + + return config.id + + def action_compare_with(self): + """Ouvre un assistant pour comparer cette configuration avec une autre""" + self.ensure_one() + return { + 'name': _('Compare Configurations'), + 'type': 'ir.actions.act_window', + 'res_model': 'udm.configuration.compare.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'default_source_config_id': self.id, + }, + } + + def action_duplicate(self): + """Duplique cette configuration""" + self.ensure_one() + + # Créer une nouvelle configuration en copiant les données brutes + new_config = self.copy({ + 'timestamp': datetime.now(), + 'name': _('%s (Copy)') % self.name, + }) + + return { + 'name': _('Duplicated Configuration'), + 'type': 'ir.actions.act_window', + 'res_model': 'udm.configuration', + 'res_id': new_config.id, + 'view_mode': 'form', + } + + def action_generate_report(self): + """Génère un rapport PDF de cette configuration""" + self.ensure_one() + return self.env.ref('udm_pro_docs.action_report_udm_configuration').report_action(self) + + def action_view_dashboard(self): + """Affiche le tableau de bord pour le site de cette configuration""" + self.ensure_one() + if not self.site_id: + raise UserError(_('This configuration is not associated with any site. Please set a site first.')) + + return { + 'name': _('Site Dashboard'), + 'view_mode': 'dashboard,form', + 'res_model': 'udm.site', + 'res_id': self.site_id.id, + 'type': 'ir.actions.act_window', + } + + def action_update_dashboard_metrics(self): + """Met à jour les métriques du tableau de bord pour le site de cette configuration""" + self.ensure_one() + if not self.site_id: + raise UserError(_('This configuration is not associated with any site. Please set a site first.')) + + # Appeler la méthode de rafraîchissement des métriques du site + return self.site_id.action_refresh_metrics() diff --git a/unifi_integration/models/udm_device.py b/unifi_integration/models/udm_device.py new file mode 100644 index 0000000..44ee890 --- /dev/null +++ b/unifi_integration/models/udm_device.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmDevice(models.Model): + """Représentation d'un périphérique dans l'UDM Pro""" + _name = 'udm.device' + _description = 'UDM Pro Device' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + name = fields.Char(string='Name') + mac = fields.Char(string='MAC Address') + ip = fields.Char(string='IP Address') + device_type = fields.Char(string='Device Type') + model = fields.Char(string='Model') + last_seen = fields.Datetime(string='Last Seen') + raw_data = fields.Text(string='Raw Data') + + # Champs calculés + status = fields.Selection([ + ('online', 'Online'), + ('offline', 'Offline'), + ('unknown', 'Unknown') + ], string='Status', compute='_compute_status', store=True) + + @api.depends('last_seen') + def _compute_status(self): + # Calcul du statut basé sur la dernière fois où le périphérique a été vu + # Ceci est un exemple simplifié + for record in self: + if not record.last_seen: + record.status = 'unknown' + continue + + from datetime import datetime, timedelta + now = fields.Datetime.now() + time_diff = now - record.last_seen + + if time_diff <= timedelta(minutes=10): + record.status = 'online' + else: + record.status = 'offline' diff --git a/unifi_integration/models/udm_firewall.py b/unifi_integration/models/udm_firewall.py new file mode 100644 index 0000000..1062cbc --- /dev/null +++ b/unifi_integration/models/udm_firewall.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmFirewallRule(models.Model): + """Représentation d'une règle de pare-feu dans l'UDM Pro""" + _name = 'udm.firewall.rule' + _description = 'UDM Pro Firewall Rule' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + name = fields.Char(string='Name', required=True) + enabled = fields.Boolean(string='Enabled', default=True) + action = fields.Selection([ + ('accept', 'Accept'), + ('drop', 'Drop'), + ('reject', 'Reject') + ], string='Action') + protocol = fields.Selection([ + ('tcp', 'TCP'), + ('udp', 'UDP'), + ('icmp', 'ICMP'), + ('all', 'All') + ], string='Protocol') + src_address = fields.Char(string='Source Address') + dst_address = fields.Char(string='Destination Address') + src_port = fields.Char(string='Source Port') + dst_port = fields.Char(string='Destination Port') + raw_data = fields.Text(string='Raw Data') + + # Champs calculés + rule_summary = fields.Char(string='Rule Summary', compute='_compute_rule_summary') + + @api.depends('action', 'protocol', 'src_address', 'dst_address', 'src_port', 'dst_port') + def _compute_rule_summary(self): + for record in self: + parts = [] + + if record.action: + parts.append(record.action.upper()) + + if record.protocol: + parts.append(record.protocol.upper()) + + if record.src_address: + src = f"from {record.src_address}" + if record.src_port: + src += f":{record.src_port}" + parts.append(src) + + if record.dst_address: + dst = f"to {record.dst_address}" + if record.dst_port: + dst += f":{record.dst_port}" + parts.append(dst) + + record.rule_summary = ' '.join(parts) if parts else 'No details' diff --git a/unifi_integration/models/udm_network.py b/unifi_integration/models/udm_network.py new file mode 100644 index 0000000..1b5faa0 --- /dev/null +++ b/unifi_integration/models/udm_network.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmNetwork(models.Model): + """Représentation d'un réseau dans l'UDM Pro""" + _name = 'udm.network' + _description = 'UDM Pro Network' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + name = fields.Char(string='Name', required=True) + purpose = fields.Char(string='Purpose') + subnet = fields.Char(string='Subnet') + vlan_id_number = fields.Integer(string='VLAN ID') + vlan_id = fields.Many2one('udm.vlan', string='VLAN', compute='_compute_vlan_id', store=True) + dhcp_enabled = fields.Boolean(string='DHCP Enabled', default=False) + dhcp_start = fields.Char(string='DHCP Start') + dhcp_stop = fields.Char(string='DHCP Stop') + domain_name = fields.Char(string='Domain Name') + raw_data = fields.Text(string='Raw Data') + + # Statistiques + device_count = fields.Integer(string='Device Count', compute='_compute_device_count') + + @api.depends('vlan_id_number', 'config_id') + def _compute_vlan_id(self): + for record in self: + if not record.vlan_id_number or not record.config_id: + record.vlan_id = False + continue + + vlan = self.env['udm.vlan'].search([ + ('config_id', '=', record.config_id.id), + ('vlan_id', '=', record.vlan_id_number) + ], limit=1) + + record.vlan_id = vlan.id if vlan else False + + def _compute_device_count(self): + for record in self: + # Cette fonction serait plus précise si les périphériques étaient liés aux réseaux + # Pour l'instant, c'est juste un exemple + record.device_count = 0 + + +class UdmVLAN(models.Model): + """Représentation d'un VLAN dans l'UDM Pro""" + _name = 'udm.vlan' + _description = 'UDM Pro VLAN' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + vlan_id = fields.Integer(string='VLAN ID', required=True) + name = fields.Char(string='Name', required=True) + enabled = fields.Boolean(string='Enabled', default=True) + raw_data = fields.Text(string='Raw Data') + + # Relations inverses + network_ids = fields.One2many('udm.network', 'vlan_id', string='Networks') + network_count = fields.Integer(compute='_compute_network_count') + + @api.depends('network_ids') + def _compute_network_count(self): + for record in self: + record.network_count = len(record.network_ids) diff --git a/unifi_integration/models/udm_settings.py b/unifi_integration/models/udm_settings.py new file mode 100644 index 0000000..1ab7d9f --- /dev/null +++ b/unifi_integration/models/udm_settings.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmSettings(models.Model): + """Configuration générale de l'UDM Pro""" + _name = 'udm.settings' + _description = 'UDM Pro Settings' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + timezone = fields.Char(string='Timezone') + ntp_servers = fields.Char(string='NTP Servers') + dns_servers = fields.Char(string='DNS Servers') + raw_data = fields.Text(string='Raw Data') + + # Champs calculés + ntp_server_list = fields.Many2many('ir.model.data', string='NTP Server List', + compute='_compute_server_lists') + dns_server_list = fields.Many2many('ir.model.data', string='DNS Server List', + compute='_compute_server_lists') + + @api.depends('ntp_servers', 'dns_servers') + def _compute_server_lists(self): + for record in self: + # Conversion des chaînes de caractères en listes pour l'affichage dans l'interface + record.ntp_server_list = False # Ceci est un champ technique pour l'UI + record.dns_server_list = False # Ceci est un champ technique pour l'UI diff --git a/unifi_integration/models/udm_system_info.py b/unifi_integration/models/udm_system_info.py new file mode 100644 index 0000000..62bd6b0 --- /dev/null +++ b/unifi_integration/models/udm_system_info.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmSystemInfo(models.Model): + """Informations système de l'UDM Pro""" + _name = 'udm.system.info' + _description = 'UDM Pro System Information' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + hostname = fields.Char(string='Hostname') + version = fields.Char(string='Firmware Version') + model = fields.Char(string='Model') + uptime = fields.Integer(string='Uptime (seconds)') + uptime_human = fields.Char(string='Uptime', compute='_compute_uptime_human') + serial = fields.Char(string='Serial Number') + mac_address = fields.Char(string='MAC Address') + raw_data = fields.Text(string='Raw Data') + + @api.depends('uptime') + def _compute_uptime_human(self): + for record in self: + if not record.uptime: + record.uptime_human = 'Unknown' + continue + + days, remainder = divmod(record.uptime, 86400) + hours, remainder = divmod(remainder, 3600) + minutes, seconds = divmod(remainder, 60) + + parts = [] + if days: + parts.append(f"{days} day{'s' if days != 1 else ''}") + if hours: + parts.append(f"{hours} hour{'s' if hours != 1 else ''}") + if minutes: + parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}") + if seconds or not parts: + parts.append(f"{seconds} second{'s' if seconds != 1 else ''}") + + record.uptime_human = ', '.join(parts) diff --git a/unifi_integration/models/udm_user.py b/unifi_integration/models/udm_user.py new file mode 100644 index 0000000..9f419a8 --- /dev/null +++ b/unifi_integration/models/udm_user.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ + +class UdmUser(models.Model): + """Représentation d'un utilisateur dans l'UDM Pro""" + _name = 'udm.user' + _description = 'UDM Pro User' + + config_id = fields.Many2one('udm.configuration', string='Configuration', ondelete='cascade') + name = fields.Char(string='Name', required=True) + email = fields.Char(string='Email') + role = fields.Char(string='Role') + enabled = fields.Boolean(string='Enabled', default=True) + raw_data = fields.Text(string='Raw Data') + + # Champs calculés pour des statistiques ou filtrage + is_admin = fields.Boolean(string='Is Admin', compute='_compute_is_admin', store=True) + + @api.depends('role') + def _compute_is_admin(self): + for record in self: + record.is_admin = record.role and 'admin' in record.role.lower() or False diff --git a/unifi_integration/security/ir.model.access.csv b/unifi_integration/security/ir.model.access.csv new file mode 100644 index 0000000..8490810 --- /dev/null +++ b/unifi_integration/security/ir.model.access.csv @@ -0,0 +1,22 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_udm_configuration_user,udm.configuration.user,model_udm_configuration,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_configuration_manager,udm.configuration.manager,model_udm_configuration,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_system_info_user,udm.system.info.user,model_udm_system_info,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_system_info_manager,udm.system.info.manager,model_udm_system_info,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_network_user,udm.network.user,model_udm_network,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_network_manager,udm.network.manager,model_udm_network,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_vlan_user,udm.vlan.user,model_udm_vlan,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_vlan_manager,udm.vlan.manager,model_udm_vlan,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_device_user,udm.device.user,model_udm_device,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_device_manager,udm.device.manager,model_udm_device,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_user_user,udm.user.user,model_udm_user,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_user_manager,udm.user.manager,model_udm_user,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_settings_user,udm.settings.user,model_udm_settings,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_settings_manager,udm.settings.manager,model_udm_settings,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_firewall_rule_user,udm.firewall.rule.user,model_udm_firewall_rule,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_firewall_rule_manager,udm.firewall.rule.manager,model_udm_firewall_rule,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +# Nouveaux modèles pour le tableau de bord et la gestion multi-sites +access_udm_site_user,udm.site.user,model_udm_site,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_site_manager,udm.site.manager,model_udm_site,udm_pro_docs.group_udm_pro_manager,1,1,1,1 +access_udm_dashboard_metric_user,udm.dashboard.metric.user,model_udm_dashboard_metric,udm_pro_docs.group_udm_pro_user,1,0,0,0 +access_udm_dashboard_metric_manager,udm.dashboard.metric.manager,model_udm_dashboard_metric,udm_pro_docs.group_udm_pro_manager,1,1,1,1 diff --git a/unifi_integration/security/udm_pro_security.xml b/unifi_integration/security/udm_pro_security.xml new file mode 100644 index 0000000..19b820b --- /dev/null +++ b/unifi_integration/security/udm_pro_security.xml @@ -0,0 +1,54 @@ + + + + + + UDM Pro Documentation + Manage UDM Pro configurations and documentation + 50 + + + + + User + + + + + + + Manager + + + + + + + + + + + + UDM Pro Configuration User Access + + [(1, '=', 1)] + + + + + + + + + + UDM Pro Configuration Manager Access + + [(1, '=', 1)] + + + + + + + + diff --git a/unifi_integration/static/description/icon.png b/unifi_integration/static/description/icon.png new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/unifi_integration/static/description/icon.png @@ -0,0 +1 @@ + diff --git a/unifi_integration/static/src/css/udm_pro.css b/unifi_integration/static/src/css/udm_pro.css new file mode 100644 index 0000000..c7ae100 --- /dev/null +++ b/unifi_integration/static/src/css/udm_pro.css @@ -0,0 +1,70 @@ +/* UDM Pro Documentation Module CSS */ + +.o_udm_pro_config_header { + background-color: #f5f5f5; + padding: 16px; + margin-bottom: 16px; + border-radius: 4px; +} + +.o_udm_pro_status_online { + color: #28a745; + font-weight: bold; +} + +.o_udm_pro_status_offline { + color: #dc3545; + font-weight: bold; +} + +.o_udm_pro_network_card { + border: 1px solid #ddd; + border-radius: 4px; + padding: 16px; + margin-bottom: 16px; + transition: all 0.3s ease; +} + +.o_udm_pro_network_card:hover { + box-shadow: 0 5px 15px rgba(0,0,0,0.1); +} + +.o_udm_pro_vlan_tag { + display: inline-block; + padding: 4px 8px; + background-color: #6c757d; + color: white; + border-radius: 4px; + margin-right: 8px; + margin-bottom: 8px; + font-size: 0.85rem; +} + +.o_udm_pro_device_count { + font-size: 1.5rem; + font-weight: bold; + color: #007bff; +} + +.o_udm_pro_firewall_rule { + padding: 12px; + border-left: 4px solid transparent; + margin-bottom: 8px; + transition: all 0.2s ease; +} + +.o_udm_pro_firewall_rule.accept { + border-left-color: #28a745; +} + +.o_udm_pro_firewall_rule.drop { + border-left-color: #dc3545; +} + +.o_udm_pro_firewall_rule.reject { + border-left-color: #fd7e14; +} + +.o_udm_pro_import_button { + margin-top: 16px; +} diff --git a/unifi_integration/views/templates.xml b/unifi_integration/views/templates.xml new file mode 100644 index 0000000..1594ade --- /dev/null +++ b/unifi_integration/views/templates.xml @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unifi_integration/views/udm_config_views.xml b/unifi_integration/views/udm_config_views.xml new file mode 100644 index 0000000..c401b07 --- /dev/null +++ b/unifi_integration/views/udm_config_views.xml @@ -0,0 +1,418 @@ + + + + + udm.configuration.tree + udm.configuration + + + + + + + + + + + + + + + + + + udm.configuration.form + udm.configuration + +
+ +
+ + + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + udm.configuration.search + udm.configuration + + + + + + + + + + + + + + + + + + udm.system.info.form + udm.system.info + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + udm.network.form + udm.network + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + udm.vlan.form + udm.vlan + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + udm.device.form + udm.device + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + udm.user.form + udm.user + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + + udm.settings.form + udm.settings + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + + udm.firewall.rule.form + udm.firewall.rule + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + Configurations + udm.configuration + tree,form + +

+ Import your first UDM Pro configuration +

+

+ Import and manage UDM Pro device configurations. +

+
+
+ + + + + + + +
diff --git a/unifi_integration/views/udm_configuration_views.xml b/unifi_integration/views/udm_configuration_views.xml new file mode 100644 index 0000000..ab67f2e --- /dev/null +++ b/unifi_integration/views/udm_configuration_views.xml @@ -0,0 +1,138 @@ + + + + + udm.configuration.tree + udm.configuration + + + + + + + + + + + + + + + + udm.configuration.form + udm.configuration + +
+ +
+ + + + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + udm.configuration.search + udm.configuration + + + + + + + + + + + + + + +
diff --git a/unifi_integration/views/udm_dashboard_metric_views.xml b/unifi_integration/views/udm_dashboard_metric_views.xml new file mode 100644 index 0000000..35510d8 --- /dev/null +++ b/unifi_integration/views/udm_dashboard_metric_views.xml @@ -0,0 +1,79 @@ + + + + + udm.dashboard.metric.tree + udm.dashboard.metric + + + + + + + + + + + + + + + udm.dashboard.metric.form + udm.dashboard.metric + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + + udm.dashboard.metric.search + udm.dashboard.metric + + + + + + + + + + + + + + + + + + + Dashboard Metrics + udm.dashboard.metric + tree,form + +

+ No dashboard metrics found +

+

+ Dashboard metrics are automatically created and updated when you refresh site metrics. +

+
+
+
diff --git a/unifi_integration/views/udm_device_views.xml b/unifi_integration/views/udm_device_views.xml new file mode 100644 index 0000000..ff91e2d --- /dev/null +++ b/unifi_integration/views/udm_device_views.xml @@ -0,0 +1,37 @@ + + + + + udm.device.form + udm.device + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/unifi_integration/views/udm_firewall_views.xml b/unifi_integration/views/udm_firewall_views.xml new file mode 100644 index 0000000..697bf13 --- /dev/null +++ b/unifi_integration/views/udm_firewall_views.xml @@ -0,0 +1,41 @@ + + + + + udm.firewall.rule.form + udm.firewall.rule + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/unifi_integration/views/udm_menu_views.xml b/unifi_integration/views/udm_menu_views.xml new file mode 100644 index 0000000..0b33281 --- /dev/null +++ b/unifi_integration/views/udm_menu_views.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + Configurations + udm.configuration + tree,form + +

+ Create your first UDM Pro configuration +

+

+ You can import configurations directly from your UDM Pro device + or manually create them for documentation purposes. +

+
+
+ + + + + + + + + Networks + udm.network + tree,form + {'search_default_group_by_config_id': 1} + + + + + + + VLANs + udm.vlan + tree,form + {'search_default_group_by_config_id': 1} + + + + + + + + + + Devices + udm.device + tree,form + {'search_default_group_by_config_id': 1} + + + + + + + + + + Users + udm.user + tree,form + {'search_default_group_by_config_id': 1} + + + + + + + Firewall Rules + udm.firewall.rule + tree,form + {'search_default_group_by_config_id': 1} + + + + + + + + + + System Info + udm.system.info + tree,form + + + + + + + UDM Settings + udm.settings + tree,form + + + + + + + Import UDM Pro Configuration + /udm_pro/import_config + self + +
diff --git a/unifi_integration/views/udm_network_views.xml b/unifi_integration/views/udm_network_views.xml new file mode 100644 index 0000000..ab3229d --- /dev/null +++ b/unifi_integration/views/udm_network_views.xml @@ -0,0 +1,38 @@ + + + + + udm.network.form + udm.network + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/unifi_integration/views/udm_settings_views.xml b/unifi_integration/views/udm_settings_views.xml new file mode 100644 index 0000000..cf80f5e --- /dev/null +++ b/unifi_integration/views/udm_settings_views.xml @@ -0,0 +1,29 @@ + + + + + udm.settings.form + udm.settings + +
+ + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/unifi_integration/views/udm_site_views.xml b/unifi_integration/views/udm_site_views.xml new file mode 100644 index 0000000..db916a2 --- /dev/null +++ b/unifi_integration/views/udm_site_views.xml @@ -0,0 +1,281 @@ + + + + + udm.site.tree + udm.site + + + + + + + + + + + + + udm.site.form + udm.site + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + udm.site.dashboard + udm.site + primary + + +
+ +
+
+

Dashboard

+
+ Site ID: +
+
+
+
+
+ + +
+ +
+
+
+
CPU Usage
+ +
+
+ +

+ + +

+
+
+
+
+
+
+
+ + +
+
+
+
Memory Usage
+ +
+
+ +

+ + +

+
+
+
+
+
+
+
+ + +
+
+
+
Network Bandwidth
+ +
+
+ +

+ + +

+
Current Network Load
+
+
+
+ + +
+
+
+
WAN Status
+ +
+
+ + + +
Connected
+
Internet is available
+
+ + +
Disconnected
+
Internet is unavailable
+
+
+
+
+ + +
+
+
+
Device Status
+ +
+
+ +

+ + online +

+
Out of devices
+
+
+
+ + +
+
+
+
Security Threats
+ +
+
+ +

+ +

+
+ Threats Detected + No Threats +
+
+
+
+
+ + +
Last updated: + +
+
+
+
+
+ + + + udm.site.search + udm.site + + + + + + + + + + + + + UDM Sites + udm.site + tree,form,dashboard + {'search_default_active': 1} + +

+ Create a new UDM Pro Site +

+

+ Create sites to organize your UDM Pro configurations by location or purpose. +

+
+
+ + + + Dashboard + udm.site + dashboard,form + {'search_default_active': 1} + [('active', '=', True)] + +

+ No UDM Pro sites found +

+

+ Create a site and configure UDM Pro to view the dashboard. +

+
+
+
diff --git a/unifi_integration/views/udm_system_info_views.xml b/unifi_integration/views/udm_system_info_views.xml new file mode 100644 index 0000000..7a1d8fa --- /dev/null +++ b/unifi_integration/views/udm_system_info_views.xml @@ -0,0 +1,33 @@ + + + + + udm.system.info.form + udm.system.info + +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/unifi_integration/views/udm_user_views.xml b/unifi_integration/views/udm_user_views.xml new file mode 100644 index 0000000..4dcf053 --- /dev/null +++ b/unifi_integration/views/udm_user_views.xml @@ -0,0 +1,35 @@ + + + + + udm.user.form + udm.user + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + +
+
+
+
+
diff --git a/unifi_integration/views/udm_vlan_views.xml b/unifi_integration/views/udm_vlan_views.xml new file mode 100644 index 0000000..311bc3a --- /dev/null +++ b/unifi_integration/views/udm_vlan_views.xml @@ -0,0 +1,39 @@ + + + + + udm.vlan.form + udm.vlan + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+