From ccf211c55474b7ac26179a23400a02b55ca66a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20V=C3=A9zina?= Date: Thu, 27 Mar 2025 15:34:13 -0400 Subject: [PATCH] unifi get datas --- odoo_rsync_backup/__init__.py | 1 + odoo_rsync_backup/__manifest__.py | 20 + odoo_rsync_backup/data/backup_cron.xml | 14 + odoo_rsync_backup/models/__init__.py | 1 + odoo_rsync_backup/models/rsync_config.py | 185 + .../security/ir.model.access.csv | 2 + .../views/rsync_config_views.xml | 33 + unifi_integration/__manifest__.py | 2 + unifi_integration/models/__init__.py | 7 +- .../models/unifi_auth_session.py | 11 + unifi_integration/models/unifi_common.py | 44 + unifi_integration/models/unifi_device.py | 14 +- unifi_integration/models/unifi_dns.py | 53 + unifi_integration/models/unifi_firewall.py | 223 +- unifi_integration/models/unifi_network.py | 158 +- .../models/unifi_port_forward.py | 14 +- .../models/unifi_routing_config.py | 6 + unifi_integration/models/unifi_site.py | 3840 +++++++++++++++-- .../models/unifi_site_controller.py | 1700 -------- .../models/unifi_site_manager.py | 1343 ------ unifi_integration/models/unifi_sync_job.py | 3 +- unifi_integration/models/unifi_system_info.py | 14 +- unifi_integration/models/unifi_user.py | 14 +- unifi_integration/models/unifi_vlan.py | 14 +- unifi_integration/models/unifi_vpn.py | 241 ++ unifi_integration/models/unifi_wifi.py | 446 ++ .../security/ir.model.access.csv | 34 +- unifi_integration/security/unifi_security.xml | 25 + .../views/unifi_api_config_views.xml | 8 +- .../views/unifi_api_log_views.xml | 14 +- .../views/unifi_device_views.xml | 5 +- .../views/unifi_firewall_views.xml | 32 +- .../views/unifi_network_views.xml | 8 +- .../views/unifi_port_forward_views.xml | 33 +- unifi_integration/views/unifi_site_views.xml | 484 ++- .../views/unifi_site_views.xml.bak | 285 -- .../views/unifi_system_info_views.xml | 5 +- unifi_integration/views/unifi_user_views.xml | 7 +- unifi_integration/views/unifi_vlan_views.xml | 7 +- unifi_integration/views/unifi_vpn_views.xml | 113 + unifi_integration/views/unifi_wifi_views.xml | 152 + 41 files changed, 5601 insertions(+), 4014 deletions(-) create mode 100644 odoo_rsync_backup/__init__.py create mode 100644 odoo_rsync_backup/__manifest__.py create mode 100644 odoo_rsync_backup/data/backup_cron.xml create mode 100644 odoo_rsync_backup/models/__init__.py create mode 100644 odoo_rsync_backup/models/rsync_config.py create mode 100644 odoo_rsync_backup/security/ir.model.access.csv create mode 100644 odoo_rsync_backup/views/rsync_config_views.xml create mode 100644 unifi_integration/models/unifi_common.py delete mode 100644 unifi_integration/models/unifi_site_controller.py delete mode 100644 unifi_integration/models/unifi_site_manager.py create mode 100644 unifi_integration/models/unifi_vpn.py create mode 100644 unifi_integration/models/unifi_wifi.py delete mode 100644 unifi_integration/views/unifi_site_views.xml.bak create mode 100644 unifi_integration/views/unifi_vpn_views.xml create mode 100644 unifi_integration/views/unifi_wifi_views.xml diff --git a/odoo_rsync_backup/__init__.py b/odoo_rsync_backup/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/odoo_rsync_backup/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo_rsync_backup/__manifest__.py b/odoo_rsync_backup/__manifest__.py new file mode 100644 index 0000000..bb770a0 --- /dev/null +++ b/odoo_rsync_backup/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'Odoo Rsync Backup', + 'version': '18.0.1.0.0', + 'category': 'Administration/Backup', + 'summary': 'Rsync backup for filestore and configuration', + 'sequence': 1, + 'author': 'Bemade', + 'website': 'https://bemade.org', + 'license': 'LGPL-3', + 'depends': [ + 'base', + 'auto_database_backup' + ], + 'data': [ + 'views/rsync_config_views.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, +} diff --git a/odoo_rsync_backup/data/backup_cron.xml b/odoo_rsync_backup/data/backup_cron.xml new file mode 100644 index 0000000..ea8aeed --- /dev/null +++ b/odoo_rsync_backup/data/backup_cron.xml @@ -0,0 +1,14 @@ + + + + + Rsync Backup: Daily Backup + + code + model._perform_backup() + days + 1 + + + + diff --git a/odoo_rsync_backup/models/__init__.py b/odoo_rsync_backup/models/__init__.py new file mode 100644 index 0000000..002f788 --- /dev/null +++ b/odoo_rsync_backup/models/__init__.py @@ -0,0 +1 @@ +from . import rsync_config diff --git a/odoo_rsync_backup/models/rsync_config.py b/odoo_rsync_backup/models/rsync_config.py new file mode 100644 index 0000000..a8e9689 --- /dev/null +++ b/odoo_rsync_backup/models/rsync_config.py @@ -0,0 +1,185 @@ +import logging +import os +import subprocess + +from odoo import models, fields, tools, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class DbBackupConfigInherit(models.Model): + """ + Extend db.backup.configure to add rsync functionality + """ + _inherit = 'db.backup.configure' + + # Rsync configuration fields + enable_rsync = fields.Boolean( + string='RSync the filestore', + help='Enable rsync backup for filestore when using dump format' + ) + + remote_host = fields.Char(string='Remote Host') + remote_user = fields.Char(string='Remote User') + remote_port = fields.Integer(string='SSH Port', default=22) + ssh_key_path = fields.Char( + string='SSH Key Path', + default='/home/odoo/.ssh/id_rsa', + help='Path to the SSH private key file' + ) + filestore_dest_path = fields.Char( + string='Filestore Destination Path', + help='Remote path where filestore will be backed up' + ) + + + def _sync_filestore_with_rsync(self): + """ + Synchronize filestore directory using rsync after backup is completed + """ + if not (self.enable_rsync and self.backup_format == 'dump'): + return + + try: + # Get filestore path + filestore_path = os.path.join(tools.config['data_dir'], 'filestore', self.env.cr.dbname) + + # Build rsync command + filestore_cmd = [ + 'rsync', '-avz', '-e', + f'ssh -p {self.remote_port} -i {self.ssh_key_path}', + filestore_path + '/', # Add trailing slash to sync contents + f'{self.remote_user}@{self.remote_host}:{self.filestore_dest_path}' + ] + + # Execute rsync command + subprocess.run(filestore_cmd, check=True) + + _logger.info('Rsync backup completed successfully') + return True + + except subprocess.CalledProcessError as e: + _logger.error('Rsync backup failed: %s', str(e)) + raise UserError(_('Rsync backup failed. Check the logs for details.')) + except Exception as e: + _logger.error('Unexpected error during rsync backup: %s', str(e)) + raise UserError(_('Unexpected error during rsync backup. Check the logs for details.')) + + def _schedule_auto_backup(self, frequency): + """ + Override _schedule_auto_backup to add rsync synchronization after backup + """ + res = super(DbBackupConfigInherit, self)._schedule_auto_backup(frequency) + self._sync_filestore_with_rsync() + return res + + name = fields.Char( + string='Name', + required=True + ) + + remote_host = fields.Char( + string='Remote Host', + required=True + ) + + remote_user = fields.Char( + string='Remote User', + required=True + ) + + remote_port = fields.Integer( + string='SSH Port', + default=22 + ) + + ssh_key_path = fields.Char( + string='SSH Key Path', + default='/home/odoo/.ssh/id_rsa', + help='Path to the SSH private key file' + ) + + filestore_dest_path = fields.Char( + string='Filestore Destination Path', + required=True, + help='Remote path where filestore will be backed up' + ) + + config_dest_path = fields.Char( + string='Config Destination Path', + required=True, + help='Remote path where odoo.conf will be backed up' + ) + + active = fields.Boolean( + default=True + ) + + last_backup = fields.Datetime( + string='Last Backup', + readonly=True + ) + + def _run_rsync_command(self, source_path, dest_path): + """ + Execute rsync command with specified parameters + """ + try: + cmd = [ + 'rsync', + '-avz', + '-e', f'ssh -p {self.remote_port} -i {self.ssh_key_path}', + source_path, + f'{self.remote_user}@{self.remote_host}:{dest_path}' + ] + + process = subprocess.run( + cmd, + capture_output=True, + text=True, + check=True + ) + _logger.info(f'Rsync successful: {process.stdout}') + return True + except subprocess.CalledProcessError as e: + _logger.error(f'Rsync failed: {e.stderr}') + return False + + def action_backup_now(self): + """ + Manually trigger the backup process + """ + self.ensure_one() + self._perform_backup() + return True + + def _perform_backup(self): + """ + Perform the actual backup operation for filestore and config + """ + success = True + + # Get filestore path from Odoo config + filestore_path = os.path.join( + self.env['ir.config_parameter'].get_param('data_dir'), + 'filestore', + self.env.cr.dbname + ) + + # Get odoo.conf path + odoo_conf_path = os.environ.get('ODOO_RC', '/etc/odoo/odoo.conf') + + # Backup filestore + if os.path.exists(filestore_path): + if not self._run_rsync_command(filestore_path + '/', self.filestore_dest_path): + success = False + + # Backup odoo.conf + if os.path.exists(odoo_conf_path): + if not self._run_rsync_command(odoo_conf_path, self.config_dest_path): + success = False + + if success: + self.write({'last_backup': fields.Datetime.now()}) + + return success diff --git a/odoo_rsync_backup/security/ir.model.access.csv b/odoo_rsync_backup/security/ir.model.access.csv new file mode 100644 index 0000000..074146b --- /dev/null +++ b/odoo_rsync_backup/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_rsync_backup_config_admin,rsync.backup.config.admin,model_rsync_backup_config,base.group_system,1,1,1,1 diff --git a/odoo_rsync_backup/views/rsync_config_views.xml b/odoo_rsync_backup/views/rsync_config_views.xml new file mode 100644 index 0000000..80d7293 --- /dev/null +++ b/odoo_rsync_backup/views/rsync_config_views.xml @@ -0,0 +1,33 @@ + + + + + db.backup.configure.form.inherit.rsync + db.backup.configure + + + + + + + + + + + + + + diff --git a/unifi_integration/__manifest__.py b/unifi_integration/__manifest__.py index 4bc5f61..4fed25f 100644 --- a/unifi_integration/__manifest__.py +++ b/unifi_integration/__manifest__.py @@ -46,6 +46,8 @@ This module allows you to: 'views/unifi_dns_config_views.xml', 'views/unifi_routing_views.xml', 'views/unifi_routing_config_views.xml', + 'views/unifi_wifi_views.xml', + 'views/unifi_vpn_views.xml', 'views/unifi_api_config_views.xml', 'views/unifi_dashboard_views.xml', # Templates diff --git a/unifi_integration/models/__init__.py b/unifi_integration/models/__init__.py index b8c6e3d..a5df5dc 100644 --- a/unifi_integration/models/__init__.py +++ b/unifi_integration/models/__init__.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- # Import module files +# Common functionality +from . import unifi_common + # UniFi models from . import unifi_site -from . import unifi_site_controller -from . import unifi_site_manager from . import unifi_auth_session from . import unifi_mfa from . import unifi_api_config @@ -21,5 +22,7 @@ from . import unifi_dns from . import unifi_dns_config from . import unifi_routing from . import unifi_routing_config +from . import unifi_wifi +from . import unifi_vpn from . import unifi_dashboard_metric from . import unifi_dashboard_stat diff --git a/unifi_integration/models/unifi_auth_session.py b/unifi_integration/models/unifi_auth_session.py index 4ff6204..8ad07e4 100644 --- a/unifi_integration/models/unifi_auth_session.py +++ b/unifi_integration/models/unifi_auth_session.py @@ -53,6 +53,17 @@ class UnifiAuthSession(models.Model): help='Session cookie for Controller API' ) + endpoint = fields.Char( + string='Authentication Endpoint', + help='The endpoint used for authentication (e.g. /api/login or /api/auth/login)' + ) + + is_udm_pro = fields.Boolean( + string='Is UDM Pro', + default=False, + help='Indicates if this session is for a UDM Pro or UDM Pro SE device' + ) + expiry = fields.Datetime( string='Expiry Date', help='Date and time when this session expires' diff --git a/unifi_integration/models/unifi_common.py b/unifi_integration/models/unifi_common.py new file mode 100644 index 0000000..3f4264a --- /dev/null +++ b/unifi_integration/models/unifi_common.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +import json +import logging +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + +class UnifiCommonMixin(models.AbstractModel): + """Mixin for common functionality across UniFi models + + This mixin provides common fields and methods that are used by multiple + UniFi models, such as formatting raw JSON data. + """ + _name = 'unifi.common.mixin' + _description = 'UniFi Common Functionality Mixin' + + def format_raw_data_json(self, raw_data): + """Format raw JSON data by removing outer braces and adjusting indentation + + Args: + raw_data (str): Raw JSON data string + + Returns: + str: Formatted JSON string without outer braces + """ + if not raw_data: + return '' + + try: + # Load and format the JSON data + data = json.loads(raw_data) + formatted_json = json.dumps(data, indent=4) + + # Remove the first and last line (the braces) + lines = formatted_json.split('\n') + if len(lines) > 2: # Ensure there are at least 3 lines + # Remove the first and last line and adjust indentation + inner_content = '\n'.join(line[4:] for line in lines[1:-1]) + return inner_content + else: + return formatted_json + except (ValueError, json.JSONDecodeError): + return 'Invalid JSON' diff --git a/unifi_integration/models/unifi_device.py b/unifi_integration/models/unifi_device.py index 5068e56..c2b4717 100644 --- a/unifi_integration/models/unifi_device.py +++ b/unifi_integration/models/unifi_device.py @@ -4,6 +4,7 @@ # pylint: disable=import-error from odoo import models, fields, api from odoo.tools.translate import _ +from .unifi_common import UnifiCommonMixin # pylint: enable=import-error import logging @@ -12,7 +13,7 @@ from datetime import datetime, timedelta _logger = logging.getLogger(__name__) -class UnifiDevice(models.Model): +class UnifiDevice(models.Model, UnifiCommonMixin): """Modèle pour les appareils UniFi Ce modèle stocke les informations sur les appareils UniFi, tels que les points d'accès, @@ -114,6 +115,17 @@ class UnifiDevice(models.Model): help="Données JSON brutes de l'appareil provenant de l'API UniFi" ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de l\'appareil au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + active = fields.Boolean( string='Actif', default=True, diff --git a/unifi_integration/models/unifi_dns.py b/unifi_integration/models/unifi_dns.py index 4131f47..18223d6 100644 --- a/unifi_integration/models/unifi_dns.py +++ b/unifi_integration/models/unifi_dns.py @@ -133,6 +133,59 @@ class UnifiDns(models.Model): record.last_sync = fields.Datetime.now() + @api.model + def create_or_update_from_data(self, site, dns_data): + """Create or update DNS entry from UniFi API data + + Args: + site (unifi.site): Site record + dns_data (dict): DNS entry data from UniFi API + + Returns: + unifi.dns: Created or updated DNS entry record + """ + _logger.info("Creating or updating DNS entry from data: %s", dns_data) + + # Extract required fields + hostname = dns_data.get('hostname') + ip_address = dns_data.get('ip_address') + unifi_id = dns_data.get('id') or dns_data.get('_id') + + if not hostname or not ip_address: + _logger.warning("Missing required fields in DNS data: %s", dns_data) + return False + + # Search for existing DNS entry + domain = [ + ('site_id', '=', site.id), + '|', + ('hostname', '=', hostname), + ('unifi_id', '=', unifi_id) + ] + + existing = self.search(domain, limit=1) + + # Prepare values for create/write + vals = { + 'hostname': hostname, + 'ip_address': ip_address, + 'unifi_id': unifi_id, + 'description': dns_data.get('description', ''), + 'enabled': dns_data.get('enabled', True), + 'entry_type': dns_data.get('type', 'static'), + 'last_sync': fields.Datetime.now(), + 'raw_data': json.dumps(dns_data, indent=2) if dns_data else False + } + + if existing: + _logger.info("Updating existing DNS entry: %s", existing.hostname) + existing.write(vals) + return existing + else: + _logger.info("Creating new DNS entry: %s", hostname) + vals['site_id'] = site.id + return self.create(vals) + def push_to_unifi(self): """Pousse les modifications vers le système UniFi""" for record in self: diff --git a/unifi_integration/models/unifi_firewall.py b/unifi_integration/models/unifi_firewall.py index e1ac4de..f7d7401 100644 --- a/unifi_integration/models/unifi_firewall.py +++ b/unifi_integration/models/unifi_firewall.py @@ -78,16 +78,56 @@ class UnifiFirewallRule(models.Model): help='Protocole réseau auquel cette règle s\'applique' ) + source_type = fields.Selection( + selection=[ + ('address', 'Adresse IP'), + ('network', 'Réseau'), + ('object', 'Objet'), + ('any', 'N\'importe où') + ], + string='Type de source', + compute='_compute_source_type', + store=True, + help='Type de la source (adresse IP, réseau ou objet)' + ) + source = fields.Char( string='Source', help='Réseau source ou adresse IP au format CIDR' ) + formatted_source = fields.Char( + string='Source formatée', + compute='_compute_formatted_source', + store=True, + help='Affichage formaté de la source en fonction de son type' + ) + + destination_type = fields.Selection( + selection=[ + ('address', 'Adresse IP'), + ('network', 'Réseau'), + ('object', 'Objet'), + ('any', 'N\'importe où') + ], + string='Type de destination', + compute='_compute_destination_type', + store=True, + help='Type de la destination (adresse IP, réseau ou objet)' + ) + destination = fields.Char( string='Destination', help='Réseau de destination ou adresse IP au format CIDR' ) + formatted_destination = fields.Char( + string='Destination formatée', + compute='_compute_formatted_destination', + store=True, + help='Affichage formaté de la destination en fonction de son type' + ) + src_port = fields.Char( string='Port source', help='Numéro de port source ou plage (ex: 80 ou 1024-2048)' @@ -115,6 +155,34 @@ class UnifiFirewallRule(models.Model): help='Type de règle dans le système UniFi' ) + detailed_rule_type = fields.Selection( + selection=[ + ('internet-in-ipv4', 'Internet-in (IPv4)'), + ('internet-out-ipv4', 'Internet-out (IPv4)'), + ('internet-local-ipv4', 'Internet-local (IPv4)'), + ('lan-in-ipv4', 'LAN-in (IPv4)'), + ('lan-out-ipv4', 'LAN-out (IPv4)'), + ('lan-local-ipv4', 'LAN-local (IPv4)'), + ('guest-in-ipv4', 'Guest-in (IPv4)'), + ('guest-out-ipv4', 'Guest-out (IPv4)'), + ('guest-local-ipv4', 'Guest-local (IPv4)'), + ('internet-in-ipv6', 'Internet-in (IPv6)'), + ('internet-out-ipv6', 'Internet-out (IPv6)'), + ('internet-local-ipv6', 'Internet-local (IPv6)'), + ('lan-in-ipv6', 'LAN-in (IPv6)'), + ('lan-out-ipv6', 'LAN-out (IPv6)'), + ('lan-local-ipv6', 'LAN-local (IPv6)'), + ('guest-in-ipv6', 'Guest-in (IPv6)'), + ('guest-out-ipv6', 'Guest-out (IPv6)'), + ('guest-local-ipv6', 'Guest-local (IPv6)'), + ('other', 'Autre') + ], + string='Type détaillé', + compute='_compute_detailed_rule_type', + store=True, + help='Type détaillé de la règle de pare-feu (Internet-in, LAN-out, etc.)' + ) + rule_index = fields.Integer( string='Index de la règle', help='Position de la règle dans la liste des règles' @@ -144,13 +212,162 @@ class UnifiFirewallRule(models.Model): help='Données brutes de la règle de pare-feu au format JSON' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de la règle de pare-feu au format JSON' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + if record.raw_data: + try: + # Charger le JSON, puis le formater sans les accolades externes + data = json.loads(record.raw_data) + formatted_json = json.dumps(data, indent=4) + # Enlever la première et la dernière ligne (les accolades) + lines = formatted_json.split('\n') + if len(lines) > 2: # S'assurer qu'il y a au moins 3 lignes + # Enlever la première et la dernière ligne et ajuster l'indentation + inner_content = '\n'.join(line[4:] for line in lines[1:-1]) + record.raw_data_json = inner_content + else: + record.raw_data_json = formatted_json + except ValueError: + record.raw_data_json = 'Invalid JSON' + + # Champs calculés rule_summary = fields.Char( string='Résumé de la règle', compute='_compute_rule_summary' ) - @api.depends('action', 'protocol', 'source', 'destination', 'src_port', 'dst_port') + @api.depends('raw_data') + def _compute_detailed_rule_type(self): + """Détermine le type détaillé de la règle de pare-feu à partir des données brutes""" + for record in self: + if not record.raw_data: + record.detailed_rule_type = 'other' + continue + + try: + rule_data = json.loads(record.raw_data) + # Extraire les informations du type de règle + rule_interface = rule_data.get('ruleset', '') + direction = rule_data.get('direction', '') + ip_version = 'ipv4' if rule_data.get('ipv6', False) is False else 'ipv6' + + # Construire le type détaillé + if rule_interface and direction: + detailed_type = f"{rule_interface}-{direction}-{ip_version}" + if detailed_type in dict(self._fields['detailed_rule_type'].selection).keys(): + record.detailed_rule_type = detailed_type + else: + record.detailed_rule_type = 'other' + else: + record.detailed_rule_type = 'other' + except (json.JSONDecodeError, AttributeError): + record.detailed_rule_type = 'other' + + @api.depends('source', 'raw_data') + def _compute_source_type(self): + """Détermine le type de la source (adresse IP, réseau ou objet)""" + for record in self: + if not record.source: + record.source_type = 'any' + continue + + try: + rule_data = json.loads(record.raw_data) if record.raw_data else {} + src_type = rule_data.get('src_type', '') + + if src_type == 'network': + record.source_type = 'network' + elif src_type == 'object': + record.source_type = 'object' + elif record.source and '/' in record.source: + # Si contient un slash, c'est probablement un réseau CIDR + record.source_type = 'network' + elif record.source: + # Sinon, c'est probablement une adresse IP + record.source_type = 'address' + else: + record.source_type = 'any' + except (json.JSONDecodeError, AttributeError): + if record.source: + record.source_type = 'address' + else: + record.source_type = 'any' + + @api.depends('destination', 'raw_data') + def _compute_destination_type(self): + """Détermine le type de la destination (adresse IP, réseau ou objet)""" + for record in self: + if not record.destination: + record.destination_type = 'any' + continue + + try: + rule_data = json.loads(record.raw_data) if record.raw_data else {} + dst_type = rule_data.get('dst_type', '') + + if dst_type == 'network': + record.destination_type = 'network' + elif dst_type == 'object': + record.destination_type = 'object' + elif record.destination and '/' in record.destination: + # Si contient un slash, c'est probablement un réseau CIDR + record.destination_type = 'network' + elif record.destination: + # Sinon, c'est probablement une adresse IP + record.destination_type = 'address' + else: + record.destination_type = 'any' + except (json.JSONDecodeError, AttributeError): + if record.destination: + record.destination_type = 'address' + else: + record.destination_type = 'any' + + @api.depends('source', 'source_type', 'raw_data') + def _compute_formatted_source(self): + """Calcule l'affichage formaté de la source en fonction de son type""" + for record in self: + if record.source_type == 'any' or not record.source: + record.formatted_source = "N'importe où" + elif record.source_type == 'object': + try: + rule_data = json.loads(record.raw_data) if record.raw_data else {} + object_name = rule_data.get('src_object_name', record.source) + record.formatted_source = f"Objet: {object_name}" + except (json.JSONDecodeError, AttributeError): + record.formatted_source = record.source + elif record.source_type == 'network': + record.formatted_source = f"Réseau: {record.source}" + else: + record.formatted_source = f"IP: {record.source}" + + @api.depends('destination', 'destination_type', 'raw_data') + def _compute_formatted_destination(self): + """Calcule l'affichage formaté de la destination en fonction de son type""" + for record in self: + if record.destination_type == 'any' or not record.destination: + record.formatted_destination = "N'importe où" + elif record.destination_type == 'object': + try: + rule_data = json.loads(record.raw_data) if record.raw_data else {} + object_name = rule_data.get('dst_object_name', record.destination) + record.formatted_destination = f"Objet: {object_name}" + except (json.JSONDecodeError, AttributeError): + record.formatted_destination = record.destination + elif record.destination_type == 'network': + record.formatted_destination = f"Réseau: {record.destination}" + else: + record.formatted_destination = f"IP: {record.destination}" + + @api.depends('action', 'protocol', 'formatted_source', 'formatted_destination', 'src_port', 'dst_port') def _compute_rule_summary(self): """Calcule un résumé lisible de la règle de pare-feu @@ -173,8 +390,8 @@ class UnifiFirewallRule(models.Model): if record.protocol: parts.append(record.protocol.upper()) - src_addr = record.source or 'n\'importe où' - dst_addr = record.destination or 'n\'importe où' + src_addr = record.formatted_source or "N'importe où" + dst_addr = record.formatted_destination or "N'importe où" src = f"de {src_addr}" if record.src_port: diff --git a/unifi_integration/models/unifi_network.py b/unifi_integration/models/unifi_network.py index b82f26d..7319b3a 100644 --- a/unifi_integration/models/unifi_network.py +++ b/unifi_integration/models/unifi_network.py @@ -3,6 +3,7 @@ # These imports will work in an Odoo environment, even if your IDE marks them as not found # pylint: disable=import-error from odoo import models, fields, api, _ +from .unifi_common import UnifiCommonMixin from odoo.exceptions import UserError, ValidationError # pylint: enable=import-error @@ -12,7 +13,7 @@ from datetime import datetime _logger = logging.getLogger(__name__) -class UnifiNetwork(models.Model): +class UnifiNetwork(models.Model, UnifiCommonMixin): """Modèle pour gérer les réseaux UniFi Ce modèle représente les réseaux configurés dans les sites UniFi, @@ -43,6 +44,8 @@ class UnifiNetwork(models.Model): ('wan', 'WAN'), ('lan', 'LAN'), ('vpn', 'VPN'), + ('site-vpn', 'Site VPN'), + ('remote-user-vpn', 'Remote User VPN'), ('vlan-only', 'VLAN Only'), ('other', 'Autre') ], @@ -135,6 +138,17 @@ class UnifiNetwork(models.Model): help='Données brutes du réseau au format JSON' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes du réseau au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + # Relations site_id = fields.Many2one( comodel_name='unifi.site', @@ -243,25 +257,149 @@ class UnifiNetwork(models.Model): # Création d'un nouveau réseau return self.create(vals) - def sync_networks(self, site): + def sync_networks(self): """Synchronise les réseaux depuis l'API UniFi - Args: - site: L'enregistrement du site UniFi - + Cette méthode est conçue pour être appelée depuis un bouton dans l'interface utilisateur. + Elle utilise le site associé à l'enregistrement en cours. + Returns: bool: True si la synchronisation a réussi, False sinon """ self.ensure_one() + # Utiliser le site associé à ce réseau + site = self.site_id + if not site: + _logger.error("Impossible de synchroniser: aucun site associé à ce réseau") + return False + # Déterminer quelle méthode utiliser en fonction du type d'API if site.api_type == 'controller': return self._sync_networks_controller(site) elif site.api_type == 'site_manager': return self._sync_networks_site_manager(site) + + def sync_networks_from_list(self): + """Synchronise tous les réseaux du site actuel depuis la vue liste + + Cette méthode vérifie si tous les réseaux affichés appartiennent au même site, + puis appelle la méthode de synchronisation appropriée. + + Returns: + dict: Action de rafraîchissement de la vue ou notification d'erreur + """ + # Vérifier si tous les réseaux appartiennent au même site + sites = self.mapped('site_id') + + if not sites: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Erreur'), + 'message': _('Aucun site associé aux réseaux sélectionnés.'), + 'sticky': False, + 'type': 'danger', + } + } + + if len(sites) > 1: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Erreur'), + 'message': _('Les réseaux sélectionnés appartiennent à différents sites. Veuillez filtrer par site.'), + 'sticky': False, + 'type': 'danger', + } + } + + # Tous les réseaux appartiennent au même site, appeler la méthode de synchronisation + site = sites[0] + + # Déterminer quelle méthode utiliser en fonction du type d'API + try: + if site.api_type == 'controller': + self._sync_networks_controller(site) + elif site.api_type == 'site_manager': + self._sync_networks_site_manager(site) + else: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Erreur'), + 'message': _('Type d\'API non pris en charge.'), + 'sticky': False, + 'type': 'danger', + } + } + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Succès'), + 'message': _('Synchronisation des réseaux terminée avec succès.'), + 'sticky': False, + 'type': 'success', + } + } + except Exception as e: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Erreur'), + 'message': _(f'Erreur lors de la synchronisation: {str(e)}'), + 'sticky': False, + 'type': 'danger', + } + } + + @api.model + def sync_site_networks(self, site_id): + """Synchronise tous les réseaux d'un site spécifique + + Cette méthode est conçue pour être appelée depuis un bouton dans la vue liste + lorsque tous les réseaux affichés appartiennent au même site. + + Args: + site_id: ID du site dont les réseaux doivent être synchronisés + + Returns: + dict: Action de rafraîchissement de la vue + """ + # Récupérer le site + site = self.env['unifi.site'].browse(site_id) + if not site.exists(): + raise UserError(_("Le site sélectionné n'existe pas.")) + + # Déterminer quelle méthode utiliser en fonction du type d'API + if site.api_type == 'controller': + self._sync_networks_controller(site) + elif site.api_type == 'site_manager': + self._sync_networks_site_manager(site) else: _logger.error(f"Type d'API non pris en charge: {site.api_type}") - return False + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Erreur'), + 'message': _('Type d\'API non pris en charge.'), + 'sticky': False, + 'type': 'danger', + } + } + + # Retourner une action pour rafraîchir la vue + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } def _sync_networks_controller(self, site): """Synchronise les réseaux depuis l'API Controller @@ -272,8 +410,8 @@ class UnifiNetwork(models.Model): Returns: bool: True si la synchronisation a réussi, False sinon """ - # Obtenir les données des réseaux depuis l'API Controller - networks_data = self.env['unifi.site.controller'].get_network_data(site) + # Obtenir les données des réseaux directement depuis le site + networks_data = site._get_controller_network_data() if not networks_data: return False @@ -292,8 +430,8 @@ class UnifiNetwork(models.Model): Returns: bool: True si la synchronisation a réussi, False sinon """ - # Obtenir les données des réseaux depuis l'API Site Manager - networks_data = self.env['unifi.site.manager'].get_network_data(site) + # Obtenir les données des réseaux directement depuis le site + networks_data = site._get_site_manager_network_data() if not networks_data: return False diff --git a/unifi_integration/models/unifi_port_forward.py b/unifi_integration/models/unifi_port_forward.py index 16ec9c2..295556f 100644 --- a/unifi_integration/models/unifi_port_forward.py +++ b/unifi_integration/models/unifi_port_forward.py @@ -4,10 +4,11 @@ import json import logging from odoo import models, fields, api +from .unifi_common import UnifiCommonMixin _logger = logging.getLogger(__name__) -class UnifiPortForward(models.Model): +class UnifiPortForward(models.Model, UnifiCommonMixin): """Représente une règle de redirection de port dans le système UniFi Ce modèle stocke les règles de redirection de port qui permettent un accès externe @@ -122,6 +123,17 @@ class UnifiPortForward(models.Model): help='Données brutes de la redirection de port au format JSON' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de la redirection de port au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + # Champs calculés rule_summary = fields.Char( string='Résumé de la règle', diff --git a/unifi_integration/models/unifi_routing_config.py b/unifi_integration/models/unifi_routing_config.py index deb37a8..5628edf 100644 --- a/unifi_integration/models/unifi_routing_config.py +++ b/unifi_integration/models/unifi_routing_config.py @@ -27,6 +27,12 @@ class UnifiRoutingConfig(models.Model): help='Site this routing configuration belongs to' ) + name = fields.Char( + string='Name', + required=True, + help='Name of the routing configuration' + ) + ospf_enabled = fields.Boolean( string='OSPF Enabled', default=False, diff --git a/unifi_integration/models/unifi_site.py b/unifi_integration/models/unifi_site.py index 77a8370..fd39031 100644 --- a/unifi_integration/models/unifi_site.py +++ b/unifi_integration/models/unifi_site.py @@ -1,41 +1,42 @@ # -*- coding: utf-8 -*- -from odoo import _, api, fields, models - -# TODO: Refactorisation du modèle UnifiSite -# Changements effectués: -# 1. Division du modèle UnifiSite en trois fichiers: -# - unifi_site.py: contient les champs et méthodes communs aux deux types d'API -# - unifi_site_controller.py: contient les champs et méthodes spécifiques à l'API Controller -# - unifi_site_manager.py: contient les champs et méthodes spécifiques à l'API Site Manager -# 2. Implémentation de méthodes de délégation dans le modèle principal pour: -# - Validation des champs requis selon le type d'API (_check_api_fields) -# - Nettoyage des champs non pertinents lors du changement de type d'API (_onchange_api_type) -# 3. Mise à jour des imports dans __init__.py pour inclure les nouveaux modèles - # These imports will work in an Odoo environment, even if your IDE marks them as not found # pylint: disable=import-error from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError +from .unifi_common import UnifiCommonMixin # pylint: enable=import-error import json import logging +import requests +import urllib3 +import tempfile +import os +import base64 +import ast from datetime import datetime, timedelta +from typing import Dict, Tuple, List, Any +from requests.exceptions import RequestException, ConnectionError _logger = logging.getLogger(__name__) -class UnifiSite(models.Model): +# TODO: Refactorisation du modèle UnifiSite +# Changements effectués: +# 1. Fusion de trois fichiers en un seul: +# - unifi_site.py, unifi_site_controller.py et unifi_site_manager.py +# 2. Intégration directe des champs et méthodes spécifiques dans le modèle principal +# 3. Simplification des relations et de la validation + +class UnifiSite(models.Model, UnifiCommonMixin): """Represents a UniFi site managed by one or more UniFi devices This model is the central entity that groups all UniFi configurations and devices. Each site can have multiple devices, networks, and users. It supports both the Site Manager API (cloud) and the Controller API (local). - The implementation is split across three files: - - unifi_site.py: Common fields and methods - - unifi_site_controller.py: Controller API specific functionality - - unifi_site_manager.py: Site Manager API specific functionality + All functionality is now integrated in a single model for simplicity and maintainability. + The API type determines which fields and methods are applicable. """ _name = 'unifi.site' _description = 'UniFi Site' @@ -92,7 +93,37 @@ class UnifiSite(models.Model): help='Configuration API utilisée pour ce site' ) - # Note: Controller-specific fields moved to unifi_site_controller.py + # Performance and synchronization settings + timeout = fields.Float( + string='Timeout', + default=10.0, + help='API request timeout in seconds' + ) + + max_retries = fields.Integer( + string='Max Retries', + default=3, + help='Maximum number of retries for API requests' + ) + + auto_sync = fields.Boolean( + string='Automatic Synchronization', + default=False, + help='Enable automatic synchronization of this site' + ) + + sync_interval = fields.Integer( + string='Sync Interval', + default=60, + help='Interval in minutes between automatic synchronizations' + ) + + # SSL verification - Common for both API types + verify_ssl = fields.Boolean( + string='Verify SSL', + default=True, + help='Enable SSL certificate verification', + ) # Connection information - Common fields timestamp = fields.Datetime( @@ -102,12 +133,45 @@ class UnifiSite(models.Model): help='Date and time when this site was created' ) + last_sync = fields.Datetime( + string='Last Sync', + readonly=True, + help='Date and time when this site was last synchronized' + ) + + last_update = fields.Datetime( + string='Last Update', + readonly=True, + help='Date and time of the last successful synchronization' + ) + last_import_date = fields.Datetime( string='Last Import Date', readonly=True, help='Date and time of the last successful configuration import' ) + last_response_headers = fields.Text( + string='Last Response Headers', + readonly=True, + copy=False, + help='Headers from the last API response' + ) + + last_response_content = fields.Text( + string='Last Response Content', + readonly=True, + copy=False, + help='Content from the last API response' + ) + + last_successful_endpoint = fields.Char( + string='Last Successful Endpoint', + readonly=True, + copy=False, + help='The last endpoint that was successfully used for authentication' + ) + import_status = fields.Selection( selection=[ ('success', 'Success'), @@ -123,213 +187,1328 @@ class UnifiSite(models.Model): verify_ssl = fields.Boolean( string='Verify SSL', default=False, - help='Enable SSL certificate verification', - compute='_compute_connection_fields', - inverse='_inverse_verify_ssl' + help='Enable SSL certificate verification' ) - # Relations avec les modèles spécifiques d'API - controller_id = fields.One2many( - comodel_name='unifi.site.controller', - inverse_name='site_id', - string='Controller API', - help='Configuration de l\'API Controller pour ce site' - ) - - manager_id = fields.One2many( - comodel_name='unifi.site.manager', - inverse_name='site_id', - string='Site Manager API', - help='Configuration de l\'API Site Manager pour ce site' - ) - - # Related fields for Controller API connection information + # Controller API specific fields host = fields.Char( string='Host', - help='IP address or hostname of the controller', - compute='_compute_connection_fields', - inverse='_inverse_host' + help='IP address or hostname of the controller' ) port = fields.Integer( string='Port', - help='Port number (default: 443)', - compute='_compute_connection_fields', - inverse='_inverse_port' + default=443, + help='Port number (default: 443)' ) username = fields.Char( string='Username', - help='Username for controller login', - compute='_compute_connection_fields', - inverse='_inverse_username' + help='Username for controller login' ) password = fields.Char( string='Password', - help='Password for controller login', - compute='_compute_connection_fields', - inverse='_inverse_password' + help='Password for controller login' ) - # Related fields for Site Manager API connection information + ssl_cert_file = fields.Binary( + string='Certificat SSL personnalisé', + attachment=True, + help='Fichier de certificat SSL personnalisé (.pem ou .crt)' + ) + + ssl_cert_filename = fields.Char( + string='Nom du fichier de certificat' + ) + + ssl_cert_path = fields.Char( + string='Chemin du certificat', + compute='_compute_ssl_cert_path', + store=True, + help='Chemin vers le fichier de certificat SSL' + ) + + # Site Manager API specific fields api_key = fields.Char( string='API Key', - help='API Key for Site Manager authentication', - compute='_compute_connection_fields', - inverse='_inverse_api_key' + help='API Key for Site Manager authentication' ) mfa_enabled = fields.Boolean( string='MFA Enabled', - help='Enable Multi-Factor Authentication', - compute='_compute_connection_fields', - inverse='_inverse_mfa_enabled' + default=False, + help='Enable Multi-Factor Authentication' ) mfa_token = fields.Char( string='MFA Token', - help='Multi-Factor Authentication token', - compute='_compute_connection_fields', - inverse='_inverse_mfa_token' + help='Multi-Factor Authentication token' ) - # Note: Controller-specific fields moved to unifi_site_controller.py - # Note: Site Manager-specific fields moved to unifi_site_manager.py + # Authentication fields + auth_session_id = fields.Many2one( + comodel_name='unifi.auth.session', + string='Session d\'authentification', + ondelete='set null', + help='Session d\'authentification active pour ce site' + ) - @api.depends('api_type', 'controller_id', 'manager_id') + # Relations avec d'autres modèles + device_ids = fields.One2many( + comodel_name='unifi.device', + inverse_name='site_id', + string='Devices', + help='Devices in this site' + ) + + device_count = fields.Integer( + string='Device Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of devices in this site' + ) + + network_ids = fields.One2many( + comodel_name='unifi.network', + inverse_name='site_id', + string='Networks', + help='Networks in this site' + ) + + network_count = fields.Integer( + string='Network Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of networks in this site' + ) + + user_ids = fields.One2many( + comodel_name='unifi.user', + inverse_name='site_id', + string='Users', + help='Users in this site' + ) + + user_count = fields.Integer( + string='User Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of users in this site' + ) + + # Relations pour les VLANs + vlan_ids = fields.One2many( + comodel_name='unifi.vlan', + inverse_name='site_id', + string='VLANs', + help='VLANs in this site' + ) + + vlan_count = fields.Integer( + string='VLAN Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of VLANs in this site' + ) + + # Relations pour les règles de pare-feu + firewall_rule_ids = fields.One2many( + comodel_name='unifi.firewall.rule', + inverse_name='site_id', + string='Firewall Rules', + help='Firewall rules in this site' + ) + + firewall_rule_count = fields.Integer( + string='Firewall Rule Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of firewall rules in this site' + ) + + # Relations pour les redirections de port + port_forward_ids = fields.One2many( + comodel_name='unifi.port.forward', + inverse_name='site_id', + string='Port Forwards', + help='Port forwarding rules in this site' + ) + + port_forward_count = fields.Integer( + string='Port Forward Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of port forwarding rules in this site' + ) + + # Relations pour les configurations de routage + routing_config_ids = fields.One2many( + comodel_name='unifi.routing.config', + inverse_name='site_id', + string='Routing Configurations', + help='Routing configurations in this site' + ) + + routing_config_count = fields.Integer( + string='Routing Config Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of routing configurations in this site' + ) + + # Relations pour les WiFi + wifi_ids = fields.One2many( + comodel_name='unifi.wifi', + inverse_name='site_id', + string='WiFi Networks', + help='WiFi networks in this site' + ) + + wifi_count = fields.Integer( + string='WiFi Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of WiFi networks in this site' + ) + + # Relations pour DNS + dns_ids = fields.One2many( + comodel_name='unifi.dns', + inverse_name='site_id', + string='DNS Entries', + help='DNS entries in this site' + ) + + dns_count = fields.Integer( + string='DNS Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of DNS entries in this site' + ) + + # Relations pour System Info + system_info_ids = fields.One2many( + comodel_name='unifi.system.info', + inverse_name='site_id', + string='System Info', + help='System information for this site' + ) + + system_info_count = fields.Integer( + string='System Info Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of system info entries in this site' + ) + + # Relations pour VPN + vpn_ids = fields.One2many( + comodel_name='unifi.vpn', + inverse_name='site_id', + string='VPN Configurations', + help='VPN configurations in this site' + ) + + vpn_count = fields.Integer( + string='VPN Count', + compute='_compute_counts', + compute_sudo=True, + store=True, + help='Number of VPN configurations in this site' + ) + + # Relations pour les logs API et les jobs de synchronisation + api_log_ids = fields.One2many( + comodel_name='unifi.api.log', + inverse_name='site_id', + string='API Logs', + help='API logs for this site' + ) + + sync_job_ids = fields.One2many( + comodel_name='unifi.sync.job', + inverse_name='site_id', + string='Sync Jobs', + help='Synchronization jobs for this site' + ) + + # Note: La méthode _compute_counts est définie plus bas dans le fichier + + @api.depends('ssl_cert_file', 'ssl_cert_filename') def _compute_connection_fields(self): - """Calcule les valeurs des champs de connexion en fonction du type d'API""" - for site in self: - # Réinitialiser tous les champs - site.host = False - site.port = False - site.username = False - site.password = False - site.api_key = False - site.mfa_enabled = False - site.mfa_token = False - site.verify_ssl = False + """Compute connection fields based on API configuration + + This method sets connection fields (like verify_ssl, host, port, etc.) + based on the selected API configuration. + """ + for record in self: + # Utiliser la valeur par défaut si aucune configuration n'est définie + if not record.api_config_id: + # Garder les valeurs actuelles ou utiliser les valeurs par défaut + if not hasattr(record, 'verify_ssl') or record.verify_ssl is None: + record.verify_ssl = False + continue + + # Si une configuration API est définie, utiliser ses valeurs + if record.api_config_id.api_type == record.api_type: + # Copier les champs de connexion de la configuration API + record.verify_ssl = record.api_config_id.verify_ssl + + # Définir les champs spécifiques au type d'API + if record.api_type == 'controller': + # Extraire l'hôte et le port de l'URL de base + from urllib.parse import urlparse + parsed_url = urlparse(record.api_config_id.base_url) + record.host = parsed_url.netloc.split(':')[0] if ':' in parsed_url.netloc else parsed_url.netloc + record.port = parsed_url.port or 443 + record.username = record.api_config_id.username + record.password = record.api_config_id.password + + elif record.api_type == 'site_manager': + record.api_key = record.api_config_id.token + + #---------------------------------------------------------- + # Méthodes pour la récupération des données (remplacent les méthodes du controller) + #---------------------------------------------------------- + + def _get_system_info_data(self): + """Récupère les informations système du site + + Cette méthode remplace l'ancienne méthode get_system_info_data du controller. + + Returns: + dict: Données d'information système ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des informations système + return {} + + def _get_device_data(self): + """Récupère les données des appareils du site + + Cette méthode remplace l'ancienne méthode get_device_data du controller. + + Returns: + dict: Données des appareils ou False en cas d'échec + """ + self.ensure_one() + _logger.info("=== DÉBUT DE LA RÉCUPÉRATION DES APPAREILS ===") + + # Vérifier que nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les appareils") + return False - # Récupérer les valeurs en fonction du type d'API - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - site.host = controller.host - site.port = controller.port - site.username = controller.username - site.password = controller.password - site.verify_ssl = controller.verify_ssl - elif site.api_type == 'site_manager': - manager = self.env['unifi.site.manager'].search([('site_id', '=', site.id)], limit=1) - if manager: - site.api_key = manager.api_key - site.mfa_enabled = manager.mfa_enabled - site.mfa_token = manager.mfa_token - site.verify_ssl = manager.verify_ssl + try: + # Construire l'URL pour récupérer les appareils + base_url = f"https://{self.host}:{self.port}" + + # Essayer de se reconnecter pour obtenir des cookies frais + _logger.info("Tentative de reconnexion pour obtenir des cookies frais") + connection_result = self._test_controller_connection() + if connection_result.get('status') != 'success': + _logger.error(f"Impossible de se reconnecter: {connection_result.get('message')}") + return False + _logger.info("Reconnexion réussie, poursuite de la récupération des appareils") + + # Déterminer si nous avons affaire à un UDM Pro/UCG Max ou un contrôleur standard + # Si nous nous sommes connectés avec /api/auth/login, c'est probablement un UDM Pro/UCG Max + # Ou si la réponse contient 'UDM Pro' ou 'Dream Machine' dans les headers ou le contenu + is_udm_pro = False + + # Vérifier si l'endpoint d'authentification est celui d'un UDM Pro + if self.auth_session_id and self.auth_session_id.endpoint and '/api/auth/login' in self.auth_session_id.endpoint: + is_udm_pro = True + + # Vérifier si le dernier contenu de réponse contient des indices d'un UDM Pro + if self.last_response_content and ('UDMPRO' in self.last_response_content or 'Dream Machine' in self.last_response_content): + is_udm_pro = True + + _logger.info(f"Type de contrôleur détecté: {'UDM Pro/UCG Max' if is_udm_pro else 'Controller standard'}") + + # Essayer différents site_id et endpoints + site_ids = [ + self.site_id, # Utiliser le site_id configuré + "default", # Valeur par défaut souvent utilisée + "" # Essayer sans site_id + ] + + # Préparer la vérification SSL + verify = self.verify_ssl + if verify and self.ssl_cert_path: + verify = self.ssl_cert_path + + # Essayer chaque combinaison de site_id et endpoint + response = None + success = False + + for site_id in site_ids: + # Construire les endpoints avec le site_id actuel + device_endpoints = [] + + # Forcer is_udm_pro à True car nous avons réussi à nous connecter avec /api/auth/login + # Ce qui est caractéristique d'un UDM Pro + if self.last_successful_endpoint and '/api/auth/login' in self.last_successful_endpoint: + is_udm_pro = True + _logger.info("Détection forcée d'un UDM Pro basée sur l'endpoint d'authentification utilisé") + + # Vérifier si les cookies contiennent un TOKEN, ce qui est caractéristique d'un UDM Pro + cookies = self._get_auth_cookies() + if cookies and 'TOKEN' in cookies: + is_udm_pro = True + _logger.info("Détection forcée d'un UDM Pro basée sur la présence d'un TOKEN dans les cookies") + + if site_id: + if is_udm_pro: + # Pour UDM Pro/UCG Max, tous les endpoints doivent être préfixés avec /proxy/network + device_endpoints = [ + f"/proxy/network/api/s/{site_id}/stat/device", # Endpoint pour UDM Pro/UCG Max + f"/proxy/network/v2/api/site/{site_id}/device", # Autre endpoint possible pour UDM Pro + f"/proxy/network/api/site/{site_id}/stat/device", # Autre endpoint possible + f"/api/s/{site_id}/stat/device", # Essayer aussi sans le préfixe + ] + else: + # Pour les contrôleurs standard + device_endpoints = [ + f"/api/s/{site_id}/stat/device", # Endpoint standard + f"/api/site/{site_id}/stat/device", # Endpoint alternatif + f"/v2/api/site/{site_id}/device" # Endpoint pour les versions plus récentes + ] + else: + # Endpoints sans site_id + if is_udm_pro: + device_endpoints = [ + "/proxy/network/api/stat/device", # Pour UDM Pro/UCG Max + "/proxy/network/api/s/default/stat/device", # Essai avec 'default' + "/proxy/network/v2/api/site/default/device", # Autre endpoint possible pour UDM Pro + "/api/stat/device" # Essai sans le préfixe + ] + else: + device_endpoints = [ + "/api/stat/device", # Essai sans site_id + "/v2/api/device" # Autre essai sans site_id + ] + + for endpoint in device_endpoints: + try: + current_url = f"{base_url}{endpoint}" + _logger.info(f"Essai avec l'endpoint: {endpoint} (site_id: {site_id or 'aucun'})") + + # Récupérer les cookies de la session d'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + continue + + # Afficher les cookies pour débogage + _logger.info(f"Cookies utilisés: {cookies}") + + # Construire les arguments de la requête + kwargs = { + "cookies": cookies, + "verify": verify, + "timeout": self.timeout + } + + # Ajouter des headers spécifiques + # Extraire le token pour l'authentification + token = cookies.get('TOKEN', '') + _logger.info(f"Token utilisé pour l'authentification: {token[:20]}..." if token else "Pas de token disponible") + + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Csrf-Token": cookies.get('csrf_token', ''), # Essayer d'ajouter un token CSRF + "User-Agent": "Mozilla/5.0 Odoo UniFi Integration", # Ajouter un User-Agent + "Authorization": f"Bearer {token}" # Ajouter le token d'autorisation pour UDM Pro + } + kwargs["headers"] = headers + + # Essayer avec une session requests pour maintenir les cookies + session = requests.Session() + for key, value in cookies.items(): + session.cookies.set(key, value) + + response = session.get(current_url, **kwargs) + _logger.info(f"Réponse reçue: Status code {response.status_code}") + + # Stocker les headers de la réponse pour une utilisation ultérieure + self.write({'last_response_headers': str(dict(response.headers))}) + + # Afficher le contenu exact de la réponse pour débogage + _logger.info(f"Contenu brut de la réponse: '{response.text}'") + _logger.info(f"Longueur de la réponse: {len(response.text)} caractères") + _logger.info(f"Headers de la réponse: {dict(response.headers)}") + + # Vérifier si la réponse est du JSON valide + try: + json_data = response.json() + if response.status_code == 200: + _logger.info(f"Récupération réussie avec l'endpoint {endpoint}") + success = True + break + except ValueError: + _logger.warning(f"La réponse n'est pas du JSON valide: {response.text[:100]}...") + except Exception as e: + _logger.warning(f"Échec avec l'endpoint {endpoint}: {str(e)}") + + if success: + break + + if not response or not success: + _logger.error("Tous les endpoints ont échoué pour la récupération des appareils") + return False + + # Analyser la réponse JSON + try: + # Vérifier si la réponse est vide + if not response.text.strip(): + _logger.error("La réponse est vide") + return False + + # Essayer d'analyser la réponse JSON + data = response.json() + _logger.info(f"Données reçues: {len(data.get('data', []))} appareils") + _logger.info(f"Structure des données: {list(data.keys())}") + return data + except ValueError as e: + _logger.error(f"Erreur lors de l'analyse de la réponse JSON: {str(e)}") + + # Essayer de comprendre pourquoi l'analyse JSON a échoué + if response.text.strip().startswith('<'): + _logger.error("La réponse semble être du HTML ou du XML, pas du JSON") + elif response.text.strip() == '': + _logger.error("La réponse est vide") + else: + _logger.error(f"Les 100 premiers caractères de la réponse: {repr(response.text[:100])}") + + return False + + except Exception as e: + _logger.error(f"Erreur lors de la récupération des appareils: {str(e)}") + _logger.exception("Détails de l'erreur:") + return False + finally: + _logger.info("=== FIN DE LA RÉCUPÉRATION DES APPAREILS ===") - def _inverse_host(self): - """Inverse pour le champ host""" - for site in self: - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - controller.host = site.host - elif site.host: # Créer un nouveau controller si nécessaire - self.env['unifi.site.controller'].create({ - 'site_id': site.id, - 'host': site.host, - }) + def _get_network_data(self): + """Récupère les données des réseaux du site + + Cette méthode remplace l'ancienne méthode get_network_data du controller. + + Returns: + dict: Données des réseaux ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des données des réseaux + return {} - def _inverse_port(self): - """Inverse pour le champ port""" - for site in self: - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - controller.port = site.port - elif site.port: # Créer un nouveau controller si nécessaire - self.env['unifi.site.controller'].create({ - 'site_id': site.id, - 'port': site.port, - }) + def _get_vlan_data(self): + """Récupère les données des VLANs du site + + Cette méthode remplace l'ancienne méthode get_vlan_data du controller. + + Returns: + dict: Données des VLANs ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des données des VLANs + return {} - def _inverse_username(self): - """Inverse pour le champ username""" - for site in self: - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - controller.username = site.username - elif site.username: # Créer un nouveau controller si nécessaire - self.env['unifi.site.controller'].create({ - 'site_id': site.id, - 'username': site.username, - }) + def _get_user_data(self): + """Récupère les données des utilisateurs du site + + Cette méthode remplace l'ancienne méthode get_user_data du controller. + + Returns: + dict: Données des utilisateurs ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des données des utilisateurs + return {} - def _inverse_password(self): - """Inverse pour le champ password""" - for site in self: - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - controller.password = site.password - elif site.password: # Créer un nouveau controller si nécessaire - self.env['unifi.site.controller'].create({ - 'site_id': site.id, - 'password': site.password, - }) + def _get_firewall_data(self): + """Récupère les données du pare-feu du site + + Cette méthode remplace l'ancienne méthode get_firewall_data du controller. + + Returns: + dict: Données du pare-feu ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des données du pare-feu + return {} + def _get_port_forward_data(self): + """Récupère les données de redirection de port du site + + Cette méthode remplace l'ancienne méthode get_port_forward_data du controller. + + Returns: + dict: Données de redirection de port ou False en cas d'échec + """ + self.ensure_one() + # TODO: Implémenter la récupération des données de redirection de port + return {} + + # Note: La méthode _inverse_verify_ssl a été supprimée car elle n'est plus nécessaire + # puisque le champ verify_ssl n'est plus un champ calculé + + def _compute_ssl_cert_path(self): + """Calcule le chemin vers le fichier de certificat SSL + + Cette méthode est appelée lorsque le fichier de certificat SSL est modifié. + Elle sauvegarde le certificat dans un fichier temporaire et stocke le chemin. + """ + for record in self: + if record.ssl_cert_file and record.ssl_cert_filename: + # Créer un fichier temporaire pour stocker le certificat + fd, path = tempfile.mkstemp(suffix='.pem') + try: + # Décoder le contenu du certificat et l'écrire dans le fichier + cert_content = base64.b64decode(record.ssl_cert_file) + os.write(fd, cert_content) + # Stocker le chemin du fichier + record.ssl_cert_path = path + finally: + os.close(fd) + else: + record.ssl_cert_path = False + + #---------------------------------------------------------- + # API Connection Methods + #---------------------------------------------------------- + + def test_connection(self): + """Test the connection to the UniFi API + + This method attempts to establish a connection with the UniFi API + to verify that the connection parameters are correct. It will use + either the Controller or Site Manager API based on the api_type. + + Returns: + dict: Dictionary with status and message + """ + self.ensure_one() + + # Create an API log entry for this test + api_log = self.env['unifi.api.log'].create({ + 'site_id': self.id, + 'api_type': self.api_type, + 'method': 'GET', + 'endpoint': '/test', + 'start_time': fields.Datetime.now(), + }) + + try: + if self.api_type == 'controller': + result = self._test_controller_connection(api_log) + elif self.api_type == 'site_manager': + result = self._test_site_manager_connection(api_log) + else: + raise ValidationError(_('Invalid API type')) + + return result + except Exception as e: + _logger.error('Error testing connection: %s', str(e)) + # Update the API log with the error + self._update_api_log(api_log, { + 'status': 'error', + 'response_code': 500, + 'end_time': fields.Datetime.now(), + 'execution_time': (fields.Datetime.now() - api_log.start_time).total_seconds(), + 'error': str(e), + }) + return { + 'status': 'error', + 'message': _('Connection test failed: %s') % str(e) + } + + def _test_controller_connection(self, api_log=None): + """Test the connection to the UniFi Controller API + + Args: + api_log: Optional API log record to update + + Returns: + dict: Dictionary with status and message + """ + self.ensure_one() + + # Add debug logging + _logger.info("=== DÉBUT DU TEST DE CONNEXION ===") + _logger.info(f"API Type: {self.api_type}") + + if self.api_type != 'controller': + _logger.error(f"Type d'API incorrect: {self.api_type}") + raise ValidationError(_('This method is only for Controller API')) + + # Validate required fields + _logger.info(f"Host: {self.host}") + _logger.info(f"Port: {self.port}") + _logger.info(f"Username: {self.username}") + _logger.info(f"Password: {'*' * len(self.password) if self.password else 'Non défini'}") + _logger.info(f"Verify SSL: {self.verify_ssl}") + + if not self.host: + _logger.error("Host manquant") + raise ValidationError(_('Host is required for Controller API')) + + if not self.port: + _logger.error("Port manquant") + raise ValidationError(_('Port is required for Controller API')) + + if not self.username: + _logger.error("Username manquant") + raise ValidationError(_('Username is required for Controller API')) + + if not self.password: + _logger.error("Password manquant") + raise ValidationError(_('Password is required for Controller API')) + + # Prepare URL + base_url = f"https://{self.host}:{self.port}" + + # Essayer différents endpoints d'authentification selon les versions de l'API UniFi + # Certaines versions utilisent /api/login, d'autres /api/auth/login + login_endpoints = [ + "/api/login", # Endpoint standard + "/api/auth/login", # Endpoint alternatif + "/v2/api/login", # Endpoint pour les versions plus récentes + "/v2/api/auth/login" # Autre endpoint possible + ] + + login_endpoint = login_endpoints[0] # Commencer par le premier endpoint + url = f"{base_url}{login_endpoint}" + _logger.info(f"URL de connexion: {url}") + _logger.info(f"Autres endpoints disponibles: {login_endpoints[1:]}") + + # Prepare request data + login_data = { + 'username': self.username, + 'password': self.password, + 'remember': True + } + _logger.info(f"Données de connexion: {{'username': '{self.username}', 'password': '****', 'remember': True}}") + + # Disable SSL warnings if verify_ssl is False + if not self.verify_ssl: + _logger.info("Désactivation des avertissements SSL") + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # Prepare SSL verification + verify = self.verify_ssl + if verify and self.ssl_cert_path: + verify = self.ssl_cert_path + _logger.info(f"Utilisation du certificat SSL: {self.ssl_cert_path}") + + try: + # Make the request + _logger.info("Envoi de la requête de connexion...") + + # Essayer chaque endpoint jusqu'à ce qu'un fonctionne + response = None + success = False + + for endpoint in login_endpoints: + try: + current_url = f"{base_url}{endpoint}" + _logger.info(f"Essai avec l'endpoint: {endpoint}") + + # Essayer avec différents formats de données + # Certaines versions attendent un JSON, d'autres des données de formulaire + for content_type, data in [ + ("json", login_data), # Format JSON standard + ("data", login_data) # Format de données de formulaire + ]: + try: + _logger.info(f"Essai avec le format de données: {content_type}") + + # Construire les arguments de la requête + kwargs = { + content_type: login_data, + "verify": verify, + "timeout": self.timeout + } + + # Ajouter des headers spécifiques + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + kwargs["headers"] = headers + + response = requests.post(current_url, **kwargs) + _logger.info(f"Réponse reçue: Status code {response.status_code}") + _logger.debug(f"Contenu de la réponse: {response.text}") + + if response.status_code == 200: + _logger.info(f"Connexion réussie avec l'endpoint {endpoint} et le format {content_type}") + success = True + break + except Exception as e: + _logger.warning(f"Échec avec le format {content_type}: {str(e)}") + + if success: + break + except Exception as e: + _logger.warning(f"Échec avec l'endpoint {endpoint}: {str(e)}") + + if not response: + raise Exception("Tous les endpoints ont échoué") + # Update API log if we have a response + if api_log and response: + _logger.info("Mise à jour du log API") + self._update_api_log(api_log, { + 'endpoint': login_endpoint, + 'request_body': json.dumps(login_data), + 'response_code': response.status_code, + 'response_body': response.text, + 'end_time': fields.Datetime.now(), + 'execution_time': (fields.Datetime.now() - api_log.start_time).total_seconds(), + 'status': 'success' if response.status_code == 200 else 'error', + }) + + # Check response + if response and response.status_code == 200: + _logger.info("Connexion réussie!") + # Store the cookies in the auth session + self._create_auth_session(response.cookies, endpoint=self.last_successful_endpoint) + + return { + 'status': 'success', + 'message': _('Connection successful') + } + elif response: + _logger.error(f"Échec de la connexion: Status code {response.status_code}") + _logger.error(f"Message d'erreur: {response.text}") + return { + 'status': 'error', + 'message': _('Connection failed with status code %s: %s') % (response.status_code, response.text) + } + except requests.exceptions.ConnectTimeout as timeout_error: + _logger.error(f"Timeout de connexion: {str(timeout_error)}") + return { + 'status': 'error', + 'message': _('Connection timeout: %s') % str(timeout_error) + } + except requests.exceptions.SSLError as ssl_error: + _logger.error(f"Erreur SSL: {str(ssl_error)}") + return { + 'status': 'error', + 'message': _('SSL Error: %s') % str(ssl_error) + } + except Exception as e: + _logger.error('Error testing Controller API connection: %s', str(e)) + _logger.exception("Détails de l'erreur:") + return { + 'status': 'error', + 'message': _('Connection test failed: %s') % str(e) + } + finally: + _logger.info("=== FIN DU TEST DE CONNEXION ===") + + def _test_site_manager_connection(self, api_log=None): + """Test the connection to the UniFi Site Manager API + + Args: + api_log: Optional API log record to update + + Returns: + dict: Dictionary with status and message + """ + self.ensure_one() + + if self.api_type != 'site_manager': + raise ValidationError(_('This method is only for Site Manager API')) + + # Validate required fields + if not self.api_key: + raise ValidationError(_('API Key is required for Site Manager API')) + + # Prepare headers + headers = { + 'X-CSRF-Token': 'unifi_site_manager', + 'Authorization': f'Bearer {self.api_key}' + } + + # Prepare URL + base_url = "https://sitemanager.unifi.ui.com" + endpoint = "/api/sites" + url = f"{base_url}{endpoint}" + + # Disable SSL warnings if verify_ssl is False + if not self.verify_ssl: + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + try: + # Make the request + response = requests.get( + url, + headers=headers, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Update API log + if api_log: + self._update_api_log(api_log, { + 'endpoint': endpoint, + 'request_headers': json.dumps(headers), + 'response_code': response.status_code, + 'response_body': response.text, + 'end_time': fields.Datetime.now(), + 'execution_time': (fields.Datetime.now() - api_log.start_date).total_seconds(), + 'status': 'success' if response.status_code == 200 else 'error', + }) + + # Check response + if response.status_code == 200: + return { + 'status': 'success', + 'message': _('Connection successful') + } + else: + return { + 'status': 'error', + 'message': _('Connection failed with status code %s: %s') % (response.status_code, response.text) + } + + except Exception as e: + _logger.error('Error testing Site Manager API connection: %s', str(e)) + return { + 'status': 'error', + 'message': _('Connection test failed: %s') % str(e) + } + + def _update_api_log(self, api_log, values): + """Update API log record with values + + Args: + api_log: API log record to update + values: Dictionary of values to update + """ + if not api_log: + return + + try: + # Convertir les valeurs au format attendu par le modèle unifi.api.log + update_vals = {} + + # Mapper les champs communs + if 'status' in values: + if values['status'] == 'success': + update_vals['success'] = True + elif values['status'] == 'error': + update_vals['success'] = False + + # Mapper le message d'erreur + if 'message' in values: + if values.get('status') == 'error': + update_vals['error_message'] = values['message'] + + # Ajouter l'heure de fin si nécessaire + if 'end_time' not in update_vals and ('status' in values or values.get('message', '').startswith('Success')): + update_vals['end_time'] = fields.Datetime.now() + + # Calculer la durée si possible + if 'end_time' in update_vals and api_log.start_time: + duration = (update_vals['end_time'] - api_log.start_time).total_seconds() * 1000 + update_vals['duration'] = duration + + # Mettre à jour le statut HTTP si fourni + if 'status_code' in values: + update_vals['status_code'] = values['status_code'] + + # Mettre à jour le corps de la réponse si fourni + if 'response_body' in values: + update_vals['response_body'] = values['response_body'] + + # Écrire les valeurs mises à jour + api_log.write(update_vals) + except Exception as e: + _logger.error('Error updating API log: %s', str(e)) + + def _check_auth_session(self): + """Vérifie si la session d'authentification est valide + + Returns: + bool: True si la session est valide, False sinon + """ + self.ensure_one() + _logger.info("Vérification de la session d'authentification") + + # Vérifier si une session existe + if not self.auth_session_id: + _logger.warning("Pas de session d'authentification existante") + return False + + # Vérifier si la session est expirée + if self.auth_session_id.expiry and self.auth_session_id.expiry < fields.Datetime.now(): + _logger.warning(f"Session expirée: {self.auth_session_id.expiry}") + return False + + # Mettre à jour la date de dernière utilisation + self.auth_session_id.write({ + 'last_used': fields.Datetime.now() + }) + + _logger.info("Session d'authentification valide") + return True + + def _get_auth_cookies(self): + """Récupère les cookies de la session d'authentification + + Returns: + dict: Cookies de la session ou False si pas de session valide + """ + self.ensure_one() + + # Vérifier si la session est valide + if not self._check_auth_session(): + _logger.warning("Tentative de récupération des cookies sans session valide") + # Essayer de se reconnecter + _logger.info("Tentative de reconnexion automatique") + result = self._test_controller_connection() + if result.get('status') != 'success': + _logger.error(f"Échec de la reconnexion: {result.get('message')}") + return False + + # Récupérer les cookies de la session + try: + cookie_str = self.auth_session_id.cookie + if not cookie_str: + _logger.error("Chaîne de cookies vide") + return False + + # Afficher la chaîne de cookies brute pour débogage + _logger.info(f"Chaîne de cookies brute: {cookie_str}") + + # Méthode 1: Extraire les cookies à partir de la chaîne + cookies = {} + + # Vérifier si c'est un objet RequestsCookieJar + if cookie_str.startswith(''): + cookie_content = cookie_str[len('')] + cookie_pairs = cookie_content.split(', ') + for pair in cookie_pairs: + if '=' in pair: + # Corriger le format des cookies comme '' + if pair.startswith('' + if ' for ' in value: + value = value.split(' for ')[0] + cookies[key.strip()] = value.strip() + + # Si nous trouvons un cookie TOKEN, c'est un UDM Pro + if key.strip() == 'TOKEN' and not self.auth_session_id.is_udm_pro: + _logger.info("Détection d'un UDM Pro basé sur le cookie TOKEN") + self.auth_session_id.write({'is_udm_pro': True}) + else: + key, value = pair.split('=', 1) + cookies[key.strip()] = value.strip() + # Sinon, essayer de parser comme un dictionnaire + elif cookie_str.startswith('{') and cookie_str.endswith('}'): + try: + cookies = ast.literal_eval(cookie_str) + except Exception as e: + _logger.warning(f"Impossible de parser les cookies comme un dictionnaire: {str(e)}") + # Sinon, essayer de parser comme une liste de paires clé=valeur + else: + cookie_pairs = cookie_str.split(';') + for pair in cookie_pairs: + if '=' in pair: + key, value = pair.split('=', 1) + cookies[key.strip()] = value.strip() + + # Ajouter des cookies spécifiques pour UniFi + if 'TOKEN' not in cookies and 'unifises' not in cookies: + # Essayer d'extraire le token des headers de la réponse + if self.last_response_headers: + try: + headers_dict = ast.literal_eval(self.last_response_headers) + for header, value in headers_dict.items(): + if header.lower() == 'set-cookie': + if 'TOKEN=' in value: + token_part = value.split('TOKEN=')[1].split(';')[0] + cookies['TOKEN'] = token_part + if 'unifises=' in value: + unifises_part = value.split('unifises=')[1].split(';')[0] + cookies['unifises'] = unifises_part + except (ValueError, SyntaxError) as e: + _logger.warning(f"Impossible de parser les headers: {str(e)}") + + _logger.info(f"Cookies récupérés: {cookies}") + return cookies + except Exception as e: + _logger.error(f"Erreur lors de la récupération des cookies: {str(e)}") + return False + + def _create_auth_session(self, cookies, endpoint=None): + """Create or update authentication session + + Args: + cookies: Session cookies from successful login + endpoint: The endpoint used for authentication + + Returns: + unifi.auth.session: Created or updated auth session record + """ + self.ensure_one() + _logger.info("Création/mise à jour de la session d'authentification") + + # Déterminer si c'est un UDM Pro en fonction de l'endpoint utilisé + is_udm_pro = False + if endpoint and '/api/auth/login' in endpoint: + is_udm_pro = True + _logger.info("Détection d'un contrôleur UDM Pro basé sur l'endpoint d'authentification") + + # Vérifier si un token est présent dans les cookies (caractéristique des UDM Pro) + token = None + if cookies and 'TOKEN' in cookies: + token = cookies.get('TOKEN') + is_udm_pro = True + _logger.info("Détection d'un contrôleur UDM Pro basé sur le cookie TOKEN") + + # Check if there's an existing session + if self.auth_session_id: + # Update existing session + _logger.info("Mise à jour de la session existante") + session_vals = { + 'cookie': str(cookies), + 'endpoint': endpoint, + 'is_udm_pro': is_udm_pro, + 'expiry': fields.Datetime.now() + timedelta(hours=24), # Default 24h expiry + 'last_used': fields.Datetime.now() + } + + # Ajouter le token si disponible + if token: + session_vals['token'] = token + + self.auth_session_id.write(session_vals) + return self.auth_session_id + else: + # Create new session + session_vals = { + 'site_id': self.id, + 'auth_type': self.api_type, + 'cookie': str(cookies), + 'endpoint': endpoint, + 'is_udm_pro': is_udm_pro, + 'expiry': fields.Datetime.now() + timedelta(hours=24), + 'last_used': fields.Datetime.now() + } + + # Ajouter le token si disponible + if token: + session_vals['token'] = token + + session = self.env['unifi.auth.session'].create(session_vals) + self.auth_session_id = session + return session + def _inverse_api_key(self): - """Inverse pour le champ api_key""" - for site in self: - if site.api_type == 'site_manager': - manager = self.env['unifi.site.manager'].search([('site_id', '=', site.id)], limit=1) - if manager: - manager.api_key = site.api_key - elif site.api_key: # Créer un nouveau manager si nécessaire - self.env['unifi.site.manager'].create({ - 'site_id': site.id, - 'api_key': site.api_key, - }) + """Inverse pour le champ api_key + + Cette méthode a été simplifiée dans le cadre de la refactorisation. + Elle n'a plus besoin de synchroniser les données avec l'ancien modèle unifi.site.manager + puisque toute la logique a été consolidée dans ce modèle. + """ + # Rien à faire, puisque api_key est maintenant directement géré par le modèle unifi.site + pass def _inverse_mfa_enabled(self): - """Inverse pour le champ mfa_enabled""" - for site in self: - if site.api_type == 'site_manager': - manager = self.env['unifi.site.manager'].search([('site_id', '=', site.id)], limit=1) - if manager: - manager.mfa_enabled = site.mfa_enabled - elif site.mfa_enabled: # Créer un nouveau manager si nécessaire - self.env['unifi.site.manager'].create({ - 'site_id': site.id, - 'mfa_enabled': site.mfa_enabled, - }) + """Inverse pour le champ mfa_enabled + + Cette méthode a été simplifiée dans le cadre de la refactorisation. + Elle n'a plus besoin de synchroniser les données avec l'ancien modèle unifi.site.manager + puisque toute la logique a été consolidée dans ce modèle. + """ + # Rien à faire, puisque mfa_enabled est maintenant directement géré par le modèle unifi.site + pass def _inverse_mfa_token(self): - """Inverse pour le champ mfa_token""" - for site in self: - if site.api_type == 'site_manager': - manager = self.env['unifi.site.manager'].search([('site_id', '=', site.id)], limit=1) - if manager: - manager.mfa_token = site.mfa_token - elif site.mfa_token: # Créer un nouveau manager si nécessaire - self.env['unifi.site.manager'].create({ - 'site_id': site.id, - 'mfa_token': site.mfa_token, - }) + """Inverse pour le champ mfa_token + + Cette méthode a été simplifiée dans le cadre de la refactorisation. + Elle n'a plus besoin de synchroniser les données avec l'ancien modèle unifi.site.manager + puisque toute la logique a été consolidée dans ce modèle. + """ + # Rien à faire, puisque mfa_token est maintenant directement géré par le modèle unifi.site + pass + + def action_test_connection(self): + """Test the connection to the UniFi API + + This method tests the connection to the UniFi API using the appropriate + connection method based on the API type (Controller or Site Manager). + It displays a notification with the result of the connection test. + + Returns: + dict: Action dictionary for client notification + """ + self.ensure_one() + + try: + # Perform connection test based on API type + if self.api_type == 'controller': + result = self._test_controller_connection() + elif self.api_type == 'site_manager': + result = self._test_site_manager_connection() + else: + raise ValidationError(_('Invalid API type')) + + # Check result and display appropriate notification + if result: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Test'), + 'message': _('Connection successful!'), + 'sticky': False, + 'type': 'success', + } + } + else: + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Test'), + 'message': _('Connection failed. Please check your settings.'), + 'sticky': True, + 'type': 'danger', + } + } + except Exception as e: + # Handle any exceptions that occur during the connection test + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Test'), + 'message': _('Error: %s') % str(e), + 'sticky': True, + 'type': 'danger', + } + } + + def action_sync_now(self): + """Trigger an immediate synchronization""" + self.ensure_one() + + # Création d'un job de synchronisation dans le modèle UnifiSyncJob + sync_job = self.env['unifi.sync.job'].create({ + 'site_id': self.id, # Référence au site actuel + 'api_type': self.api_type, # Type d'API (controller ou site_manager) + 'sync_type': 'manual', # Type de synchronisation (manuel dans ce cas) + 'start_time': fields.Datetime.now(), # Horodatage du début + 'state': 'running', # État initial du job (en cours d'exécution) + }) + + try: + if self.api_type == 'controller': + result = self._sync_controller() + elif self.api_type == 'site_manager': + result = self._sync_site_manager() + else: + raise ValidationError(_('Invalid API type')) + + # Update sync job with result + if result: + # Mise à jour des champs dans le modèle UnifiSyncJob pour indiquer le succès + sync_job.write({ + 'end_time': fields.Datetime.now(), # Horodatage de fin + 'state': 'completed', # État: terminé + 'status': 'success', # Statut: succès + }) + + # Update last_sync timestamp + self.write({ + 'last_sync': fields.Datetime.now(), + 'last_update': fields.Datetime.now() if result else self.last_update, + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Synchronization'), + 'message': _('Synchronization completed successfully'), + 'sticky': False, + 'type': 'success', + } + } + else: + sync_job.write({ + 'end_time': fields.Datetime.now(), + 'status': 'failed', + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Synchronization'), + 'message': _('Synchronization failed'), + 'sticky': True, + 'type': 'danger', + } + } + except Exception as e: + _logger.error('Error during synchronization: %s', str(e)) + + # Mise à jour du job de synchronisation avec les détails de l'erreur + # Note: Si le champ 'error' n'existe pas dans le modèle, on l'ignore + vals = { + 'end_time': fields.Datetime.now(), + 'status': 'failed', + } + + # Vérifier si le champ 'error' existe dans le modèle + if 'error' in self.env['unifi.sync.job']._fields: + vals['error'] = str(e) + + sync_job.write(vals) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Synchronization'), + 'message': _('Synchronization failed: %s') % str(e), + 'sticky': True, + 'type': 'danger', + } + } def _inverse_verify_ssl(self): - """Inverse pour le champ verify_ssl""" - for site in self: - if site.api_type == 'controller': - controller = self.env['unifi.site.controller'].search([('site_id', '=', site.id)], limit=1) - if controller: - controller.verify_ssl = site.verify_ssl - elif site.api_type == 'site_manager': - manager = self.env['unifi.site.manager'].search([('site_id', '=', site.id)], limit=1) - if manager: - manager.verify_ssl = site.verify_ssl + """Inverse pour le champ verify_ssl + + Cette méthode a été simplifiée dans le cadre de la refactorisation. + Elle n'a plus besoin de synchroniser les données avec les anciens modèles + unifi.site.controller et unifi.site.manager puisque toute la logique + a été consolidée dans ce modèle. + """ + # Rien à faire, puisque verify_ssl est maintenant directement géré par le modèle unifi.site + pass # Configuration data last_update = fields.Datetime( @@ -343,6 +1522,17 @@ class UnifiSite(models.Model): help='Raw configuration data in JSON format' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes du site au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + # Synchronization settings sync_interval = fields.Integer( string='Sync Interval (minutes)', @@ -430,9 +1620,9 @@ class UnifiSite(models.Model): # Relations with system models system_info_id = fields.Many2one( comodel_name='unifi.system.info', - string='System Info', + string='Primary System Info', ondelete='cascade', - help='System information snapshot', + help='Primary system information snapshot', required=False ) @@ -525,32 +1715,45 @@ class UnifiSite(models.Model): """ for site in self: if site.api_type == 'controller': - # Delegate to controller-specific implementation - self.env['unifi.site.controller']._check_required_fields(site) + # Check controller-specific required fields + if not site.host: + raise ValidationError(_('Host is required for Controller API')) + if not site.port: + raise ValidationError(_('Port is required for Controller API')) + if not site.username: + raise ValidationError(_('Username is required for Controller API')) + if not site.password: + raise ValidationError(_('Password is required for Controller API')) elif site.api_type == 'site_manager': - # Delegate to site manager-specific implementation - self.env['unifi.site.manager']._check_required_fields(site) + # Check site manager-specific required fields + if not site.api_key: + raise ValidationError(_('API Key is required for Site Manager API')) @api.onchange('api_type') def _onchange_api_type(self): """Clear fields that are not relevant to the selected API type - This method delegates to the appropriate API-specific model for field clearing. + This method implements field clearing logic directly based on the API type. + Previously this was delegated to specific models, but now it's integrated here + as part of the refactoring. """ if self.api_type == 'controller': - # Delegate to controller-specific implementation - self.env['unifi.site.controller']._clear_irrelevant_fields(self) + # Clear site_manager-specific fields + self.api_key = False + self.mfa_enabled = False + self.mfa_token = False elif self.api_type == 'site_manager': - # Delegate to site manager-specific implementation - self.env['unifi.site.manager']._clear_irrelevant_fields(self) + # Clear controller-specific fields + self.username = False + self.password = False - @api.depends('network_ids', 'device_ids', 'user_ids', 'firewall_rule_ids') + @api.depends('network_ids', 'device_ids', 'user_ids', 'firewall_rule_ids', 'vlan_ids', 'port_forward_ids', 'routing_config_ids', 'wifi_ids', 'dns_ids', 'system_info_ids', 'vpn_ids') def _compute_counts(self): """Compute counts for related records - This method calculates the number of networks, devices, users, and firewall rules - associated with this site. It's triggered automatically when any of these related - records are added or removed. + This method calculates the number of networks, devices, users, firewall rules, + VLANs, port forwards, routing configurations, and WiFi networks associated with this site. + It's triggered automatically when any of these related records are added or removed. """ for site in self: # Safely get counts, handling potential errors @@ -559,10 +1762,17 @@ class UnifiSite(models.Model): site.device_count = len(site.device_ids) if site.device_ids else 0 site.user_count = len(site.user_ids) if site.user_ids else 0 site.firewall_rule_count = len(site.firewall_rule_ids) if site.firewall_rule_ids else 0 + site.vlan_count = len(site.vlan_ids) if site.vlan_ids else 0 + site.port_forward_count = len(site.port_forward_ids) if site.port_forward_ids else 0 + site.routing_config_count = len(site.routing_config_ids) if site.routing_config_ids else 0 + site.wifi_count = len(site.wifi_ids) if site.wifi_ids else 0 + site.dns_count = len(site.dns_ids) if site.dns_ids else 0 + site.system_info_count = len(site.system_info_ids) if site.system_info_ids else 0 + site.vpn_count = len(site.vpn_ids) if site.vpn_ids else 0 except Exception as e: _logger.error('Error computing counts for site %s: %s', site.name, str(e)) # Set default values in case of error - site.network_count = site.device_count = site.user_count = site.firewall_rule_count = 0 + site.network_count = site.device_count = site.user_count = site.firewall_rule_count = site.vlan_count = site.port_forward_count = site.routing_config_count = site.wifi_count = site.dns_count = site.system_info_count = site.vpn_count = 0 @api.depends('user_ids') def _compute_client_count(self): @@ -588,7 +1798,7 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('Networks'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.network', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', @@ -600,7 +1810,7 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('Devices'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.device', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', @@ -612,7 +1822,7 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('Users'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.user', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', @@ -624,7 +1834,7 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('Firewall Rules'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.firewall.rule', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', @@ -636,7 +1846,7 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('API Logs'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.api.log', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', @@ -648,13 +1858,73 @@ class UnifiSite(models.Model): self.ensure_one() return { 'name': _('Sync Jobs'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.sync.job', 'domain': [('site_id', '=', self.id)], 'type': 'ir.actions.act_window', 'context': {'default_site_id': self.id} } + def action_view_vlans(self): + """Open the VLANs view filtered for this site""" + self.ensure_one() + return { + 'name': _('VLANs'), + 'view_mode': 'list,form', + 'res_model': 'unifi.vlan', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + 'context': {'default_site_id': self.id} + } + + def action_view_port_forwards(self): + """Open the port forwards view filtered for this site""" + self.ensure_one() + return { + 'name': _('Port Forwards'), + 'view_mode': 'list,form', + 'res_model': 'unifi.port_forward', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + 'context': {'default_site_id': self.id} + } + + def action_view_system_info(self): + """Open the system info view filtered for this site""" + self.ensure_one() + return { + 'name': _('System Info'), + 'view_mode': 'list,form', + 'res_model': 'unifi.system.info', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + 'context': {'default_site_id': self.id} + } + + def action_view_dns(self): + """Open the DNS entries view filtered for this site""" + self.ensure_one() + return { + 'name': _('DNS Entries'), + 'view_mode': 'list,form', + 'res_model': 'unifi.dns', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + 'context': {'default_site_id': self.id} + } + + def action_view_vpn(self): + """Open the VPN configurations view filtered for this site""" + self.ensure_one() + return { + 'name': _('VPN Configurations'), + 'view_mode': 'list,form', + 'res_model': 'unifi.vpn', + 'domain': [('site_id', '=', self.id)], + 'type': 'ir.actions.act_window', + 'context': {'default_site_id': self.id} + } + def action_configure_controller(self): """Open the controller configuration view for this site @@ -664,98 +1934,160 @@ class UnifiSite(models.Model): """ self.ensure_one() - # Check if a controller configuration already exists for this site - controller = self.env['unifi.site.controller'].search([('site_id', '=', self.id)], limit=1) - - if controller: - # Open existing controller configuration - return { - 'name': _('Controller API Configuration'), - 'view_mode': 'form', - 'res_model': 'unifi.site.controller', - 'res_id': controller.id, - 'type': 'ir.actions.act_window', - 'target': 'new', - } - else: - # Create and open a new controller configuration - controller = self.env['unifi.site.controller'].create({ - 'site_id': self.id, - 'verify_ssl': self.verify_ssl, - }) - - return { - 'name': _('Controller API Configuration'), - 'view_mode': 'form', - 'res_model': 'unifi.site.controller', - 'res_id': controller.id, - 'type': 'ir.actions.act_window', - 'target': 'new', - } + # Since we've consolidated all API functionality into the main model, + # we just need to open the current site record in form view + return { + 'name': _('Controller API Configuration'), + 'view_mode': 'form', + 'res_model': 'unifi.site', + 'res_id': self.id, + 'type': 'ir.actions.act_window', + 'target': 'current', + } def action_configure_site_manager(self): - """Open the site manager configuration view for this site + """Open the site configuration form in edit mode - This method checks if a site manager configuration already exists for this site. - If it does, it opens the existing configuration in form view. - If not, it creates a new configuration and opens it in form view. + This method has been updated as part of the refactorization. + Since we've consolidated all API-related functionality into the main + unifi.site model, we simply open this record in form view for editing. """ self.ensure_one() - # Check if a site manager configuration already exists for this site - manager = self.env['unifi.site.manager'].search([('site_id', '=', self.id)], limit=1) + # Ensure API type is set to site_manager + if self.api_type != 'site_manager': + self.api_type = 'site_manager' - if manager: - # Open existing site manager configuration - return { - 'name': _('Site Manager API Configuration'), - 'view_mode': 'form', - 'res_model': 'unifi.site.manager', - 'res_id': manager.id, - 'type': 'ir.actions.act_window', - 'target': 'new', - } - else: - # Create and open a new site manager configuration - manager = self.env['unifi.site.manager'].create({ - 'site_id': self.id, - 'verify_ssl': self.verify_ssl, - }) - - return { - 'name': _('Site Manager API Configuration'), - 'view_mode': 'form', - 'res_model': 'unifi.site.manager', - 'res_id': manager.id, - 'type': 'ir.actions.act_window', - 'target': 'new', - } + # Open this site record in form view + return { + 'name': _('Site Manager API Configuration'), + 'view_mode': 'form', + 'res_model': 'unifi.site', + 'res_id': self.id, + 'type': 'ir.actions.act_window', + 'target': 'current', + } - def action_sync_now(self): - """Trigger an immediate synchronization""" - self.ensure_one() - if self.api_type == 'controller': - return self._sync_controller() - elif self.api_type == 'site_manager': - return self._sync_site_manager() + return True def action_sync_networks(self): """Synchronize only networks for this site""" self.ensure_one() try: - # Utiliser la méthode de synchronisation du modèle unifi.network - self.env['unifi.network'].sync_networks(self) - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Network Synchronization'), - 'message': _('Networks synchronized successfully!'), - 'sticky': False, - 'type': 'success', + # Vérifier si nous avons une session d'authentification valide + if not self.auth_session_id or not self._check_auth_session(): + _logger.warning("Pas de session d'authentification valide, tentative de connexion automatique") + + # Tenter de se connecter au contrôleur UniFi + connection_result = self._test_controller_connection() + _logger.info(f"Résultat de la connexion: {connection_result}") + + if connection_result.get('status') != 'success': + _logger.error(f"Impossible de se connecter au contrôleur UniFi: {connection_result.get('message')}") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Error'), + 'message': _(f"Unable to connect to UniFi Controller: {connection_result.get('message')}"), + 'sticky': True, + 'type': 'danger', + } + } + _logger.info("Connexion au contrôleur UniFi réussie, poursuite de la synchronisation") + + # Récupérer les données des réseaux depuis l'API UniFi + _logger.info("Récupération des données des réseaux depuis l'API UniFi") + + # Déterminer quelle méthode utiliser en fonction du type d'API + if self.api_type == 'controller': + networks_data = self._get_controller_network_data() + elif self.api_type == 'site_manager': + networks_data = self._get_site_manager_network_data() + else: + networks_data = None + _logger.error(f"Type d'API non pris en charge: {self.api_type}") + + if not networks_data: + _logger.error("Impossible de récupérer les données des réseaux") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Network Synchronization'), + 'message': _('Failed to retrieve network data from UniFi API'), + 'sticky': True, + 'type': 'danger', + } } + + # Analyser les données des réseaux + _logger.info(f"Données de réseaux reçues: {type(networks_data)}") + + # Vérifier la structure des données + if isinstance(networks_data, list): + networks_list = networks_data + elif isinstance(networks_data, dict): + # La plupart des API UniFi renvoient les données dans une clé 'data' + networks_list = networks_data.get('data', []) + if not networks_list and 'networks' in networks_data: + networks_list = networks_data.get('networks', []) + else: + networks_list = [] + + _logger.info(f"Nombre de réseaux trouvés dans l'API: {len(networks_list)}") + + # Créer ou mettre à jour les réseaux + processed_networks = self.env['unifi.network'] + + for network_data in networks_list: + network_id = network_data.get('_id') or network_data.get('id') + if not network_id: + _logger.warning("Réseau sans identifiant ignoré") + continue + + # Rechercher un réseau existant par ID + network = self.env['unifi.network'].search([ + ('site_id', '=', self.id), + ('network_id', '=', network_id) + ], limit=1) + + if network: + _logger.info(f"Mise à jour du réseau existant: {network.name} (ID: {network_id})") + else: + _logger.info(f"Création d'un nouveau réseau avec ID: {network_id}") + + # Créer ou mettre à jour le réseau + network = self.env['unifi.network'].create_or_update_from_data(self, network_data) + if network: + processed_networks += network + + # Afficher un message de succès et retourner la vue des réseaux + # D'abord afficher la notification + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Network Synchronization'), + 'message': _(f'{len(networks_list)} réseaux trouvés, {len(processed_networks)} créés ou mis à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Ensuite retourner la vue des réseaux + return { + 'type': 'ir.actions.act_window', + 'name': _('Networks'), + 'res_model': 'unifi.network', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', } except Exception as e: return { @@ -772,23 +2104,151 @@ class UnifiSite(models.Model): def action_sync_devices(self): """Synchronize only devices for this site""" self.ensure_one() + _logger.info("=== DÉBUT DE LA SYNCHRONISATION DES APPAREILS (action_sync_devices) ===") + try: - # Utiliser la méthode de synchronisation du modèle unifi.device - devices = self.env['unifi.device'].search([('site_id', '=', self.id)]) - for device in devices: - device.sync_from_unifi() - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Device Synchronization'), - 'message': _('Devices synchronized successfully!'), - 'sticky': False, - 'type': 'success', + # Vérifier si nous avons une session d'authentification valide + if not self.auth_session_id or not self._check_auth_session(): + _logger.warning("Pas de session d'authentification valide, tentative de connexion automatique") + + # Tenter de se connecter au contrôleur UniFi + connection_result = self._test_controller_connection() + _logger.info(f"Résultat de la connexion: {connection_result}") + + if connection_result.get('status') != 'success': + _logger.error(f"Impossible de se connecter au contrôleur UniFi: {connection_result.get('message')}") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Connection Error'), + 'message': _(f"Unable to connect to UniFi Controller: {connection_result.get('message')}"), + 'sticky': True, + 'type': 'danger', + } + } + _logger.info("Connexion au contrôleur UniFi réussie, poursuite de la synchronisation") + + # Récupérer les données des appareils depuis l'API UniFi + _logger.info("Récupération des données des appareils depuis l'API UniFi") + device_data = self._get_device_data() + + if not device_data: + _logger.error("Impossible de récupérer les données des appareils") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Device Synchronization'), + 'message': _('Failed to retrieve device data from UniFi API'), + 'sticky': True, + 'type': 'danger', + } + } + + # Analyser les données des appareils + _logger.info(f"Données d'appareils reçues: {type(device_data)}") + + # Vérifier la structure des données + if isinstance(device_data, dict): + _logger.info(f"Clés dans les données: {list(device_data.keys())}") + + # La plupart des API UniFi renvoient les données dans une clé 'data' + devices_list = device_data.get('data', []) + if not devices_list and 'devices' in device_data: + devices_list = device_data.get('devices', []) + + _logger.info(f"Nombre d'appareils trouvés dans l'API: {len(devices_list)}") + + if devices_list: + # Afficher des informations sur le premier appareil pour débogage + first_device = devices_list[0] + _logger.info(f"Premier appareil: {first_device.get('name', 'Sans nom')}") + _logger.info(f"MAC: {first_device.get('mac', 'N/A')}") + _logger.info(f"Modèle: {first_device.get('model', 'N/A')}") + + # Créer ou mettre à jour les appareils à partir des données de l'API + existing_devices = self.env['unifi.device'].search([('site_id', '=', self.id)]) + _logger.info(f"Nombre d'appareils existants dans Odoo: {len(existing_devices)}") + + # Garder une trace des appareils traités + processed_devices = self.env['unifi.device'] + + # Créer ou mettre à jour les appareils + for device_data in devices_list: + mac = device_data.get('mac') + if not mac: + _logger.warning("Appareil sans adresse MAC ignoré") + continue + + # Rechercher un appareil existant par MAC + device = self.env['unifi.device'].search([ + ('site_id', '=', self.id), + ('mac_address', '=', mac) + ], limit=1) + + if device: + _logger.info(f"Mise à jour de l'appareil existant: {device.name} (MAC: {mac})") + else: + _logger.info(f"Création d'un nouvel appareil avec MAC: {mac}") + + # Créer ou mettre à jour l'appareil + device = self.env['unifi.device'].create_from_api_data(self, device_data) + if device: + processed_devices += device + + # Afficher un message de succès et retourner la vue des appareils + # D'abord afficher la notification + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Device Synchronization'), + 'message': _(f'{len(devices_list)} appareils trouvés, {len(processed_devices)} créés ou mis à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Ensuite retourner la vue des appareils + return { + 'type': 'ir.actions.act_window', + 'name': _('Devices'), + 'res_model': 'unifi.device', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', + } + else: + _logger.warning("Aucun appareil trouvé dans les données de l'API") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Device Synchronization'), + 'message': _('No devices found in the UniFi API data'), + 'sticky': False, + 'type': 'warning', + } + } + else: + _logger.error(f"Format de données inattendu: {type(device_data)}") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Device Synchronization'), + 'message': _('Unexpected data format received from UniFi API'), + 'sticky': True, + 'type': 'danger', + } } - } except Exception as e: + _logger.exception(f"Erreur lors de la synchronisation des appareils: {str(e)}") return { 'type': 'ir.actions.client', 'tag': 'display_notification', @@ -800,24 +2260,300 @@ class UnifiSite(models.Model): } } + def action_sync_dns(self): + """Synchronize DNS entries from UniFi + + This method retrieves DNS data from the UniFi API and creates or updates + DNS records in Odoo. It handles both success and error notifications. + """ + self.ensure_one() + + try: + # Récupérer les données DNS depuis l'API UniFi en fonction du type d'API + if self.api_type == 'controller': + dns_data = self._get_controller_get_dns_data() + else: + dns_data = self._get_site_manager_dns_data() + + if dns_data: + # Vérifier le format des données + if isinstance(dns_data, list): + # Initialiser la liste des entrées DNS traitées + processed_dns = self.env['unifi.dns'] + dns_list = dns_data + + # Traiter chaque entrée DNS + for dns_item in dns_list: + # Créer ou mettre à jour l'entrée DNS + dns = self.env['unifi.dns'].create_or_update_from_data(self, dns_item) + if dns: + processed_dns += dns + + # Afficher un message de succès + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('DNS Synchronization'), + 'message': _(f'{len(dns_list)} entrées DNS trouvées, {len(processed_dns)} créées ou mises à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Retourner une action pour afficher la liste des entrées DNS + return { + 'name': _('DNS Entries'), + 'type': 'ir.actions.act_window', + 'res_model': 'unifi.dns', + 'view_mode': 'list,form', + 'domain': [('site_id', '=', self.id)], + 'context': {'default_site_id': self.id}, + } + else: + # Format de données incorrect + raise UserError(_('Invalid data format received from UniFi API')) + else: + # Aucune donnée reçue, mais ce n'est pas une erreur + # Afficher un message d'information + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('DNS Synchronization'), + 'message': _('Aucune entrée DNS trouvée dans le contrôleur UniFi.'), + 'sticky': False, + 'type': 'info', + } + } + ) + + # Retourner une action pour afficher la liste des entrées DNS (même si vide) + return { + 'name': _('DNS Entries'), + 'type': 'ir.actions.act_window', + 'res_model': 'unifi.dns', + 'view_mode': 'list,form', + 'domain': [('site_id', '=', self.id)], + 'context': {'default_site_id': self.id}, + } + + except Exception as e: + # Gérer les erreurs + _logger.error(f"Error synchronizing DNS entries: {str(e)}") + raise UserError(_(f"Error synchronizing DNS entries: {str(e)}")) + + def action_sync_system_info(self): + """Synchronize system information from UniFi + + This method retrieves system information from the UniFi API and creates or updates + system info records in Odoo. It handles both success and error notifications. + """ + self.ensure_one() + + try: + # Récupérer les données système depuis l'API UniFi + system_info_data = self._get_site_manager_system_info_data() + + if system_info_data: + # Vérifier le format des données + if isinstance(system_info_data, list) or isinstance(system_info_data, dict): + # Convertir en liste si c'est un dictionnaire + if isinstance(system_info_data, dict): + system_info_list = [system_info_data] + else: + system_info_list = system_info_data + + # Initialiser la liste des informations système traitées + processed_system_info = self.env['unifi.system.info'] + + # Traiter chaque information système + for system_info_item in system_info_list: + # Créer ou mettre à jour l'information système + system_info = self.env['unifi.system.info'].create_or_update_from_data(self, system_info_item) + if system_info: + processed_system_info += system_info + + # Afficher un message de succès + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('System Info Synchronization'), + 'message': _(f'{len(system_info_list)} informations système trouvées, {len(processed_system_info)} créées ou mises à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Retourner une action pour afficher la liste des informations système + return { + 'name': _('System Info'), + 'type': 'ir.actions.act_window', + 'res_model': 'unifi.system.info', + 'view_mode': 'list,form', + 'domain': [('site_id', '=', self.id)], + 'context': {'default_site_id': self.id}, + } + else: + # Format de données incorrect + raise UserError(_('Invalid data format received from UniFi API')) + else: + # Aucune donnée reçue + raise UserError(_('No system info data received from UniFi API')) + + except Exception as e: + # Gérer les erreurs + _logger.error(f"Error synchronizing system info: {str(e)}") + raise UserError(_(f"Error synchronizing system info: {str(e)}")) + + def action_sync_wifi(self): + """Synchronize WiFi networks from UniFi + + This method retrieves WiFi network data from the UniFi API and creates or updates + WiFi network records in Odoo. It handles both success and error notifications. + """ + self.ensure_one() + + try: + # Récupérer les données WiFi depuis l'API UniFi + wifi_data = self._get_controller_wifi_data() + + if wifi_data: + # Vérifier le format des données + if isinstance(wifi_data, list): + # Initialiser la liste des réseaux WiFi traités + processed_wifi = self.env['unifi.wifi'] + wifi_list = wifi_data + + # Traiter chaque réseau WiFi + for wifi_item in wifi_list: + # Créer ou mettre à jour le réseau WiFi + wifi = self.env['unifi.wifi'].create_or_update_from_data(self, wifi_item) + if wifi: + processed_wifi += wifi + + # Afficher un message de succès + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('WiFi Network Synchronization'), + 'message': _(f'{len(wifi_list)} réseaux WiFi trouvés, {len(processed_wifi)} créés ou mis à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Retourner une action pour afficher la liste des réseaux WiFi + return { + 'name': _('WiFi Networks'), + 'type': 'ir.actions.act_window', + 'res_model': 'unifi.wifi', + 'view_mode': 'list,form', + 'domain': [('site_id', '=', self.id)], + 'context': {'default_site_id': self.id}, + } + else: + # Format de données incorrect + raise UserError(_('Invalid data format received from UniFi API')) + else: + # Aucune donnée reçue + raise UserError(_('No WiFi network data received from UniFi API')) + + except Exception as e: + # Gérer les erreurs + _logger.error(f"Error synchronizing WiFi networks: {str(e)}") + raise UserError(_(f"Error synchronizing WiFi networks: {str(e)}")) + def action_sync_users(self): """Synchronize only users for this site""" self.ensure_one() try: - # Utiliser la méthode de synchronisation du modèle unifi.user - self.env['unifi.user'].sync_users(self) + # Récupérer les données des utilisateurs depuis l'API UniFi + user_data = self._get_controller_user_data() + + if user_data: + # Vérifier le format des données + if isinstance(user_data, list): + # Initialiser la liste des utilisateurs traités + processed_users = self.env['unifi.user'] + users_list = user_data - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('User Synchronization'), - 'message': _('Users synchronized successfully!'), - 'sticky': False, - 'type': 'success', + # Traiter chaque utilisateur + for user_item in users_list: + # Créer ou mettre à jour l'utilisateur + user = self.env['unifi.user'].create_or_update_from_data(self, user_item) + if user: + processed_users += user + + # Afficher un message de succès et retourner la vue des utilisateurs + # D'abord afficher la notification + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('User Synchronization'), + 'message': _(f'{len(users_list)} utilisateurs trouvés, {len(processed_users)} créés ou mis à jour'), + 'sticky': False, + 'type': 'success', + } + } + ) + + # Ensuite retourner la vue des utilisateurs + return { + 'type': 'ir.actions.act_window', + 'name': _('Users'), + 'res_model': 'unifi.user', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', + } + else: + _logger.warning("Aucun utilisateur trouvé dans les données de l'API") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('User Synchronization'), + 'message': _('No users found in the UniFi API data'), + 'sticky': False, + 'type': 'warning', + } + } + else: + _logger.error(f"Format de données inattendu: {type(user_data)}") + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('User Synchronization'), + 'message': _('Unexpected data format received from UniFi API'), + 'sticky': True, + 'type': 'danger', + } } - } except Exception as e: + _logger.exception(f"Erreur lors de la synchronisation des utilisateurs: {str(e)}") return { 'type': 'ir.actions.client', 'tag': 'display_notification', @@ -834,17 +2570,35 @@ class UnifiSite(models.Model): self.ensure_one() try: # Utiliser la méthode de synchronisation du modèle unifi.vlan - self.env['unifi.vlan'].sync_vlans_from_api(self) - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('VLAN Synchronization'), - 'message': _('VLANs synchronized successfully!'), - 'sticky': False, - 'type': 'success', + vlans = self.env['unifi.vlan'].sync_vlans_from_api(self) + + # Afficher un message de succès + # Vérifier si vlans est une liste ou un booléen + vlan_count = len(vlans) if isinstance(vlans, list) else 0 + + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('VLAN Synchronization'), + 'message': _(f'{vlan_count} VLANs synchronisés avec succès!'), + 'sticky': False, + 'type': 'success', + } } + ) + + # Retourner la vue des VLANs + return { + 'type': 'ir.actions.act_window', + 'name': _('VLANs'), + 'res_model': 'unifi.vlan', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', } except Exception as e: return { @@ -863,17 +2617,36 @@ class UnifiSite(models.Model): self.ensure_one() try: # Utiliser la méthode de synchronisation du modèle unifi.firewall.rule - self.env['unifi.firewall.rule'].sync_firewall_rules(self) - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Firewall Rule Synchronization'), - 'message': _('Firewall rules synchronized successfully!'), - 'sticky': False, - 'type': 'success', + rules = self.env['unifi.firewall.rule'].sync_firewall_rules(self) + + # Afficher un message de succès + # La méthode sync_firewall_rules retourne True/False et non une liste + # Récupérer le nombre de règles de pare-feu pour ce site + rule_count = self.env['unifi.firewall.rule'].search_count([('site_id', '=', self.id)]) + + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Firewall Rule Synchronization'), + 'message': _(f'{rule_count} règles de pare-feu synchronisées avec succès!'), + 'sticky': False, + 'type': 'success', + } } + ) + + # Retourner la vue des règles de pare-feu + return { + 'type': 'ir.actions.act_window', + 'name': _('Firewall Rules'), + 'res_model': 'unifi.firewall.rule', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', } except Exception as e: return { @@ -892,17 +2665,36 @@ class UnifiSite(models.Model): self.ensure_one() try: # Utiliser la méthode de synchronisation du modèle unifi.port.forward - self.env['unifi.port.forward'].sync_port_forwards(self) - - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Port Forward Synchronization'), - 'message': _('Port forwards synchronized successfully!'), - 'sticky': False, - 'type': 'success', + port_forwards = self.env['unifi.port.forward'].sync_port_forwards(self) + + # Afficher un message de succès + # La méthode sync_port_forwards retourne True/False et non une liste + # Récupérer le nombre de redirections de port pour ce site + port_forward_count = self.env['unifi.port.forward'].search_count([('site_id', '=', self.id)]) + + self.env['bus.bus']._sendone( + self.env.user.partner_id, + 'web_client.action', + { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Port Forward Synchronization'), + 'message': _(f'{port_forward_count} redirections de port synchronisées avec succès!'), + 'sticky': False, + 'type': 'success', + } } + ) + + # Retourner la vue des redirections de port + return { + 'type': 'ir.actions.act_window', + 'name': _('Port Forwards'), + 'res_model': 'unifi.port.forward', + 'domain': [('site_id', '=', self.id)], + 'view_mode': 'list,form', + 'target': 'current', } except Exception as e: return { @@ -952,81 +2744,9 @@ class UnifiSite(models.Model): } } - def action_test_connection(self): - """Test the connection to the UniFi site""" - self.ensure_one() - try: - if self.api_type == 'controller': - connection_result = self._test_controller_connection() - elif self.api_type == 'site_manager': - connection_result = self._test_site_manager_connection() - else: - connection_result = False - - if connection_result: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Connection Test'), - 'message': _('Connection successful!'), - 'sticky': False, - 'type': 'success', - } - } - else: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Connection Test'), - 'message': _('Connection failed. Please check your settings.'), - 'sticky': True, - 'type': 'danger', - } - } - except Exception as e: - return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { - 'title': _('Connection Test'), - 'message': _('Error: %s') % str(e), - 'sticky': True, - 'type': 'danger', - } - } + # API-specific methods - def _test_controller_connection(self): - """Test connection to the Controller API""" - try: - # Récupérer l'objet controller associé à ce site - controller = self.env['unifi.site.controller'].search([('site_id', '=', self.id)], limit=1) - if not controller: - _logger.error('Controller not found for site %s', self.name) - return False - - # Déléguer le test de connexion au modèle controller - return controller._authenticate() - except Exception as e: - _logger.error('Error testing Controller API connection: %s', str(e)) - return False - - def _test_site_manager_connection(self): - """Test connection to the Site Manager API""" - try: - # Récupérer l'objet site manager associé à ce site - site_manager = self.env['unifi.site.manager'].search([('site_id', '=', self.id)], limit=1) - if not site_manager: - _logger.error('Site Manager not found for site %s', self.name) - return False - - # Déléguer le test de connexion au modèle site manager - return site_manager._test_site_manager_connection() - except Exception as e: - _logger.error('Error testing Site Manager API connection: %s', str(e)) - return False def _sync_controller(self): """Synchronize data with the Controller API @@ -1052,19 +2772,11 @@ class UnifiSite(models.Model): 'api_type': 'controller', }) - # Récupérer l'objet controller associé à ce site - controller = self.env['unifi.site.controller'].search([('site_id', '=', self.id)], limit=1) - if not controller: - if sync_job: - sync_job.write({ - 'end_time': fields.Datetime.now(), - 'state': 'failed', - 'message': 'Controller not found', - }) - return False - + # Puisque nous avons fusionné les modèles, le site lui-même est le contrôleur + # Nous pouvons donc utiliser directement les méthodes du site + # Authenticate with the Controller API - if not controller._authenticate(): + if not self._test_controller_connection(): if sync_job: sync_job.write({ 'end_time': fields.Datetime.now(), @@ -1078,7 +2790,8 @@ class UnifiSite(models.Model): # Synchronize system info try: - system_info_data = controller.get_system_info_data(self) + # Utiliser une méthode du site pour récupérer les informations système + system_info_data = self._get_system_info_data() if system_info_data: # Process and store system info data # TODO: Implement system info synchronization @@ -1093,22 +2806,56 @@ class UnifiSite(models.Model): # Synchronize devices try: - device_data = controller.get_device_data(self) + _logger.info("=== DÉBUT DE LA SYNCHRONISATION DES APPAREILS ===") + # Utiliser une méthode du site pour récupérer les données des appareils + device_data = self._get_device_data() + if device_data: - # Process and store device data - # TODO: Implement device synchronization - sync_messages.append('Devices synchronized') + _logger.info(f"Données d'appareils reçues: {type(device_data)}") + + # Vérifier la structure des données + if isinstance(device_data, dict): + _logger.info(f"Clés dans les données: {list(device_data.keys())}") + + # La plupart des API UniFi renvoient les données dans une clé 'data' + devices = device_data.get('data', []) + if not devices and 'devices' in device_data: + devices = device_data.get('devices', []) + + _logger.info(f"Nombre d'appareils trouvés: {len(devices)}") + + if devices: + # Afficher des informations sur le premier appareil pour débogage + first_device = devices[0] + _logger.info(f"Premier appareil: {first_device.get('name', 'Sans nom')}") + _logger.info(f"MAC: {first_device.get('mac', 'N/A')}") + _logger.info(f"Modèle: {first_device.get('model', 'N/A')}") + + # TODO: Implémenter la synchronisation des appareils + # Pour l'instant, juste compter les appareils + sync_messages.append(f'{len(devices)} appareils trouvés') + else: + _logger.warning("Aucun appareil trouvé dans les données") + sync_messages.append('Aucun appareil trouvé') + else: + _logger.warning(f"Format de données inattendu: {type(device_data)}") + sync_messages.append('Format de données inattendu') + success = False else: - sync_messages.append('Failed to retrieve devices') + _logger.error("Impossible de récupérer les données des appareils") + sync_messages.append('Échec de la récupération des appareils') success = False + _logger.info("=== FIN DE LA SYNCHRONISATION DES APPAREILS ===") except Exception as e: - sync_messages.append(f'Error synchronizing devices: {str(e)}') - _logger.error('Error synchronizing devices: %s', str(e)) + sync_messages.append(f'Erreur lors de la synchronisation des appareils: {str(e)}') + _logger.error('Erreur lors de la synchronisation des appareils: %s', str(e)) + _logger.exception("Détails de l'erreur:") success = False # Synchronize networks try: - network_data = controller.get_network_data(self) + # Utiliser une méthode du site pour récupérer les données des réseaux + network_data = self._get_network_data() if network_data: # Process and store network data # TODO: Implement network synchronization @@ -1123,7 +2870,8 @@ class UnifiSite(models.Model): # Synchronize VLANs try: - vlan_data = controller.get_vlan_data(self) + # Utiliser une méthode du site pour récupérer les données des VLANs + vlan_data = self._get_vlan_data() if vlan_data: # Process and store VLAN data # TODO: Implement VLAN synchronization @@ -1138,7 +2886,8 @@ class UnifiSite(models.Model): # Synchronize users try: - user_data = controller.get_user_data(self) + # Utiliser une méthode du site pour récupérer les données des utilisateurs + user_data = self._get_user_data() if user_data: # Process and store user data # TODO: Implement user synchronization @@ -1153,7 +2902,8 @@ class UnifiSite(models.Model): # Synchronize firewall rules try: - firewall_data = controller.get_firewall_data(self) + # Utiliser une méthode du site pour récupérer les données du pare-feu + firewall_data = self._get_firewall_data() if firewall_data: # Process and store firewall data # TODO: Implement firewall rule synchronization @@ -1168,7 +2918,8 @@ class UnifiSite(models.Model): # Synchronize port forwards try: - port_forward_data = controller.get_port_forward_data(self) + # Utiliser une méthode du site pour récupérer les données de redirection de port + port_forward_data = self._get_port_forward_data() if port_forward_data: # Process and store port forward data # TODO: Implement port forward synchronization @@ -1181,14 +2932,13 @@ class UnifiSite(models.Model): _logger.error('Error synchronizing port forwards: %s', str(e)) success = False - # Logout from the Controller API - controller._logout() + # Pas besoin de déconnexion explicite puisque nous utilisons directement les méthodes du site # Update sync job if sync_job: sync_job.write({ 'end_time': fields.Datetime.now(), - 'state': 'completed' if success else 'partial', + 'state': 'completed' if success else 'failed', 'message': '\n'.join(sync_messages), }) @@ -1232,19 +2982,19 @@ class UnifiSite(models.Model): 'api_type': 'site_manager', }) - # Récupérer l'objet site manager associé à ce site - site_manager = self.env['unifi.site.manager'].search([('id', '=', self.id)], limit=1) - if not site_manager: + # Vérifier que l'API est configurée correctement + if self.api_type != 'site_manager': if sync_job: sync_job.write({ 'end_time': fields.Datetime.now(), 'state': 'failed', - 'message': 'Site Manager not found', + 'message': 'Incorrect API type: site_manager required', }) return False - + # Test the connection to ensure we can authenticate - if not site_manager.test_connection(): + # Use the internal method directly + if not self._test_site_manager_connection(): if sync_job: sync_job.write({ 'end_time': fields.Datetime.now(), @@ -1258,7 +3008,8 @@ class UnifiSite(models.Model): # Synchronize system info try: - system_info_data = site_manager.get_system_info_data(self) + # Use the integrated method directly instead of calling site_manager model + system_info_data = self._get_site_manager_system_info_data() if system_info_data: # Process and store system info data # TODO: Implement system info synchronization @@ -1273,7 +3024,8 @@ class UnifiSite(models.Model): # Synchronize devices try: - device_data = site_manager.get_device_data(self) + # Use the integrated method directly instead of calling site_manager model + device_data = self._get_site_manager_device_data() if device_data: # Process and store device data # TODO: Implement device synchronization @@ -1288,7 +3040,8 @@ class UnifiSite(models.Model): # Synchronize networks try: - network_data = site_manager.get_network_data(self) + # Use the integrated method directly instead of calling site_manager model + network_data = self._get_site_manager_network_data() if network_data: # Process and store network data # TODO: Implement network synchronization @@ -1303,7 +3056,8 @@ class UnifiSite(models.Model): # Synchronize VLANs try: - vlan_data = site_manager.get_vlan_data(self) + # Use the integrated method directly instead of calling site_manager model + vlan_data = self._get_site_manager_vlan_data() if vlan_data: # Process and store VLAN data # TODO: Implement VLAN synchronization @@ -1318,7 +3072,8 @@ class UnifiSite(models.Model): # Synchronize users try: - user_data = site_manager.get_user_data(self) + # Use the integrated method directly instead of calling site_manager model + user_data = self._get_site_manager_user_data() if user_data: # Process and store user data # TODO: Implement user synchronization @@ -1333,7 +3088,8 @@ class UnifiSite(models.Model): # Synchronize firewall rules try: - firewall_data = site_manager.get_firewall_data(self) + # Use the integrated method directly instead of calling site_manager model + firewall_data = self._get_site_manager_firewall_data() if firewall_data: # Process and store firewall data # TODO: Implement firewall rule synchronization @@ -1348,7 +3104,8 @@ class UnifiSite(models.Model): # Synchronize port forwards try: - port_forward_data = site_manager.get_port_forward_data(self) + # Use the integrated method directly instead of calling site_manager model + port_forward_data = self._get_site_manager_port_forward_data() if port_forward_data: # Process and store port forward data # TODO: Implement port forward synchronization @@ -1365,7 +3122,7 @@ class UnifiSite(models.Model): if sync_job: sync_job.write({ 'end_time': fields.Datetime.now(), - 'state': 'completed' if success else 'partial', + 'state': 'completed' if success else 'failed', 'message': '\n'.join(sync_messages), }) @@ -1448,26 +3205,1159 @@ class UnifiSite(models.Model): """ self.ensure_one() - # Déterminer le type d'API à utiliser + # Determine which API implementation to use if self.api_type == 'controller': - # Utiliser l'API Controller - return self.env['unifi.site.controller'].get_device_data(self) + # Use Controller API implementation + # This is now directly implemented here instead of delegating to another model + return self._get_controller_device_data() elif self.api_type == 'site_manager': - # Utiliser l'API Site Manager - return self.env['unifi.site.manager'].get_device_data(self) + # Use Site Manager API implementation + # This is now directly implemented here instead of delegating to another model + return self._get_site_manager_device_data() + + def _create_api_log(self, api_method, message_text, direction): + """Create a new API log entry + + Args: + api_method: API method being called (e.g., 'get_device_data') + message_text: Log message + direction: Direction of the API call (e.g., 'outgoing', 'incoming') + + Returns: + Record: Newly created API log record + """ + try: + # Déterminer le type d'API en fonction du contexte + api_type = self.api_type or 'controller' + + # Créer un endpoint basé sur le nom de la méthode + endpoint = f"/api/{api_method}" + + # Déterminer la méthode HTTP en fonction de la direction + http_method = 'GET' if direction == 'outgoing' else 'POST' + + # Create a new api.log record + api_log_vals = { + 'site_id': self.id, + 'api_type': api_type, + 'endpoint': endpoint, + 'method': http_method, + 'error_message': message_text if direction != 'outgoing' else None, + 'start_time': fields.Datetime.now(), + } + # Create and return the log record + return self.env['unifi.api.log'].create(api_log_vals) + except Exception as e: + _logger.error('Error creating API log: %s', str(e)) + return False + + def _get_controller_device_data(self): + """Get device data from UniFi Controller API + + This method directly implements the device data retrieval logic for the Controller API type. + It was previously in the unifi.site.controller model, but has been integrated here. + + Returns: + list: Device data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_device_data', 'Getting device data from Controller API', 'outgoing') + + try: + # Implement the Controller-specific API call logic here + # This would be similar to what was in the unifi.site.controller model + base_url = f"https://{self.host}:{self.port}" + endpoint = f"/api/s/{self.site_id}/stat/device" # Using site_id field + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved device data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting device data from Controller API: %s", str(e)) + return False + + def _get_site_manager_device_data(self): + """Get device data from UniFi Site Manager API + + This method directly implements the device data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: Device data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_device_data', 'Getting device data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # This would be similar to what was in the unifi.site.manager model + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved device data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting device data from Site Manager API: %s", str(e)) + return False else: - # Type d'API non pris en charge - _logger.error("Type d'API non pris en charge: %s", self.api_type) + # Unsupported API type + _logger.error("Unsupported API type: %s", self.api_type) + return False + + def _get_site_manager_vlan_data(self): + """Get VLAN data from UniFi Site Manager API + + This method directly implements the VLAN data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: VLAN data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_vlan_data', 'Getting VLAN data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved VLAN data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting VLAN data from Site Manager API: %s", str(e)) + return False + + def _get_site_manager_user_data(self): + """Get user data from UniFi Site Manager API + + This method directly implements the user data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: User data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_user_data', 'Getting user data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved user data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting user data from Site Manager API: %s", str(e)) + return False + + def _get_site_manager_firewall_data(self): + """Get firewall data from UniFi Site Manager API + + This method directly implements the firewall data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: Firewall data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_firewall_data', 'Getting firewall data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved firewall data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting firewall data from Site Manager API: %s", str(e)) + return False + + def _get_site_manager_port_forward_data(self): + """Get port forward data from UniFi Site Manager API + + This method directly implements the port forward data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: Port forward data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_port_forward_data', 'Getting port forward data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved port forward data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting port forward data from Site Manager API: %s", str(e)) + return False + + def _get_site_manager_system_info_data(self): + """Get system info data from UniFi Site Manager API + + This method directly implements the system info data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + dict: System info data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_system_info_data', 'Getting system info data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = {} + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved system info data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting system info data from Site Manager API: %s", str(e)) + return False + + def _get_site_manager_dns_data(self): + """Get DNS data from UniFi Site Manager API + + This method directly implements the DNS data retrieval logic for the Site Manager API type. + + Returns: + list: DNS data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_dns_data', 'Getting DNS data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved DNS data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting DNS data from Site Manager API: %s", str(e)) + return False + + def _get_controller_wifi_data(self): + """Get WiFi network data from UniFi Controller API + + This method directly implements the WiFi network data retrieval logic for the Controller API type. + + Returns: + list: WiFi network data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_wifi_data', 'Getting WiFi network data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données des réseaux WiFi") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/wlanconf" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/wlanconf" + + _logger.info(f"Récupération des réseaux WiFi depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête HTTP + url = f"{base_url}{endpoint}" + _logger.info(f"Envoi de la requête GET à {url}") + + response = requests.get( + url, + headers=headers, + cookies=cookies, + verify=self.verify_ssl + ) + + # Vérifier le code de statut de la réponse + if response.status_code != 200: + _logger.error(f"Erreur lors de la récupération des données des réseaux WiFi: {response.status_code} - {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: HTTP {response.status_code}', + 'status': 'error' + }) + return False + + # Analyser la réponse JSON + try: + result = response.json() + _logger.info(f"Réponse reçue: {type(result)}") + + # Enregistrer les détails de la réponse dans le log API + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': 'Success: Retrieved WiFi network data', + 'status': 'success' + }) + + # La plupart des API UniFi renvoient les données dans une clé 'data' + if isinstance(result, dict) and 'data' in result: + return result['data'] + return result + + except json.JSONDecodeError as e: + _logger.error(f"Erreur lors de l'analyse de la réponse JSON: {str(e)}") + _logger.error(f"Contenu de la réponse: {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: Invalid JSON response - {str(e)}', + 'status': 'error' + }) + return False + + except Exception as e: + # Log the error + _logger.exception(f"Erreur lors de la récupération des données des réseaux WiFi: {str(e)}") + self._update_api_log(api_log, { + 'message': f'Error: {str(e)}', + 'status': 'error' + }) + return False + + def _get_controller_user_data(self): + """Get user data from UniFi Controller API + + This method directly implements the user data retrieval logic for the Controller API type. + + Returns: + list: User data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_user_data', 'Getting user data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données des utilisateurs") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/user" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/user" + + _logger.info(f"Récupération des utilisateurs depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête HTTP + url = f"{base_url}{endpoint}" + _logger.info(f"Envoi de la requête GET à {url}") + + response = requests.get( + url, + headers=headers, + cookies=cookies, + verify=self.verify_ssl + ) + + # Vérifier le code de statut de la réponse + if response.status_code != 200: + _logger.error(f"Erreur lors de la récupération des données des utilisateurs: {response.status_code} - {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: HTTP {response.status_code}', + 'status': 'error' + }) + return False + + # Analyser la réponse JSON + try: + result = response.json() + _logger.info(f"Réponse reçue: {type(result)}") + + # Enregistrer les détails de la réponse dans le log API + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': 'Success: Retrieved user data', + 'status': 'success' + }) + + # La plupart des API UniFi renvoient les données dans une clé 'data' + if isinstance(result, dict) and 'data' in result: + return result['data'] + return result + + except json.JSONDecodeError as e: + _logger.error(f"Erreur lors de l'analyse de la réponse JSON: {str(e)}") + _logger.error(f"Contenu de la réponse: {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: Invalid JSON response - {str(e)}', + 'status': 'error' + }) + return False + + except Exception as e: + # Log the error + _logger.exception(f"Erreur lors de la récupération des données des utilisateurs: {str(e)}") + self._update_api_log(api_log, { + 'message': f'Error: {str(e)}', + 'status': 'error' + }) + return False + + def _get_controller_get_firewall_data(self): + """Get firewall rules data from UniFi Controller API + + This method directly implements the firewall rules data retrieval logic for the Controller API type. + + Returns: + list: Firewall rules data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_firewall_data', 'Getting firewall rules data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données des règles de pare-feu") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/firewallrule" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/firewallrule" + + _logger.info(f"Récupération des règles de pare-feu depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête GET pour récupérer les données des règles de pare-feu + response = requests.get( + f"{base_url}{endpoint}", + headers=headers, + cookies=cookies, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Vérifier si la requête a réussi + if response.status_code != 200: + error_msg = f"Erreur lors de la récupération des règles de pare-feu: {response.status_code} - {response.text}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error', 'response': response.text}) + return False + + # Analyser la réponse JSON + response_data = response.json() + + # Extraire les données des règles de pare-feu + firewall_data = response_data.get('data', []) + + # Mettre à jour le log API avec le succès + self._update_api_log(api_log, { + 'message': f"Succès: {len(firewall_data)} règles de pare-feu récupérées", + 'status': 'success', + 'response': json.dumps(response_data) + }) + + return firewall_data + except Exception as e: + # Log the error + error_msg = f"Erreur lors de la récupération des règles de pare-feu: {str(e)}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error'}) + return False + + def _get_controller_get_port_forward_data(self): + """Get port forwarding data from UniFi Controller API + + This method directly implements the port forwarding data retrieval logic for the Controller API type. + + Returns: + list: Port forwarding data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_port_forward_data', 'Getting port forwarding data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données de redirection de port") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/portforward" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/portforward" + + _logger.info(f"Récupération des redirections de port depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête GET pour récupérer les données de redirection de port + response = requests.get( + f"{base_url}{endpoint}", + headers=headers, + cookies=cookies, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Vérifier si la requête a réussi + if response.status_code != 200: + error_msg = f"Erreur lors de la récupération des redirections de port: {response.status_code} - {response.text}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error', 'response': response.text}) + return False + + # Analyser la réponse JSON + response_data = response.json() + + # Extraire les données des redirections de port + port_forward_data = response_data.get('data', []) + + # Mettre à jour le log API avec le succès + self._update_api_log(api_log, { + 'message': f"Succès: {len(port_forward_data)} redirections de port récupérées", + 'status': 'success', + 'response': json.dumps(response_data) + }) + + return port_forward_data + except Exception as e: + # Log the error + error_msg = f"Erreur lors de la récupération des redirections de port: {str(e)}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error'}) + return False + + def _get_controller_get_dns_data(self): + """Get DNS data from UniFi Controller API + + This method directly implements the DNS data retrieval logic for the Controller API type. + + Returns: + list: DNS data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_dns_data', 'Getting DNS data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données DNS") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/setting" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/setting" + + _logger.info(f"Récupération des entrées DNS depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête GET pour récupérer les données DNS + response = requests.get( + f"{base_url}{endpoint}", + headers=headers, + cookies=cookies, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Vérifier si la requête a réussi + if response.status_code != 200: + error_msg = f"Erreur lors de la récupération des entrées DNS: {response.status_code} - {response.text}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error', 'response': response.text}) + return False + + # Analyser la réponse JSON + response_data = response.json() + + # Extraire les données DNS des paramètres du site + settings_data = response_data.get('data', []) + + # Rechercher les paramètres DNS dans les données de configuration + dns_entries = [] + for setting in settings_data: + if setting.get('key') == 'networks': + networks = setting.get('values', []) + for network in networks: + # Extraire les serveurs DNS configurés dans chaque réseau + dns_servers = network.get('dns_servers', []) + if dns_servers: + for i, dns_server in enumerate(dns_servers): + if dns_server and dns_server.strip(): + dns_entries.append({ + 'hostname': f"dns-server-{network.get('name', '')}-{i+1}", + 'ip_address': dns_server, + 'description': f"DNS Server {i+1} for network {network.get('name', '')}", + 'enabled': True, + 'unifi_id': f"{network.get('_id', '')}-dns-{i}", + 'entry_type': 'server' + }) + + # Extraire les entrées DNS statiques configurées dans chaque réseau + static_dns = network.get('static_dns', []) + if static_dns: + for i, entry in enumerate(static_dns): + dns_entries.append({ + 'hostname': entry.get('name', f"static-dns-{i}"), + 'ip_address': entry.get('ip', ''), + 'description': f"Static DNS entry for {entry.get('name', '')}", + 'enabled': True, + 'unifi_id': f"{network.get('_id', '')}-static-dns-{i}", + 'entry_type': 'static' + }) + + # Si aucune entrée DNS n'est trouvée, essayer de récupérer les paramètres DNS généraux + if not dns_entries: + for setting in settings_data: + if setting.get('key') == 'dns': + dns_config = setting.get('values', {}) + servers = dns_config.get('servers', []) + for i, server in enumerate(servers): + if server and server.strip(): + dns_entries.append({ + 'hostname': f"global-dns-server-{i+1}", + 'ip_address': server, + 'description': f"Global DNS Server {i+1}", + 'enabled': True, + 'unifi_id': f"global-dns-{i}", + 'entry_type': 'server' + }) + + formatted_dns_data = dns_entries + + # Mettre à jour le log API avec le succès, même si aucune entrée n'est trouvée + message = f"Succès: {len(formatted_dns_data)} entrées DNS récupérées" + if not formatted_dns_data: + message = "Succès: Aucune entrée DNS trouvée dans la configuration" + + self._update_api_log(api_log, { + 'message': message, + 'status': 'success', + 'response': json.dumps(response_data) + }) + + # Retourner les données formatées, même si c'est une liste vide + return formatted_dns_data + except Exception as e: + # Log the error + error_msg = f"Erreur lors de la récupération des entrées DNS: {str(e)}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error'}) + return False + + def _get_controller_get_system_info_data(self): + """Get system information data from UniFi Controller API + + This method directly implements the system information data retrieval logic for the Controller API type. + + Returns: + list: System information data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_system_info_data', 'Getting system information data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données d'information système") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/stat/device" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/stat/device" + + _logger.info(f"Récupération des informations système depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête GET pour récupérer les données d'information système + response = requests.get( + f"{base_url}{endpoint}", + headers=headers, + cookies=cookies, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Vérifier si la requête a réussi + if response.status_code != 200: + error_msg = f"Erreur lors de la récupération des informations système: {response.status_code} - {response.text}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error', 'response': response.text}) + return False + + # Analyser la réponse JSON + response_data = response.json() + + # Extraire les données d'information système + devices = response_data.get('data', []) + + # Transformer les données des appareils en format d'information système + system_info_data = [] + for device in devices: + # Extraire les informations pertinentes pour le système + system_info = { + 'hostname': device.get('name') or device.get('hostname', 'Unknown'), + 'version': device.get('version', 'Unknown'), + 'model': device.get('model', 'Unknown'), + 'uptime': device.get('uptime', 0), + 'serial': device.get('serial', 'Unknown'), + 'mac_address': device.get('mac', 'Unknown'), + 'device_id': device.get('_id', ''), + 'ip_address': device.get('ip', 'Unknown'), + 'cpu_usage': device.get('system_stats', {}).get('cpu', 0), + 'memory_usage': device.get('system_stats', {}).get('mem', 0), + 'temperature': device.get('general_temperature', 0) + } + system_info_data.append(system_info) + + # Mettre à jour le log API avec le succès + self._update_api_log(api_log, { + 'message': f"Succès: {len(system_info_data)} informations système récupérées", + 'status': 'success', + 'response': json.dumps(response_data) + }) + + return system_info_data + except Exception as e: + # Log the error + error_msg = f"Erreur lors de la récupération des informations système: {str(e)}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error'}) + return False + + def _get_controller_get_vlan_data(self): + """Get VLAN data from UniFi Controller API + + This method directly implements the VLAN data retrieval logic for the Controller API type. + + Returns: + list: VLAN data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_vlan_data', 'Getting VLAN data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données des VLANs") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/networkconf" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/networkconf" + + _logger.info(f"Récupération des VLANs depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête GET pour récupérer les données des VLANs + response = requests.get( + f"{base_url}{endpoint}", + headers=headers, + cookies=cookies, + verify=self.verify_ssl, + timeout=self.timeout + ) + + # Vérifier si la requête a réussi + if response.status_code != 200: + error_msg = f"Erreur lors de la récupération des VLANs: {response.status_code} - {response.text}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error', 'response': response.text}) + return False + + # Analyser la réponse JSON + response_data = response.json() + + # Extraire les données des VLANs + vlan_data = [] + for network in response_data.get('data', []): + # Filtrer uniquement les réseaux qui ont un VLAN ID + if 'vlan' in network and network.get('vlan') not in [None, 0]: + # Transformer les données du réseau en format VLAN + vlan = { + 'vlan_id': network.get('vlan'), + 'name': network.get('name', f"VLAN {network.get('vlan')}"), + 'purpose': network.get('purpose', 'corporate'), + 'enabled': network.get('enabled', True), + '_id': network.get('_id'), + 'subnet': network.get('ip_subnet'), + 'created_at': network.get('created', fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S')), + 'updated_at': network.get('updated', fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + } + vlan_data.append(vlan) + + # Mettre à jour le log API avec le succès + self._update_api_log(api_log, { + 'message': f"Succès: {len(vlan_data)} VLANs récupérés", + 'status': 'success', + 'response': json.dumps(response_data) + }) + + return vlan_data + except Exception as e: + # Log the error + error_msg = f"Erreur lors de la récupération des VLANs: {str(e)}" + _logger.error(error_msg) + self._update_api_log(api_log, {'message': error_msg, 'status': 'error'}) + return False + + def _get_controller_network_data(self): + """Get network data from UniFi Controller API + + This method directly implements the network data retrieval logic for the Controller API type. + It was previously in the unifi.site.controller model, but has been integrated here. + + Returns: + list: Network data from Controller API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_network_data', 'Getting network data from Controller API', 'outgoing') + + try: + # Vérifier si nous avons une session d'authentification valide + if not self._check_auth_session(): + _logger.error("Pas de session d'authentification valide pour récupérer les données des réseaux") + return False + + # Construire l'URL de base en fonction du type de contrôleur (standard ou UDM Pro) + base_url = f"https://{self.host}:{self.port}" + + # Déterminer si nous avons affaire à un UDM Pro + is_udm_pro = self.auth_session_id and self.auth_session_id.is_udm_pro + + # Construire l'endpoint en fonction du type de contrôleur + if is_udm_pro: + # Pour UDM Pro, ajouter le préfixe /proxy/network + endpoint = f"/proxy/network/api/s/{self.site_id}/rest/networkconf" + else: + # Pour les contrôleurs standard + endpoint = f"/api/s/{self.site_id}/rest/networkconf" + + _logger.info(f"Récupération des réseaux depuis l'endpoint: {endpoint}") + + # Préparer les en-têtes de la requête + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Ajouter le token d'authentification si disponible (pour UDM Pro) + if is_udm_pro and self.auth_session_id.token: + headers['Authorization'] = f"Bearer {self.auth_session_id.token}" + + # Préparer les cookies pour l'authentification + cookies = self._get_auth_cookies() + if not cookies: + _logger.error("Impossible de récupérer les cookies d'authentification") + return False + + # Effectuer la requête HTTP + url = f"{base_url}{endpoint}" + _logger.info(f"Envoi de la requête GET à {url}") + + response = requests.get( + url, + headers=headers, + cookies=cookies, + verify=self.verify_ssl + ) + + # Vérifier le code de statut de la réponse + if response.status_code != 200: + _logger.error(f"Erreur lors de la récupération des données des réseaux: {response.status_code} - {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: HTTP {response.status_code}', + 'status': 'error' + }) + return False + + # Analyser la réponse JSON + try: + result = response.json() + _logger.info(f"Réponse reçue: {type(result)}") + + # Enregistrer les détails de la réponse dans le log API + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': 'Success: Retrieved network data', + 'status': 'success' + }) + + # La plupart des API UniFi renvoient les données dans une clé 'data' + if isinstance(result, dict) and 'data' in result: + return result['data'] + return result + + except json.JSONDecodeError as e: + _logger.error(f"Erreur lors de l'analyse de la réponse JSON: {str(e)}") + _logger.error(f"Contenu de la réponse: {response.text}") + self._update_api_log(api_log, { + 'status_code': response.status_code, + 'response_body': response.text, + 'message': f'Error: Invalid JSON response - {str(e)}', + 'status': 'error' + }) + return False + + except Exception as e: + # Log the error + _logger.exception(f"Erreur lors de la récupération des données des réseaux: {str(e)}") + self._update_api_log(api_log, { + 'message': f'Error: {str(e)}', + 'status': 'error' + }) + return False + + def _get_site_manager_network_data(self): + """Get network data from UniFi Site Manager API + + This method directly implements the network data retrieval logic for the Site Manager API type. + It was previously in the unifi.site.manager model, but has been integrated here. + + Returns: + list: Network data from Site Manager API + """ + # Create a log entry for this API call + api_log = self._create_api_log('get_network_data', 'Getting network data from Site Manager API', 'outgoing') + + try: + # Implement the Site Manager-specific API call logic here + # Make the API request and process the response + # For now, this is a placeholder + result = [] + + # Log the successful API call + self._update_api_log(api_log, {'message': 'Success: Retrieved network data', 'status': 'success'}) + return result + except Exception as e: + # Log the error + self._update_api_log(api_log, {'message': f'Error: {str(e)}', 'status': 'error'}) + _logger.error("Error getting network data from Site Manager API: %s", str(e)) return False def _delegate_api_method(self, method_name): """Délègue l'appel d'une méthode à l'API appropriée - Cette méthode générique permet de déléguer l'appel d'une méthode - au modèle spécifique en fonction du type d'API configuré. + Cette méthode générique permet d'appeler la méthode interne appropriée + en fonction du type d'API configuré. Args: - method_name: Nom de la méthode à appeler + method_name: Nom de la méthode à appeler (sans le préfixe) Returns: Le résultat de la méthode appelée, ou False si le type d'API n'est pas pris en charge @@ -1476,11 +4366,21 @@ class UnifiSite(models.Model): # Déterminer le type d'API à utiliser if self.api_type == 'controller': - # Utiliser l'API Controller - return getattr(self.env['unifi.site.controller'], method_name)(self) + # Utiliser l'implémentation Controller + controller_method = f"_get_controller_{method_name}" + if hasattr(self, controller_method): + return getattr(self, controller_method)() + else: + _logger.error(f"Méthode {controller_method} non implémentée") + return False elif self.api_type == 'site_manager': - # Utiliser l'API Site Manager - return getattr(self.env['unifi.site.manager'], method_name)(self) + # Utiliser l'implémentation Site Manager + site_manager_method = f"_get_site_manager_{method_name}" + if hasattr(self, site_manager_method): + return getattr(self, site_manager_method)() + else: + _logger.error(f"Méthode {site_manager_method} non implémentée") + return False else: # Type d'API non pris en charge _logger.error("Type d'API non pris en charge: %s", self.api_type) @@ -1564,11 +4464,11 @@ class UnifiSite(models.Model): """ self.ensure_one() - # Vérifier si le site a déjà un contrôleur ou un gestionnaire de site associé - controller = self.env['unifi.site.controller'].search([('site_id', '=', self.id)], limit=1) - site_manager = self.env['unifi.site.manager'].search([('site_id', '=', self.id)], limit=1) + # Dans la version refactorisée, nous n'avons plus besoin de vérifier si des anciens modèles + # sont associés puisque toute la logique est maintenant intégrée dans ce modèle - if not controller and not site_manager: + # Vérifier si le site a déjà été configuré pour une API + if not self.api_type: # Si aucun contrôleur ou gestionnaire de site n'est associé, ouvrir l'assistant d'importation return { 'name': _('Import UniFi Site'), @@ -1581,9 +4481,9 @@ class UnifiSite(models.Model): # Tester la connexion connection_success = False - if self.api_type == 'controller' and controller: + if self.api_type == 'controller': connection_success = self._test_controller_connection() - elif self.api_type == 'site_manager' and site_manager: + elif self.api_type == 'site_manager': connection_success = self._test_site_manager_connection() if connection_success: diff --git a/unifi_integration/models/unifi_site_controller.py b/unifi_integration/models/unifi_site_controller.py deleted file mode 100644 index f36a6fe..0000000 --- a/unifi_integration/models/unifi_site_controller.py +++ /dev/null @@ -1,1700 +0,0 @@ -# -*- coding: utf-8 -*- - -# These imports will work in an Odoo environment, even if your IDE marks them as not found -# pylint: disable=import-error -from odoo import models, fields, api, _ -from odoo.exceptions import UserError, ValidationError -# pylint: enable=import-error - -import json -import logging -import requests -import urllib3 -from datetime import datetime, timedelta -from typing import Dict, Tuple, List, Any -from requests.exceptions import RequestException, ConnectionError - -_logger = logging.getLogger(__name__) - -class UnifiSiteController(models.Model): - """Extension du modèle UnifiSite pour l'API Controller (Local) - - Cette classe ajoute les champs et méthodes spécifiques à l'API Controller local. - Elle est utilisée lorsque api_type = 'controller'. - """ - _name = 'unifi.site.controller' - _description = 'UniFi Site Controller API' - - # Champs pour établir la relation avec unifi.site - site_id = fields.Many2one( - comodel_name='unifi.site', - string='Site', - required=True, - ondelete='cascade', - help='Site associé à cette configuration API Controller' - ) - - # Champs nécessaires pour le fonctionnement du modèle - name = fields.Char(related='site_id.name', string='Nom', readonly=True) - api_type = fields.Selection(related='site_id.api_type', string='Type API', readonly=True) - verify_ssl = fields.Boolean(string='Verify SSL', default=True, help='Vérifier les certificats SSL. Désactivez cette option pour les certificats auto-signés.') - ssl_cert_file = fields.Binary(string='Certificat SSL personnalisé', attachment=True, help='Fichier de certificat SSL personnalisé (.pem ou .crt)') - ssl_cert_filename = fields.Char(string='Nom du fichier de certificat') - ssl_cert_path = fields.Char(string='Chemin du certificat', compute='_compute_ssl_cert_path', store=True, help='Chemin vers le fichier de certificat SSL') - last_sync = fields.Datetime(string='Dernière synchronisation') - - @api.depends('ssl_cert_file', 'ssl_cert_filename') - def _compute_ssl_cert_path(self): - """Calcule le chemin vers le fichier de certificat SSL - - Cette méthode est appelée lorsque le fichier de certificat SSL est modifié. - Elle sauvegarde le certificat dans un fichier temporaire et stocke le chemin. - """ - import tempfile - import os - import base64 - - for record in self: - if record.ssl_cert_file and record.ssl_cert_filename: - # Créer un fichier temporaire pour stocker le certificat - fd, path = tempfile.mkstemp(suffix='.pem') - try: - # Décoder le contenu du certificat et l'écrire dans le fichier - cert_content = base64.b64decode(record.ssl_cert_file) - os.write(fd, cert_content) - # Stocker le chemin du fichier - record.ssl_cert_path = path - finally: - os.close(fd) - else: - record.ssl_cert_path = False - - @api.model - def _check_required_fields(self, site): - """Vérifie que les champs requis pour l'API Controller sont renseignés - - Cette méthode est appelée par le modèle principal lors de la validation des contraintes. - - Args: - site: L'enregistrement du site à vérifier - - Raises: - ValidationError: Si des champs requis ne sont pas renseignés - """ - if not site.host: - raise ValidationError(_("Le champ 'Host' est requis pour l'API Controller.")) - - if not site.port: - raise ValidationError(_("Le champ 'Port' est requis pour l'API Controller.")) - - if not site.username: - raise ValidationError(_("Le champ 'Username' est requis pour l'API Controller.")) - - if not site.password: - raise ValidationError(_("Le champ 'Password' est requis pour l'API Controller.")) - - @api.model - def _create_or_update_api_log(self, api_log=None, **kwargs): - """Crée ou met à jour un enregistrement de log API - - Cette méthode d'aide simplifie la gestion des logs API en créant un nouvel enregistrement - si api_log est None, ou en mettant à jour l'enregistrement existant sinon. - - Args: - api_log: L'enregistrement de log API existant, ou None pour en créer un nouveau - **kwargs: Les valeurs à écrire dans l'enregistrement - - Returns: - L'enregistrement de log API créé ou mis à jour - """ - if not api_log: - # Valeurs par défaut pour la création - default_values = { - 'site_id': self.id, - 'api_type': 'controller', - 'method': 'GET', - 'endpoint': 'error_handler', - 'start_time': fields.Datetime.now() - } - # Fusionner les valeurs par défaut avec les valeurs fournies - values = {**default_values, **kwargs} - return self.env['unifi.api.log'].create(values) - else: - # Mettre à jour l'enregistrement existant - api_log.write(kwargs) - return api_log - - def _clear_irrelevant_fields(self, site): - """Nettoie les champs qui ne sont pas pertinents pour l'API Controller - - Cette méthode est appelée par le modèle principal lors du changement de type d'API. - Elle efface les champs spécifiques à l'API Site Manager. - - Args: - site: L'enregistrement du site à nettoyer - """ - # Effacer les champs spécifiques à l'API Site Manager - site.api_key = False - site.mfa_enabled = False - site.mfa_token = False - - # Controller configuration - Only used when api_type = 'controller' - controller_type = fields.Selection( - selection=[ - ('udm', 'UDM/UDR'), - ('software', 'Software Controller') - ], - string='Controller Type', - required=False, - default='udm', - - help='Type of UniFi controller managing this site' - ) - - # Controller API fields - Only used when api_type = 'controller' - host = fields.Char( - string='Host', - - help='IP address or hostname of the controller' - ) - - port = fields.Integer( - string='Port', - default=443, - - help='Port number (default: 443)' - ) - - username = fields.Char( - string='Username', - - help='Username for controller login' - ) - - password = fields.Char( - string='Password', - - help='Password for controller login' - ) - - # Champs pour la gestion de session - session_cookies = fields.Text( - string='Session Cookies', - help='Cookies de session pour l\'API Controller', - readonly=True, - copy=False - ) - - csrf_token = fields.Text( - string='CSRF Token', - help='Token CSRF pour l\'API Controller', - readonly=True, - copy=False - ) - - last_login = fields.Datetime( - string='Dernière connexion', - help='Date et heure de la dernière connexion réussie', - readonly=True, - copy=False - ) - - # Endpoints API communs à tous les types de contrôleurs - COMMON_ENDPOINTS = { - 'login': '/api/auth/login', - 'logout': '/api/auth/logout', - 'status': '/api/s/{site_id}/stat/status', - 'sites': '/api/self/sites', - 'devices': '/api/s/{site_id}/stat/device', - 'device': '/api/s/{site_id}/stat/device/{mac}', - 'clients': '/api/s/{site_id}/stat/sta', - 'client': '/api/s/{site_id}/stat/sta/{mac}', - 'networks': '/api/s/{site_id}/rest/networkconf', - 'wlans': '/api/s/{site_id}/rest/wlanconf', - 'users': '/api/s/{site_id}/list/user', - 'firewall_rules': '/api/s/{site_id}/rest/firewallrule', - 'port_forwards': '/api/s/{site_id}/rest/portforward', - 'health': '/api/s/{site_id}/stat/health', - 'dashboard': '/api/s/{site_id}/stat/dashboard', - 'alarms': '/api/s/{site_id}/list/alarm', - 'events': '/api/s/{site_id}/stat/event', - 'dpi': '/api/s/{site_id}/stat/dpi', - 'settings': '/api/s/{site_id}/get/setting', - 'routing': '/api/s/{site_id}/rest/routing', - 'system_info': '/api/s/{site_id}/stat/sysinfo', - 'vouchers': '/api/s/{site_id}/stat/voucher', - 'hotspot': '/api/s/{site_id}/rest/hotspot', - } - - # Endpoints spécifiques au type de contrôleur UDM - UDM_ENDPOINTS = { - 'login': '/api/auth/login', - 'logout': '/api/auth/logout', - } - - # Endpoints spécifiques au type de contrôleur Cloud Key - CLOUD_KEY_ENDPOINTS = { - 'login': '/api/login', - 'logout': '/api/logout', - } - - def _get_endpoint(self, endpoint_name: str, site_id: str = 'default', **kwargs) -> str: - """Récupère l'URL complète d'un endpoint API en fonction du type de contrôleur - - Args: - endpoint_name: Nom de l'endpoint à récupérer - site_id: ID du site UniFi (par défaut: 'default') - **kwargs: Paramètres supplémentaires pour formater l'URL - - Returns: - str: URL complète de l'endpoint - """ - # Sélectionner les endpoints en fonction du type de contrôleur - if self.controller_type == 'udm': - endpoints = {**self.COMMON_ENDPOINTS, **self.UDM_ENDPOINTS} - elif self.controller_type == 'software': - endpoints = {**self.COMMON_ENDPOINTS, **self.CLOUD_KEY_ENDPOINTS} - else: - endpoints = self.COMMON_ENDPOINTS - - # Vérifier si l'endpoint existe - if endpoint_name not in endpoints: - _logger.error(f"Endpoint '{endpoint_name}' non trouvé pour le contrôleur de type '{self.controller_type}'") - return '' - - # Récupérer l'endpoint et le formater avec les paramètres - endpoint = endpoints[endpoint_name] - params = {'site_id': site_id, **kwargs} - - try: - return endpoint.format(**params) - except KeyError as e: - _logger.error(f"Paramètre manquant pour l'endpoint '{endpoint_name}': {str(e)}") - return '' - - def _get_base_url(self) -> str: - """Construit l'URL de base pour les requêtes API - - Returns: - str: URL de base pour les requêtes API - """ - return f"https://{self.host}:{self.port}" - - def _get_headers(self, csrf_token: str = '') -> Dict[str, str]: - """Prépare les en-têtes pour les requêtes API - - Args: - csrf_token: Token CSRF à inclure dans les en-têtes (optionnel) - - Returns: - Dict[str, str]: En-têtes pour les requêtes API - """ - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'User-Agent': 'Odoo UniFi Integration' - } - - # Ajouter le token CSRF si présent - if csrf_token: - headers['X-CSRF-Token'] = csrf_token - - return headers - - def _authenticate(self) -> bool: - """Authentifie la session auprès du contrôleur UniFi - - Returns: - bool: True si l'authentification a réussi, False sinon - """ - self.ensure_one() - - if self.api_type != 'controller': - return False - - # Désactiver les avertissements SSL si verify_ssl est False - if not self.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Créer une nouvelle session pour cette authentification - session = requests.Session() - - # Construire l'URL de connexion - base_url = self._get_base_url() - login_endpoint = self._get_endpoint('login') - login_url = f"{base_url}{login_endpoint}" - - # Préparer les données de connexion - login_data = { - 'username': self.username, - 'password': self.password, - 'remember': True - } - - # Préparer les en-têtes - headers = self._get_headers() - - # Initialiser le log API - auth_api_log = self.env['unifi.api.log'].create({ - 'site_id': self.site_id.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Déterminer comment gérer la vérification SSL - verify = self.verify_ssl - - # Si un certificat personnalisé est fourni, l'utiliser - if self.ssl_cert_path and self.verify_ssl: - verify = self.ssl_cert_path - _logger.info(f"Utilisation du certificat SSL personnalisé: {verify}") - - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=verify, - timeout=10 - ) - - # Mettre à jour le log API avec les données de réponse - auth_api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code == 200: - # Stocker les cookies de session et le token CSRF - self.session_cookies = json.dumps(dict(session.cookies.items())) - - # Extraire le token CSRF si présent - csrf_token = '' - if 'X-CSRF-Token' in response.headers: - csrf_token = response.headers['X-CSRF-Token'] - self.csrf_token = csrf_token - - # Mettre à jour la date de dernière connexion - self.last_login = fields.Datetime.now() - - return True - - # Journaliser l'échec de connexion - _logger.error(f"Échec de connexion à l'API Controller: {response.status_code} - {response.text}") - return False - - except Exception as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de l'authentification: {str(e)}" - auth_api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - def _logout(self) -> bool: - """Déconnecte la session du contrôleur UniFi - - Returns: - bool: True si la déconnexion a réussi, False sinon - """ - self.ensure_one() - - if self.api_type != 'controller': - return False - - # Vérifier si nous avons des cookies de session - if not self.session_cookies: - return True # Déjà déconnecté - - # Créer une nouvelle session pour cette déconnexion - session = requests.Session() - - # Restaurer les cookies de session - if self.session_cookies: - try: - cookies = json.loads(self.session_cookies) - for key, value in cookies.items(): - session.cookies.set(key, value) - except json.JSONDecodeError: - _logger.error("Impossible de décoder les cookies de session") - return False - - # Construire l'URL de déconnexion - base_url = self._get_base_url() - logout_endpoint = self._get_endpoint('logout') - logout_url = f"{base_url}{logout_endpoint}" - - # Préparer les en-têtes - headers = self._get_headers(self.csrf_token if self.csrf_token else '') - - # Initialiser le log API - logout_api_log = self.env['unifi.api.log'].create({ - 'site_id': self.site_id.id, - 'api_type': 'controller', - 'endpoint': logout_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - try: - - # Effectuer la requête de déconnexion - response = session.post( - logout_url, - headers=headers, - verify=self.verify_ssl, - timeout=10 - ) - - # Mettre à jour le log API avec les données de réponse - logout_api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Effacer les cookies de session et le token CSRF - self.session_cookies = False - self.csrf_token = False - - return response.status_code in [200, 204] - - except Exception as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la déconnexion: {str(e)}" - logout_api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - def _make_request(self, method: str, endpoint_name: str, site_id: str = '', - params: Dict[str, str] = {}, data: Dict[str, str] = {}, - retry_auth: bool = True) -> Tuple[bool, Dict, int]: - """Effectue une requête API au contrôleur UniFi - - Args: - method: Méthode HTTP à utiliser (GET, POST, PUT, DELETE) - endpoint_name: Nom de l'endpoint à appeler - site_id: ID du site UniFi (par défaut: '') - params: Paramètres de requête (pour GET et DELETE) - data: Données de requête (pour POST et PUT) - retry_auth: Si True, réessaie avec une nouvelle authentification en cas d'échec - - Returns: - Tuple[bool, Dict, int]: (Succès, Données de réponse, Code de statut HTTP) - """ - self.ensure_one() - - if self.api_type != 'controller': - return False, {"error": "Type d'API non pris en charge"}, 400 - - # Désactiver les avertissements SSL si verify_ssl est False - if not self.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Créer une nouvelle session pour cette requête - session = requests.Session() - - # Restaurer les cookies de session - if self.session_cookies: - try: - cookies = json.loads(self.session_cookies) - for key, value in cookies.items(): - session.cookies.set(key, value) - except json.JSONDecodeError: - _logger.error("Impossible de décoder les cookies de session") - if retry_auth: - # Tenter une nouvelle authentification - if self._authenticate(): - return self._make_request(method, endpoint_name, site_id, params, data, False) - return False, {"error": "Erreur de session"}, 401 - - # Construire l'URL complète - base_url = self._get_base_url() - endpoint = self._get_endpoint(endpoint_name, site_id) - url = f"{base_url}{endpoint}" - - # Préparer les en-têtes - headers = self._get_headers(self.csrf_token if self.csrf_token else '') - - # Journaliser l'appel API - request_api_log = self.env['unifi.api.log'].create({ - 'site_id': self.site_id.id, - 'api_type': 'controller', - 'endpoint': url, - 'method': method, - 'request_headers': json.dumps(headers), - 'request_params': json.dumps(params) if params else '', - 'request_body': json.dumps(data) if data else '', - 'start_time': fields.Datetime.now() - }) - - try: - # Déterminer comment gérer la vérification SSL - verify = self.verify_ssl - - # Si un certificat personnalisé est fourni, l'utiliser - if self.ssl_cert_path and self.verify_ssl: - verify = self.ssl_cert_path - _logger.debug(f"Utilisation du certificat SSL personnalisé: {verify}") - - # Effectuer la requête - if method == 'GET': - response = session.get(url, params=params, headers=headers, verify=verify, timeout=10) - elif method == 'POST': - response = session.post(url, json=data, headers=headers, verify=verify, timeout=10) - elif method == 'PUT': - response = session.put(url, json=data, headers=headers, verify=verify, timeout=10) - elif method == 'DELETE': - response = session.delete(url, params=params, headers=headers, verify=verify, timeout=10) - else: - return False, {"error": "Méthode HTTP non prise en charge"}, 400 - - # Mettre à jour le log API avec les données de réponse - request_api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la session a expiré (401 Unauthorized) - if response.status_code == 401 and retry_auth: - # Tenter une nouvelle authentification - if self._authenticate(): - return self._make_request(method, endpoint_name, site_id, params, data, False) - return False, {"error": "Session expirée et échec de réauthentification"}, 401 - - # Traiter la réponse - if response.status_code in [200, 201, 204]: - try: - # Certains endpoints peuvent retourner une réponse vide - if not response.text: - return True, {}, response.status_code - - # Décoder la réponse JSON - json_response = response.json() - return True, json_response, response.status_code - except json.JSONDecodeError: - return False, {"error": "Erreur de décodage JSON"}, response.status_code - - # Gérer les erreurs - return False, {"error": f"Erreur HTTP {response.status_code}"}, response.status_code - - except Exception as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la requête: {str(e)}" - request_api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False, {"error": str(e)}, 500 - finally: - # Fermer la session - session.close() - - def _get(self, endpoint_name: str, site_id: str = '', - params: Dict[str, str] = {}) -> Tuple[bool, Dict, int]: - """Effectue une requête GET - - Args: - endpoint_name: Nom de l'endpoint à appeler - site_id: ID du site UniFi (par défaut: '') - params: Paramètres de requête - - Returns: - Tuple[bool, Dict, int]: (Succès, Données de réponse, Code de statut HTTP) - """ - return self._make_request('GET', endpoint_name, site_id, params=params) - - def _post(self, endpoint_name: str, site_id: str = '', - data: Dict[str, str] = {}) -> Tuple[bool, Dict, int]: - """Effectue une requête POST - - Args: - endpoint_name: Nom de l'endpoint à appeler - site_id: ID du site UniFi (par défaut: '') - data: Données de requête - - Returns: - Tuple[bool, Dict, int]: (Succès, Données de réponse, Code de statut HTTP) - """ - return self._make_request('POST', endpoint_name, site_id, data=data) - - def _put(self, endpoint_name: str, site_id: str = '', - data: Dict[str, str] = {}) -> Tuple[bool, Dict, int]: - """Effectue une requête PUT - - Args: - endpoint_name: Nom de l'endpoint à appeler - site_id: ID du site UniFi (par défaut: '') - data: Données de requête - - Returns: - Tuple[bool, Dict, int]: (Succès, Données de réponse, Code de statut HTTP) - """ - return self._make_request('PUT', endpoint_name, site_id, data=data) - - def _delete(self, endpoint_name: str, site_id: str = '', - params: Dict[str, str] = {}) -> Tuple[bool, Dict, int]: - """Effectue une requête DELETE - - Args: - endpoint_name: Nom de l'endpoint à appeler - site_id: ID du site UniFi (par défaut: '') - params: Paramètres de requête - - Returns: - Tuple[bool, Dict, int]: (Succès, Données de réponse, Code de statut HTTP) - """ - return self._make_request('DELETE', endpoint_name, site_id, params=params) - - def _test_controller_connection(self): - """Test connection to the Controller API""" - self.ensure_one() - - if self.api_type != 'controller': - return False - - try: - # Tester l'authentification - if not self._authenticate(): - return False - - # Tester la récupération du statut du système - success, response, status_code = self._get('status') - if not success: - self._logout() - return False - - # Déconnexion propre - self._logout() - - return True - - except Exception as e: - _logger.error(f"Erreur lors du test de connexion: {str(e)}") - return False - - @api.model - def get_device_data(self, site, mac_address=None): - """Récupère les données d'un appareil spécifique ou de tous les appareils du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les appareils. - - Args: - site: L'enregistrement du site UniFi - mac_address: Adresse MAC de l'appareil spécifique à récupérer (optionnel) - - Returns: - dict ou list: Données de l'appareil ou liste des données de tous les appareils - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des appareils - if mac_address: - # URL pour un appareil spécifique - device_url = f"{base_url}/api/s/{site.site_id}/stat/device/{mac_address}" - else: - # URL pour tous les appareils - device_url = f"{base_url}/api/s/{site.site_id}/stat/device" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': device_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des appareils - response = session.get( - device_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des appareils: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée d'appareil trouvée dans la réponse") - return False - - # Retourner les données des appareils - if mac_address and data['data']: - # Retourner les données de l'appareil spécifique - return data['data'][0] if data['data'] else False - else: - # Retourner la liste des données de tous les appareils - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des appareils: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_system_info_data(self, site): - """Récupère les données d'information système du site - - Cette méthode utilise l'API Controller pour obtenir les informations système. - - Args: - site: L'enregistrement du site UniFi - - Returns: - dict: Données d'information système - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données système - system_url = f"{base_url}/api/s/{site.site_id}/stat/sysinfo" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': system_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données système - response = session.get( - system_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données système: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data or not data['data']: - _logger.error("Aucune donnée système trouvée dans la réponse") - return False - - # Retourner les données système (premier élément de la liste) - return data['data'][0] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données système: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_vlan_data(self, site): - """Récupère les données des VLANs du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les VLANs. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les VLANs - """ - # Initialiser api_log à None pour éviter les erreurs de variable potentiellement indépendante - api_log = None - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des VLANs - vlan_url = f"{base_url}/api/s/{site.site_id}/rest/vlan" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': vlan_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des VLANs - response = session.get( - vlan_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des VLANs: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée de VLAN trouvée dans la réponse") - return False - - # Retourner la liste des données de tous les VLANs - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des VLANs: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_network_data(self, site): - """Récupère les données des réseaux du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les réseaux. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les réseaux - """ - # Initialiser api_log à None pour éviter les erreurs de variable potentiellement indépendante - api_log = None - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des réseaux - network_url = f"{base_url}/api/s/{site.site_id}/rest/networkconf" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': network_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des réseaux - response = session.get( - network_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des réseaux: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée de réseau trouvée dans la réponse") - return False - - # Retourner la liste des données de tous les réseaux - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des réseaux: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - def _sync_controller(self): - """Synchronize data with the Controller API""" - self.ensure_one() - - if self.api_type != 'controller': - return False - - # Create a sync job to track progress - sync_job = self.env['unifi.sync.job'].create({ - 'site_id': self.id, - 'api_type': 'controller', - 'status': 'running', - 'start_time': fields.Datetime.now() - }) - - try: - # Implement synchronization logic here - # This would typically involve: - # 1. Authenticating with the controller - # 2. Fetching device data - # 3. Fetching network data - # 4. Fetching client data - # 5. Updating local records - - # For now, just a placeholder - _logger.info("Starting synchronization with Controller API for site %s", self.name) - - # Update sync job with success status - sync_job.write({ - 'status': 'completed', - 'end_time': fields.Datetime.now(), - 'message': 'Synchronization completed successfully' - }) - - # Update site's last sync timestamp - self.write({ - 'last_sync': fields.Datetime.now() - }) - - return True - - except Exception as e: - # Log the error and update sync job - _logger.error("Error during synchronization with Controller API: %s", str(e)) - sync_job.write({ - 'status': 'failed', - 'end_time': fields.Datetime.now(), - 'message': f'Synchronization failed: {str(e)}' - }) - return False - - @api.model - def get_user_data(self, site): - """Récupère les données des utilisateurs du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les utilisateurs. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les utilisateurs - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des utilisateurs - # Pour les utilisateurs réguliers - user_url = f"{base_url}/api/s/{site.site_id}/rest/user" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': user_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des utilisateurs - response = session.get( - user_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des utilisateurs: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée d'utilisateur trouvée dans la réponse de l'API Controller") - return [] - - # Récupérer également les utilisateurs invités - guest_url = f"{base_url}/api/s/{site.site_id}/rest/guest" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': guest_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des utilisateurs invités - guest_response = session.get( - guest_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': guest_response.status_code, - 'response_headers': json.dumps(dict(guest_response.headers)), - 'response_body': guest_response.text - }) - - # Si la requête pour les invités a réussi, ajouter les données à la liste - if guest_response.status_code == 200: - guest_data = guest_response.json() - if 'data' in guest_data: - # Marquer ces utilisateurs comme invités - for guest in guest_data['data']: - guest['is_guest'] = True - # Ajouter les invités à la liste des utilisateurs - data['data'].extend(guest_data['data']) - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - _logger.error("Erreur lors de la communication avec l'API Controller: %s", str(e)) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_firewall_data(self, site): - """Récupère les données des règles de pare-feu du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les règles de pare-feu. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de toutes les règles de pare-feu - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des règles de pare-feu - firewall_url = f"{base_url}/api/s/{site.site_id}/rest/firewallrule" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': firewall_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des règles de pare-feu - response = session.get( - firewall_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des règles de pare-feu: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée de règle de pare-feu trouvée dans la réponse de l'API Controller") - return [] - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - _logger.error("Erreur lors de la communication avec l'API Controller: %s", str(e)) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_port_forward_data(self, site): - """Récupère les données des redirections de port du site - - Cette méthode utilise l'API Controller pour obtenir les informations sur les redirections de port. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de toutes les redirections de port - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password, - 'remember': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Controller: %s", response.text) - return False - - # Construire l'URL pour récupérer les données des redirections de port - port_forward_url = f"{base_url}/api/s/{site.site_id}/rest/portforward" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'controller', - 'endpoint': port_forward_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des redirections de port - response = session.get( - port_forward_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des redirections de port: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée de redirection de port trouvée dans la réponse de l'API Controller") - return [] - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - _logger.error("Erreur lors de la communication avec l'API Controller: %s", str(e)) - return False - finally: - # Fermer la session - session.close() diff --git a/unifi_integration/models/unifi_site_manager.py b/unifi_integration/models/unifi_site_manager.py deleted file mode 100644 index 87d32ce..0000000 --- a/unifi_integration/models/unifi_site_manager.py +++ /dev/null @@ -1,1343 +0,0 @@ -# -*- coding: utf-8 -*- - -# These imports will work in an Odoo environment, even if your IDE marks them as not found -# pylint: disable=import-error -from odoo import models, fields, api, _ -from odoo.exceptions import UserError, ValidationError -# pylint: enable=import-error - -import json -import logging -import requests -import urllib3 -from datetime import datetime, timedelta -from requests.exceptions import RequestException - -_logger = logging.getLogger(__name__) - -class UnifiSiteManager(models.Model): - """Extension du modèle UnifiSite pour l'API Site Manager (Cloud) - - Cette classe ajoute les champs et méthodes spécifiques à l'API Site Manager cloud. - Elle est utilisée lorsque api_type = 'site_manager'. - """ - _name = 'unifi.site.manager' - _description = 'UniFi Site Manager API' - - # Champs pour établir la relation avec unifi.site - site_id = fields.Many2one( - comodel_name='unifi.site', - string='Site', - required=True, - ondelete='cascade', - help='Site associé à cette configuration API Site Manager' - ) - - # Champs nécessaires pour le fonctionnement du modèle - name = fields.Char(related='site_id.name', string='Nom', readonly=True) - api_type = fields.Selection(related='site_id.api_type', string='Type API', readonly=True) - verify_ssl = fields.Boolean(string='Verify SSL', default=True) - last_sync = fields.Datetime(string='Dernière synchronisation') - - def test_connection(self): - """Teste la connexion à l'API Site Manager - - Cette méthode tente d'établir une connexion avec l'API Site Manager - pour vérifier que les paramètres de connexion sont corrects. - - Returns: - bool: True si la connexion est établie avec succès, False sinon - """ - try: - # Utiliser la méthode existante pour tester la connexion - return self._test_site_manager_connection() - except Exception as e: - _logger.error('Erreur lors du test de connexion à l\'API Site Manager: %s', str(e)) - return False - - def _make_request(self, method, endpoint, data=None, params=None, headers=None): - """Effectue une requête HTTP vers l'API Site Manager - - Cette méthode gère les requêtes HTTP vers l'API Site Manager, en incluant - les en-têtes d'authentification et en gérant les erreurs. - - Args: - method: Méthode HTTP (GET, POST, PUT, DELETE) - endpoint: Point de terminaison API (chemin relatif) - data: Données à envoyer dans le corps de la requête (optionnel) - params: Paramètres de requête (optionnel) - headers: En-têtes HTTP supplémentaires (optionnel) - - Returns: - Response: Objet réponse HTTP - - Raises: - RequestException: Si une erreur se produit lors de la requête - """ - self.ensure_one() - - # Créer un log API pour cette requête - api_log = self.env['unifi.api.log'].create({ - 'site_id': self.site_id.id, - 'api_type': 'site_manager', - 'endpoint': endpoint, - 'method': method, - 'request_body': json.dumps(data) if data else '', - 'request_params': json.dumps(params) if params else '', - 'request_headers': json.dumps(headers) if headers else '', - 'start_time': fields.Datetime.now(), - }) - - try: - # Désactiver les avertissements SSL si verify_ssl est False - if not self.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Construire l'URL de base - base_url = "https://sitemanager.unifi.ui.com" - url = f"{base_url}{endpoint}" - - # Préparer les en-têtes avec la clé API - default_headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'Odoo UniFi Integration', - 'Authorization': f'Bearer {self.api_key}' - } - - # Fusionner les en-têtes par défaut avec les en-têtes personnalisées - if headers: - default_headers.update(headers) - - # Effectuer la requête HTTP - response = requests.request( - method=method, - url=url, - json=data if data else None, - params=params, - headers=default_headers, - verify=self.verify_ssl, - timeout=30 - ) - - # Mettre à jour le log API avec la réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_body': response.text if response.text else '', - 'response_headers': json.dumps(dict(response.headers)) if response.headers else '', - 'duration': (fields.Datetime.now() - api_log.start_time).total_seconds() * 1000, - }) - - # Gérer les erreurs HTTP - response.raise_for_status() - - return response - - except RequestException as e: - # Mettre à jour le log API avec l'erreur - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': e.response.status_code if hasattr(e, 'response') and e.response else 0, - 'error_message': str(e), - 'duration': (fields.Datetime.now() - api_log.start_time).total_seconds() * 1000, - }) - - _logger.error('Erreur lors de la requête HTTP vers l\'API Site Manager: %s', str(e)) - raise - - @api.model - def _check_required_fields(self, site): - """Vérifie que les champs requis pour l'API Site Manager sont renseignés - - Cette méthode est appelée par le modèle principal lors de la validation des contraintes. - - Args: - site: L'enregistrement du site à vérifier - - Raises: - ValidationError: Si des champs requis ne sont pas renseignés - """ - if not site.api_key: - raise ValidationError(_("Le champ 'API Key' est requis pour l'API Site Manager.")) - - if site.mfa_enabled and not site.mfa_token: - raise ValidationError(_("Le champ 'MFA Token' est requis lorsque l'authentification à deux facteurs est activée.")) - - @api.model - def _create_or_update_api_log(self, api_log=None, **kwargs): - """Crée ou met à jour un enregistrement de log API - - Cette méthode d'aide simplifie la gestion des logs API en créant un nouvel enregistrement - si api_log est None, ou en mettant à jour l'enregistrement existant sinon. - - Args: - api_log: L'enregistrement de log API existant, ou None pour en créer un nouveau - **kwargs: Les valeurs à écrire dans l'enregistrement - - Returns: - L'enregistrement de log API créé ou mis à jour - """ - if not api_log: - # Valeurs par défaut pour la création - default_values = { - 'site_id': self.id, - 'api_type': 'site_manager', - 'method': 'GET', - 'endpoint': 'error_handler', - 'start_time': fields.Datetime.now() - } - # Fusionner les valeurs par défaut avec les valeurs fournies - values = {**default_values, **kwargs} - return self.env['unifi.api.log'].create(values) - else: - # Mettre à jour l'enregistrement existant - api_log.write(kwargs) - return api_log - - def _clear_irrelevant_fields(self, site): - """Nettoie les champs qui ne sont pas pertinents pour l'API Site Manager - - Cette méthode est appelée par le modèle principal lors du changement de type d'API. - Elle supprime les enregistrements unifi.site.controller associés au site. - - Args: - site: L'enregistrement du site à nettoyer - """ - # Rechercher et supprimer les enregistrements unifi.site.controller associés au site - controllers = self.env['unifi.site.controller'].search([('site_id', '=', site.id)]) - if controllers: - _logger.info("Suppression des enregistrements unifi.site.controller associés au site %s", site.name) - controllers.unlink() - - # Site Manager API fields - Only used when api_type = 'site_manager' - api_key = fields.Char( - string='API Key', - - help='API key for Site Manager authentication' - ) - - mfa_enabled = fields.Boolean( - string='MFA Enabled', - default=False, - - help='Whether two-factor authentication is enabled for this site' - ) - - mfa_token = fields.Char( - string='MFA Token', - - help='Two-factor authentication token if enabled' - ) - - def _test_site_manager_connection(self): - """Test connection to the Site Manager API""" - self.ensure_one() - - if self.api_type != 'site_manager': - return False - - # Disable SSL warnings if verify_ssl is False - if not self.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Create a new session for this connection test - session = requests.Session() - - # Build the API URL - base_url = "https://sitemanager.unifi.ui.com/api" - sites_url = f"{base_url}/sites" - - # Prepare headers with API key - headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'Odoo UniFi Integration', - 'Authorization': f'Bearer {self.api_key}' - } - - # Add MFA token if enabled - if self.mfa_enabled and self.mfa_token: - headers['X-MFA-Token'] = self.mfa_token - - # Initialiser le log API - api_log = self.env['unifi.api.log'].create({ - 'site_id': self.site_id.id, - 'api_type': 'site_manager', - 'endpoint': sites_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - try: - - # Make the request - response = session.get( - sites_url, - headers=headers, - verify=self.verify_ssl, - timeout=10 - ) - - # Update the API log with response data - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Return True if the request was successful - return response.status_code == 200 - - except Exception as e: - # Utiliser la méthode d'aide pour créer ou mettre à jour le log API - if self.env.get('unifi.api.log'): - self._create_or_update_api_log( - api_log=api_log, - end_time=fields.Datetime.now(), - error_message=str(e) - ) - _logger.error("Error testing connection to Site Manager API: %s", str(e)) - return False - finally: - # Close the session - session.close() - - @api.model - def get_system_info_data(self, site): - """Récupère les données d'information système du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations système. - - Args: - site: L'enregistrement du site UniFi - - Returns: - dict: Données d'information système - """ - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://sitemanager.unifi.ui.com/api" - - # Préparer les en-têtes avec la clé API - headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'Odoo UniFi Integration', - 'Authorization': f'Bearer {site.api_key}' - } - - # Ajouter le token MFA si activé - if site.mfa_enabled and site.mfa_token: - headers['X-MFA-Token'] = site.mfa_token - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Construire l'URL pour récupérer les données système - system_url = f"{base_url}/sites/{site.site_id}/system" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': system_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête pour récupérer les données système - response = session.get( - system_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données système: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if not data: - _logger.error("Aucune donnée système trouvée dans la réponse") - return False - - # Retourner les données système - return data - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données système: {str(e)}" - if api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_device_data(self, site, mac_address=None): - """Récupère les données d'un appareil spécifique ou de tous les appareils du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les appareils. - - Args: - site: L'enregistrement du site UniFi - mac_address: Adresse MAC de l'appareil spécifique à récupérer (optionnel) - - Returns: - dict ou list: Données de l'appareil ou liste des données de tous les appareils - """ - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://api.cloud.ui.com" - - # URL pour l'authentification - auth_url = f"{base_url}/auth/login" - - # Données d'authentification - auth_data = { - 'username': site.username, - 'password': site.password, - 'token': site.api_key or '', - 'rememberMe': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': auth_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(auth_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête d'authentification - response = session.post( - auth_url, - json=auth_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si l'authentification a réussi - if response.status_code != 200: - _logger.error("Erreur d'authentification à l'API Site Manager: %s", response.text) - return False - - # Extraire le token d'authentification - auth_response = response.json() - if 'access_token' not in auth_response: - _logger.error("Token d'authentification non trouvé dans la réponse") - return False - - # Mettre à jour les en-têtes avec le token d'authentification - headers['Authorization'] = f"Bearer {auth_response['access_token']}" - - # Construire l'URL pour récupérer les données des appareils - if mac_address: - # URL pour un appareil spécifique - device_url = f"{base_url}/api/site/{site.site_id}/device/{mac_address}" - else: - # URL pour tous les appareils - device_url = f"{base_url}/api/site/{site.site_id}/device" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': device_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des appareils - response = session.get( - device_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des appareils: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée d'appareil trouvée dans la réponse") - return False - - # Retourner les données des appareils - if mac_address and data['data']: - # Retourner les données de l'appareil spécifique - return data['data'] - else: - # Retourner la liste des données de tous les appareils - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des appareils: {str(e)}" - if api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_vlan_data(self, site): - """Récupère les données des VLANs du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les VLANs. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les VLANs - """ - # Initialiser api_log à None pour éviter les erreurs de variable potentiellement indépendante - api_log = None - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://api.cloud.ui.com" - - # URL pour l'authentification - auth_url = f"{base_url}/auth/login" - - # Données d'authentification - auth_data = { - 'username': site.username, - 'password': site.password, - 'token': site.api_key or '', - 'rememberMe': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': auth_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(auth_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête d'authentification - response = session.post( - auth_url, - json=auth_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si l'authentification a réussi - if response.status_code != 200: - _logger.error("Erreur d'authentification à l'API Site Manager: %s", response.text) - return False - - # Extraire le token d'authentification - auth_response = response.json() - if 'access_token' not in auth_response: - _logger.error("Token d'authentification non trouvé dans la réponse") - return False - - # Mettre à jour les en-têtes avec le token d'authentification - headers['Authorization'] = f"Bearer {auth_response['access_token']}" - - # Construire l'URL pour récupérer les données des VLANs - vlan_url = f"{base_url}/api/site/{site.site_id}/vlan" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': vlan_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des VLANs - response = session.get( - vlan_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des VLANs: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée de VLAN trouvée dans la réponse") - return False - - # Retourner la liste des données de tous les VLANs - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des VLANs: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_network_data(self, site): - """Récupère les données des réseaux du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les réseaux. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les réseaux - """ - # Initialiser api_log à None pour éviter les erreurs de variable potentiellement indépendante - api_log = None - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://api.cloud.ui.com" - - # URL pour l'authentification - auth_url = f"{base_url}/auth/login" - - # Données d'authentification - auth_data = { - 'username': site.username, - 'password': site.password, - 'token': site.api_key or '', - 'rememberMe': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': auth_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(auth_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête d'authentification - response = session.post( - auth_url, - json=auth_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si l'authentification a réussi - if response.status_code != 200: - _logger.error("Erreur d'authentification à l'API Site Manager: %s", response.text) - return False - - # Extraire le token d'authentification - auth_response = response.json() - if 'access_token' not in auth_response: - _logger.error("Token d'authentification non trouvé dans la réponse") - return False - - # Mettre à jour les en-têtes avec le token d'authentification - headers['Authorization'] = f"Bearer {auth_response['access_token']}" - - # Construire l'URL pour récupérer les données des réseaux - network_url = f"{base_url}/api/site/{site.site_id}/network" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': network_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des réseaux - response = session.get( - network_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des réseaux: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.error("Aucune donnée de réseau trouvée dans la réponse") - return False - - # Retourner la liste des données de tous les réseaux - return data['data'] - - except (RequestException, json.JSONDecodeError) as e: - # Journaliser l'erreur - error_msg = f"Erreur lors de la récupération des données des réseaux: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - def _sync_site_manager(self): - """Synchronize data with the Site Manager API""" - self.ensure_one() - - if self.api_type != 'site_manager': - return False - - # Create a sync job to track progress - sync_job = self.env['unifi.sync.job'].create({ - 'site_id': self.id, - 'api_type': 'site_manager', - 'status': 'running', - 'start_time': fields.Datetime.now() - }) - - try: - # Implement synchronization logic here - # This would typically involve: - # 1. Authenticating with the Site Manager API - # 2. Fetching site data - # 3. Fetching device data - # 4. Fetching network data - # 5. Updating local records - - # For now, just a placeholder - _logger.info("Starting synchronization with Site Manager API for site %s", self.name) - - # Update sync job with success status - sync_job.write({ - 'status': 'completed', - 'end_time': fields.Datetime.now(), - 'message': 'Synchronization completed successfully' - }) - - # Update site's last sync timestamp - self.write({ - 'last_sync': fields.Datetime.now() - }) - - return True - - except Exception as e: - # Log the error and update sync job - _logger.error("Error during synchronization with Site Manager API: %s", str(e)) - sync_job.write({ - 'status': 'failed', - 'end_time': fields.Datetime.now(), - 'message': f'Synchronization failed: {str(e)}' - }) - return False - - @api.model - def get_user_data(self, site): - """Récupère les données des utilisateurs du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les utilisateurs. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de tous les utilisateurs - """ - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://api.cloud.ui.com" - - # URL pour l'authentification - auth_url = f"{base_url}/auth/login" - - # Données d'authentification - auth_data = { - 'username': site.username, - 'password': site.password, - 'token': site.api_key or '', - 'rememberMe': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': auth_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(auth_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête d'authentification - response = session.post( - auth_url, - json=auth_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si l'authentification a réussi - if response.status_code != 200: - _logger.error("Erreur d'authentification à l'API Site Manager: %s", response.text) - return False - - # Extraire le jeton d'authentification - auth_response = response.json() - if 'token' not in auth_response: - _logger.error("Jeton d'authentification manquant dans la réponse de l'API Site Manager") - return False - - # Mettre à jour les en-têtes avec le jeton d'authentification - headers['Authorization'] = f"Bearer {auth_response['token']}" - - # Construire l'URL pour récupérer les données des utilisateurs - # Pour les utilisateurs réguliers - user_url = f"{base_url}/proxy/network/api/s/{site.site_id}/rest/user" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': user_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des utilisateurs - response = session.get( - user_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des utilisateurs: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée d'utilisateur trouvée dans la réponse de l'API Site Manager") - return [] - - # Récupérer également les utilisateurs invités - guest_url = f"{base_url}/proxy/network/api/s/{site.site_id}/rest/guest" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': guest_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des utilisateurs invités - guest_response = session.get( - guest_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': guest_response.status_code, - 'response_headers': json.dumps(dict(guest_response.headers)), - 'response_body': guest_response.text - }) - - # Si la requête pour les invités a réussi, ajouter les données à la liste - if guest_response.status_code == 200: - guest_data = guest_response.json() - if 'data' in guest_data: - # Marquer ces utilisateurs comme invités - for guest in guest_data['data']: - guest['is_guest'] = True - # Ajouter les invités à la liste des utilisateurs - data['data'].extend(guest_data['data']) - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - error_msg = f"Erreur lors de la récupération des données des utilisateurs: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_firewall_data(self, site): - """Récupère les données des règles de pare-feu du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les règles de pare-feu. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de toutes les règles de pare-feu - """ - # Créer une session pour les requêtes - session = requests.Session() - - # URL de base pour l'API Site Manager - base_url = "https://api.cloud.ui.com" - - # URL pour l'authentification - auth_url = f"{base_url}/auth/login" - - # Données d'authentification - auth_data = { - 'username': site.username, - 'password': site.password, - 'token': site.api_key or '', - 'rememberMe': True - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': auth_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(auth_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête d'authentification - response = session.post( - auth_url, - json=auth_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si l'authentification a réussi - if response.status_code != 200: - _logger.error("Erreur d'authentification à l'API Site Manager: %s", response.text) - return False - - # Extraire le jeton d'authentification - auth_response = response.json() - if 'token' not in auth_response: - _logger.error("Jeton d'authentification manquant dans la réponse de l'API Site Manager") - return False - - # Mettre à jour les en-têtes avec le jeton d'authentification - headers['Authorization'] = f"Bearer {auth_response['token']}" - - # Construire l'URL pour récupérer les données des règles de pare-feu - firewall_url = f"{base_url}/proxy/network/api/s/{site.site_id}/rest/firewallrule" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': firewall_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des règles de pare-feu - response = session.get( - firewall_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des règles de pare-feu: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée de règle de pare-feu trouvée dans la réponse de l'API Site Manager") - return [] - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - error_msg = f"Erreur lors de la récupération des données des règles de pare-feu: {str(e)}" - if 'api_log' in locals() and api_log: - api_log.write({ - 'end_time': fields.Datetime.now(), - 'error_message': error_msg - }) - _logger.error(error_msg) - return False - finally: - # Fermer la session - session.close() - - @api.model - def get_port_forward_data(self, site): - """Récupère les données des redirections de port du site - - Cette méthode utilise l'API Site Manager pour obtenir les informations sur les redirections de port. - - Args: - site: L'enregistrement du site UniFi - - Returns: - list: Liste des données de toutes les redirections de port - """ - # Créer une session pour les requêtes - session = requests.Session() - - # Construire l'URL de base - base_url = f"https://{site.host}:{site.port}" - - # URL pour la connexion - login_url = f"{base_url}/api/auth/login" - - # Données de connexion - login_data = { - 'username': site.username, - 'password': site.password - } - - # En-têtes de la requête - headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - - # Désactiver les avertissements SSL si verify_ssl est False - if not site.verify_ssl: - urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': login_url, - 'method': 'POST', - 'request_headers': json.dumps(headers), - 'request_body': json.dumps(login_data), - 'start_time': fields.Datetime.now() - }) - - try: - # Effectuer la requête de connexion - response = session.post( - login_url, - json=login_data, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la connexion a réussi - if response.status_code != 200: - _logger.error("Erreur de connexion à l'API Site Manager: %s", response.text) - return False - - # Analyser la réponse JSON - auth_data = response.json() - - # Vérifier si la réponse contient un jeton d'authentification - if 'data' not in auth_data or 'token' not in auth_data['data']: - _logger.error("Aucun jeton d'authentification trouvé dans la réponse de l'API Site Manager") - return False - - # Extraire le jeton d'authentification - token = auth_data['data']['token'] - - # Mettre à jour les en-têtes avec le jeton d'authentification - headers['Authorization'] = f"Bearer {token}" - - # Construire l'URL pour récupérer les données des redirections de port - port_forward_url = f"{base_url}/proxy/network/api/s/{site.site_id}/rest/portforward" - - # Journaliser l'appel API - api_log = self.env['unifi.api.log'].create({ - 'site_id': site.id, - 'api_type': 'site_manager', - 'endpoint': port_forward_url, - 'method': 'GET', - 'request_headers': json.dumps(headers), - 'start_time': fields.Datetime.now() - }) - - # Effectuer la requête pour récupérer les données des redirections de port - response = session.get( - port_forward_url, - headers=headers, - verify=site.verify_ssl, - timeout=10 - ) - - # Mettre à jour le journal API avec les données de réponse - api_log.write({ - 'end_time': fields.Datetime.now(), - 'status_code': response.status_code, - 'response_headers': json.dumps(dict(response.headers)), - 'response_body': response.text - }) - - # Vérifier si la requête a réussi - if response.status_code != 200: - _logger.error("Erreur lors de la récupération des données des redirections de port: %s", response.text) - return False - - # Analyser la réponse JSON - data = response.json() - - # Vérifier si la réponse contient des données - if 'data' not in data: - _logger.warning("Aucune donnée de redirection de port trouvée dans la réponse de l'API Site Manager") - return [] - - return data['data'] - - except RequestException as e: - # Gérer les erreurs de requête - _logger.error("Erreur lors de la communication avec l'API Site Manager: %s", str(e)) - return False - finally: - # Fermer la session - session.close() diff --git a/unifi_integration/models/unifi_sync_job.py b/unifi_integration/models/unifi_sync_job.py index 02a6fe1..9402ccd 100644 --- a/unifi_integration/models/unifi_sync_job.py +++ b/unifi_integration/models/unifi_sync_job.py @@ -62,6 +62,7 @@ class UnifiSyncJob(models.Model): ('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), + ('partial', 'Partially Completed'), ('failed', 'Failed'), ('cancelled', 'Cancelled') ], @@ -237,7 +238,7 @@ class UnifiSyncJob(models.Model): return { 'name': _('API Logs'), - 'view_mode': 'tree,form', + 'view_mode': 'list,form', 'res_model': 'unifi.api.log', 'domain': [('sync_job_id', '=', self.id)], 'type': 'ir.actions.act_window', diff --git a/unifi_integration/models/unifi_system_info.py b/unifi_integration/models/unifi_system_info.py index a281d73..db46a2c 100644 --- a/unifi_integration/models/unifi_system_info.py +++ b/unifi_integration/models/unifi_system_info.py @@ -4,10 +4,11 @@ import json import logging from odoo import models, fields, api +from .unifi_common import UnifiCommonMixin _logger = logging.getLogger(__name__) -class UnifiSystemInfo(models.Model): +class UnifiSystemInfo(models.Model, UnifiCommonMixin): """System information of the UniFi device""" _name = 'unifi.system.info' _description = 'UniFi System Information' @@ -51,6 +52,17 @@ class UnifiSystemInfo(models.Model): # Raw data for debugging and future extensions raw_data = fields.Text(string='Raw Data') + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes du système au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + @api.depends('uptime') def _compute_uptime_human(self): for record in self: diff --git a/unifi_integration/models/unifi_user.py b/unifi_integration/models/unifi_user.py index 04984ce..1bea6ef 100644 --- a/unifi_integration/models/unifi_user.py +++ b/unifi_integration/models/unifi_user.py @@ -3,6 +3,7 @@ # These imports will work in an Odoo environment, even if your IDE marks them as not found # pylint: disable=import-error from odoo import models, fields, api, _ +from .unifi_common import UnifiCommonMixin from odoo.exceptions import UserError, ValidationError # pylint: enable=import-error @@ -12,7 +13,7 @@ from datetime import datetime _logger = logging.getLogger(__name__) -class UnifiUser(models.Model): +class UnifiUser(models.Model, UnifiCommonMixin): """Modèle pour gérer les utilisateurs UniFi Ce modèle représente les utilisateurs configurés dans les sites UniFi, @@ -113,6 +114,17 @@ class UnifiUser(models.Model): help='Données brutes de l\'utilisateur au format JSON' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de l\'utilisateur au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + # Relations site_id = fields.Many2one( comodel_name='unifi.site', diff --git a/unifi_integration/models/unifi_vlan.py b/unifi_integration/models/unifi_vlan.py index 64c7a3d..87e3e01 100644 --- a/unifi_integration/models/unifi_vlan.py +++ b/unifi_integration/models/unifi_vlan.py @@ -3,11 +3,12 @@ import json import logging from odoo import models, fields, api, _ +from .unifi_common import UnifiCommonMixin from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) -class UnifiVLAN(models.Model): +class UnifiVLAN(models.Model, UnifiCommonMixin): """Modèle pour gérer les VLANs UniFi Ce modèle représente les VLANs configurés dans les sites UniFi. @@ -99,6 +100,17 @@ class UnifiVLAN(models.Model): help='Données brutes JSON du VLAN depuis l\'API' ) + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes du VLAN au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + _sql_constraints = [ ('vlan_id_site_uniq', 'unique(vlan_id, site_id)', 'L\'ID VLAN doit être unique par site!') ] diff --git a/unifi_integration/models/unifi_vpn.py b/unifi_integration/models/unifi_vpn.py new file mode 100644 index 0000000..e0f4e8c --- /dev/null +++ b/unifi_integration/models/unifi_vpn.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, _ +from .unifi_common import UnifiCommonMixin +from odoo.exceptions import ValidationError +import logging +import json +from pprint import pformat + +_logger = logging.getLogger(__name__) + +class UnifiVpn(models.Model, UnifiCommonMixin): + """Configuration VPN pour le système UniFi + + Ce modèle stocke les configurations VPN pour les contrôleurs UniFi. + Il gère les tunnels VPN IPsec et autres types de VPN. + + Les configurations VPN sont liées à un site spécifique et sont automatiquement supprimées + lorsque le site est supprimé (cascade). + """ + _name = 'unifi.vpn' + _description = 'UniFi VPN Configuration' + _rec_name = 'name' + _order = 'name' + + site_id = fields.Many2one( + comodel_name='unifi.site', + string='Site', + required=True, + ondelete='cascade', + help='Site this VPN configuration belongs to' + ) + + name = fields.Char( + string='Name', + required=True, + help='Name of this VPN configuration' + ) + + vpn_type = fields.Selection( + selection=[ + ('ipsec-vpn', 'IPsec VPN'), + ('l2tp-vpn', 'L2TP VPN'), + ('openvpn', 'OpenVPN'), + ('wireguard', 'WireGuard'), + ('other', 'Other') + ], + string='VPN Type', + default='ipsec-vpn', + help='Type of VPN connection' + ) + + enabled = fields.Boolean( + string='Enabled', + default=True, + help='Whether this VPN configuration is active' + ) + + peer_ip = fields.Char( + string='Peer IP', + help='IP address of the remote VPN peer' + ) + + local_ip = fields.Char( + string='Local IP', + help='Local IP address for this VPN connection' + ) + + remote_subnets = fields.Char( + string='Remote Subnets', + help='Comma-separated list of remote subnets accessible through this VPN' + ) + + interface = fields.Char( + string='Interface', + help='Network interface used for this VPN' + ) + + purpose = fields.Char( + string='Purpose', + help='Purpose of this VPN connection' + ) + + unifi_id = fields.Char( + string='UniFi ID', + help='ID of this VPN configuration in the UniFi system' + ) + + last_sync = fields.Datetime( + string='Last Synchronization', + help='Last time this VPN configuration was synchronized with the UniFi system' + ) + + raw_data = fields.Text( + string='Raw Data', + help='Raw VPN configuration data in JSON format' + ) + + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de la configuration VPN au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + + formatted_data = fields.Html( + string='Formatted Configuration', + compute='_compute_formatted_data', + help='Formatted view of the VPN configuration data' + ) + + @api.depends('raw_data') + def _compute_formatted_data(self): + """Transforme les données brutes JSON en format HTML lisible""" + for record in self: + if not record.raw_data: + record.formatted_data = "

Aucune donnée disponible

" + continue + + try: + # Analyser les données JSON + data = json.loads(record.raw_data) + + # Créer une représentation HTML formatée + html = "
" + + # Informations générales + html += "

Informations générales

" + html += "" + html += f"" + html += f"" + html += f"" + html += f"" + html += f"" + html += "
Nom:{data.get('name', 'N/A')}
Type:{data.get('vpn_type', 'N/A')}
Activé:{'Oui' if data.get('enabled') else 'Non'}
Interface:{data.get('ifname', 'N/A')}
But:{data.get('purpose', 'N/A')}
" + + # Configuration IPsec + if data.get('vpn_type') == 'ipsec-vpn': + html += "

Configuration IPsec

" + html += "" + html += f"" + html += f"" + html += f"" + html += f"" + html += f"" + html += f"" + html += f"" + html += "
IP Pair:{data.get('ipsec_peer_ip', 'N/A')}
IP Locale:{data.get('ipsec_local_ip', 'N/A')}
Interface:{data.get('ipsec_interface', 'N/A')}
Échange de clés:{data.get('ipsec_key_exchange', 'N/A')}
Profil:{data.get('ipsec_profile', 'N/A')}
PFS:{'Oui' if data.get('ipsec_pfs') else 'Non'}
Routage dynamique:{'Oui' if data.get('ipsec_dynamic_routing') else 'Non'}
" + + # Paramètres IKE + html += "

Paramètres IKE

" + html += "" + html += f"" + html += f"" + html += f"" + html += f"" + html += "
Groupe DH:{data.get('ipsec_ike_dh_group', 'N/A')}
Chiffrement:{data.get('ipsec_ike_encryption', 'N/A')}
Hash:{data.get('ipsec_ike_hash', 'N/A')}
Durée de vie (s):{data.get('ipsec_ike_lifetime', 'N/A')}
" + + # Paramètres ESP + html += "

Paramètres ESP

" + html += "" + html += f"" + html += f"" + html += f"" + html += f"" + html += "
Groupe DH:{data.get('ipsec_esp_dh_group', 'N/A')}
Chiffrement:{data.get('ipsec_esp_encryption', 'N/A')}
Hash:{data.get('ipsec_esp_hash', 'N/A')}
Durée de vie (s):{data.get('ipsec_esp_lifetime', 'N/A')}
" + + # Sous-réseaux distants + remote_subnets = data.get('remote_vpn_subnets', []) + if remote_subnets: + html += "

Sous-réseaux distants

" + html += "
    " + for subnet in remote_subnets: + html += f"
  • {subnet}
  • " + html += "
" + + html += "
" + record.formatted_data = html + except Exception as e: + _logger.error("Erreur lors du formatage des données VPN: %s", str(e)) + record.formatted_data = f"

Erreur lors du formatage des données: {str(e)}

" + + @api.model + def create_or_update_from_data(self, site, vpn_data): + """Create or update VPN configuration from UniFi API data + + Args: + site (unifi.site): Site record + vpn_data (dict): VPN configuration data from UniFi API + + Returns: + unifi.vpn: Created or updated VPN configuration record + """ + _logger.info("Creating or updating VPN configuration from data") + + # Extract required fields + name = vpn_data.get('name') + vpn_type = vpn_data.get('vpn_type') + unifi_id = vpn_data.get('_id') + + if not name or not vpn_type: + _logger.warning("Missing required fields in VPN data") + return False + + # Search for existing VPN configuration + domain = [ + ('site_id', '=', site.id), + '|', + ('name', '=', name), + ('unifi_id', '=', unifi_id) + ] + + existing = self.search(domain, limit=1) + + # Prepare values for create/write + vals = { + 'name': name, + 'vpn_type': vpn_type, + 'unifi_id': unifi_id, + 'enabled': vpn_data.get('enabled', True), + 'peer_ip': vpn_data.get('ipsec_peer_ip'), + 'local_ip': vpn_data.get('ipsec_local_ip'), + 'interface': vpn_data.get('ifname'), + 'purpose': vpn_data.get('purpose'), + 'remote_subnets': ', '.join(vpn_data.get('remote_vpn_subnets', [])), + 'last_sync': fields.Datetime.now(), + 'raw_data': json.dumps(vpn_data, indent=2) if vpn_data else False + } + + if existing: + _logger.info("Updating existing VPN configuration: %s", existing.name) + existing.write(vals) + return existing + else: + _logger.info("Creating new VPN configuration: %s", name) + vals['site_id'] = site.id + return self.create(vals) diff --git a/unifi_integration/models/unifi_wifi.py b/unifi_integration/models/unifi_wifi.py new file mode 100644 index 0000000..7f8052d --- /dev/null +++ b/unifi_integration/models/unifi_wifi.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- + +# These imports will work in an Odoo environment, even if your IDE marks them as not found +# pylint: disable=import-error +from odoo import models, fields, api, _ +from .unifi_common import UnifiCommonMixin +from odoo.exceptions import UserError, ValidationError +# pylint: enable=import-error + +import json +import logging +from datetime import datetime + +_logger = logging.getLogger(__name__) + +class UnifiWifi(models.Model, UnifiCommonMixin): + """Model for UniFi WiFi networks + + This model represents WiFi networks configured in UniFi sites. + It stores configuration details such as SSID, password, security type, etc. + """ + _name = 'unifi.wifi' + _description = 'UniFi WiFi Network' + _order = 'name' + + active = fields.Boolean( + string='Active', + default=True, + help='Whether this record is active in Odoo' + ) + + # Basic information + name = fields.Char( + string='Name', + required=True, + help='Name of the WiFi network (SSID)' + ) + + wifi_id = fields.Char( + string='WiFi ID', + required=True, + help='Unique identifier for this WiFi network in the UniFi system' + ) + + # WiFi configuration + enabled = fields.Boolean( + string='Enabled', + default=True, + help='Whether this WiFi network is enabled' + ) + + hidden = fields.Boolean( + string='Hidden SSID', + default=False, + help='Whether this SSID is hidden from network scans' + ) + + security = fields.Selection( + selection=[ + ('open', 'Open'), + ('wep', 'WEP'), + ('wpapsk', 'WPA Personal'), + ('wpa2psk', 'WPA2 Personal'), + ('wpapskwpa2psk', 'WPA/WPA2 Personal'), + ('wpa3', 'WPA3 Personal'), + ('wpa2enterprise', 'WPA2 Enterprise'), + ('wpa3enterprise', 'WPA3 Enterprise'), + ('radius', 'RADIUS') + ], + string='Security Type', + default='wpa2psk', + help='Security protocol used by this WiFi network' + ) + + password = fields.Char( + string='Password', + help='Password for this WiFi network (stored securely)' + ) + + # Network configuration + network_id = fields.Many2one( + comodel_name='unifi.network', + string='Network', + help='Network associated with this WiFi' + ) + + vlan_id = fields.Many2one( + comodel_name='unifi.vlan', + string='VLAN', + help='VLAN associated with this WiFi' + ) + + # WiFi settings + band = fields.Selection( + selection=[ + ('2g', '2.4 GHz'), + ('5g', '5 GHz'), + ('both', '2.4 & 5 GHz') + ], + string='Band', + default='both', + help='WiFi frequency band' + ) + + channel = fields.Integer( + string='Channel', + help='WiFi channel (0 for auto)' + ) + + channel_width = fields.Selection( + selection=[ + ('20', '20 MHz'), + ('40', '40 MHz'), + ('80', '80 MHz'), + ('160', '160 MHz') + ], + string='Channel Width', + help='WiFi channel width' + ) + + tx_power = fields.Selection( + selection=[ + ('auto', 'Auto'), + ('low', 'Low'), + ('medium', 'Medium'), + ('high', 'High'), + ('custom', 'Custom') + ], + string='TX Power', + default='auto', + help='Transmission power' + ) + + tx_power_custom = fields.Integer( + string='Custom TX Power', + help='Custom transmission power in dBm' + ) + + # Guest network settings + is_guest = fields.Boolean( + string='Guest Network', + default=False, + help='Whether this is a guest WiFi network' + ) + + guest_policy = fields.Selection( + selection=[ + ('none', 'No restrictions'), + ('mac', 'MAC filtering'), + ('auth', 'Authentication required') + ], + string='Guest Policy', + default='none', + help='Access policy for guest networks' + ) + + # Advanced settings + wpa_mode = fields.Selection( + selection=[ + ('wpa', 'WPA'), + ('wpa2', 'WPA2'), + ('wpa3', 'WPA3'), + ('wpa/wpa2', 'WPA/WPA2'), + ('wpa2/wpa3', 'WPA2/WPA3') + ], + string='WPA Mode', + help='WPA mode for this network' + ) + + wpa_encryption = fields.Selection( + selection=[ + ('auto', 'Auto'), + ('ccmp', 'CCMP (AES)'), + ('tkip', 'TKIP'), + ('tkip-ccmp', 'TKIP/CCMP') + ], + string='WPA Encryption', + default='auto', + help='Encryption method used for WPA' + ) + + pmf_mode = fields.Selection( + selection=[ + ('disabled', 'Disabled'), + ('optional', 'Optional'), + ('required', 'Required') + ], + string='PMF Mode', + default='optional', + help='Protected Management Frames mode' + ) + + # Tracking fields + created_at = fields.Datetime( + string='Created At', + default=fields.Datetime.now, + help='When this record was created in Odoo' + ) + + updated_at = fields.Datetime( + string='Updated At', + help='When this record was last updated in Odoo' + ) + + last_sync = fields.Datetime( + string='Last Sync', + help='When this record was last synchronized with UniFi' + ) + + # Raw data + raw_data = fields.Text( + string='Raw Data', + help='Raw JSON data from UniFi API' + ) + + raw_data_json = fields.Text( + string='Données brutes (JSON)', + compute='_compute_raw_data_json', + help='Données brutes de la configuration WiFi au format JSON formaté' + ) + + @api.depends('raw_data') + def _compute_raw_data_json(self): + for record in self: + record.raw_data_json = self.format_raw_data_json(record.raw_data) + + # Relations + site_id = fields.Many2one( + comodel_name='unifi.site', + string='Site', + required=True, + ondelete='cascade', + help='UniFi site this WiFi network belongs to' + ) + + # Methods + def create_or_update_from_data(self, site, wifi_data): + """Create or update a WiFi network from API data + + Args: + site: The UniFi site record + wifi_data: The WiFi network data from the API + + Returns: + record: The created or updated WiFi network record + """ + # Extract the WiFi ID + wifi_id = wifi_data.get('_id') or wifi_data.get('id') + if not wifi_id: + _logger.error("Cannot create/update WiFi network: missing ID") + return False + + # Search for an existing WiFi network with this ID + existing_wifi = self.search([ + ('wifi_id', '=', wifi_id), + ('site_id', '=', site.id) + ], limit=1) + + # Prepare values for creation/update + vals = { + 'wifi_id': wifi_id, + 'name': wifi_data.get('name', wifi_data.get('ssid', f"WiFi {wifi_id}")), + 'site_id': site.id, + 'enabled': wifi_data.get('enabled', True), + 'hidden': wifi_data.get('hide_ssid', False), + 'security': self._map_security_type(wifi_data.get('security', 'wpa2psk')), + 'is_guest': wifi_data.get('is_guest', False), + 'band': self._map_band(wifi_data.get('band', 'both')), + 'last_sync': fields.Datetime.now(), + 'raw_data': json.dumps(wifi_data) + } + + # Add password if available (and not empty) + if wifi_data.get('x_passphrase') and wifi_data.get('x_passphrase') != 'null': + vals['password'] = wifi_data.get('x_passphrase') + + # Map network and VLAN if available + if wifi_data.get('networkconf_id'): + network = self.env['unifi.network'].search([ + ('network_id', '=', wifi_data.get('networkconf_id')), + ('site_id', '=', site.id) + ], limit=1) + if network: + vals['network_id'] = network.id + + if wifi_data.get('vlan_id'): + vlan = self.env['unifi.vlan'].search([ + ('vlan_id', '=', wifi_data.get('vlan_id')), + ('site_id', '=', site.id) + ], limit=1) + if vlan: + vals['vlan_id'] = vlan.id + + # Add advanced settings if available + if 'channel' in wifi_data: + vals['channel'] = wifi_data.get('channel') + + if 'channel_width' in wifi_data: + vals['channel_width'] = str(wifi_data.get('channel_width', '20')) + + if 'tx_power' in wifi_data: + vals['tx_power'] = self._map_tx_power(wifi_data.get('tx_power')) + + if 'tx_power_mode' in wifi_data and wifi_data.get('tx_power_mode') == 'custom': + vals['tx_power'] = 'custom' + vals['tx_power_custom'] = wifi_data.get('tx_power') + + # WPA settings + if 'wpa_mode' in wifi_data: + vals['wpa_mode'] = self._map_wpa_mode(wifi_data.get('wpa_mode')) + + if 'wpa_enc' in wifi_data: + vals['wpa_encryption'] = self._map_wpa_encryption(wifi_data.get('wpa_enc')) + + if 'pmf_mode' in wifi_data: + vals['pmf_mode'] = self._map_pmf_mode(wifi_data.get('pmf_mode')) + + if existing_wifi: + # Update existing WiFi network + vals['updated_at'] = fields.Datetime.now() + existing_wifi.write(vals) + return existing_wifi + else: + # Create new WiFi network + return self.create(vals) + + def _map_security_type(self, security): + """Map UniFi security type to model selection value + + Args: + security: Security type from UniFi API + + Returns: + str: Mapped security type + """ + security_map = { + 'open': 'open', + 'wep': 'wep', + 'wpapsk': 'wpapsk', + 'wpa2psk': 'wpa2psk', + 'wpapskwpa2psk': 'wpapskwpa2psk', + 'wpa3': 'wpa3', + 'wpa2enterprise': 'wpa2enterprise', + 'wpa3enterprise': 'wpa3enterprise', + 'radius': 'radius' + } + return security_map.get(security, 'wpa2psk') + + def _map_band(self, band): + """Map UniFi band to model selection value + + Args: + band: Band from UniFi API + + Returns: + str: Mapped band + """ + band_map = { + 'ng': '2g', + '2g': '2g', + 'na': '5g', + '5g': '5g', + 'both': 'both', + 'all': 'both' + } + return band_map.get(band, 'both') + + def _map_tx_power(self, tx_power): + """Map UniFi TX power to model selection value + + Args: + tx_power: TX power from UniFi API + + Returns: + str: Mapped TX power + """ + # If it's already a string value that matches our selection options + if tx_power in ['auto', 'low', 'medium', 'high', 'custom']: + return tx_power + + # If it's a numeric value, map it to a named level + try: + power = int(tx_power) + if power <= 10: + return 'low' + elif power <= 18: + return 'medium' + else: + return 'high' + except (ValueError, TypeError): + return 'auto' + + def _map_wpa_mode(self, wpa_mode): + """Map UniFi WPA mode to model selection value + + Args: + wpa_mode: WPA mode from UniFi API + + Returns: + str: Mapped WPA mode + """ + wpa_mode_map = { + '1': 'wpa', + '2': 'wpa2', + '3': 'wpa3', + '12': 'wpa/wpa2', + '23': 'wpa2/wpa3' + } + return wpa_mode_map.get(str(wpa_mode), 'wpa2') + + def _map_wpa_encryption(self, wpa_enc): + """Map UniFi WPA encryption to model selection value + + Args: + wpa_enc: WPA encryption from UniFi API + + Returns: + str: Mapped WPA encryption + """ + wpa_enc_map = { + 'auto': 'auto', + 'ccmp': 'ccmp', + 'tkip': 'tkip', + 'tkip-ccmp': 'tkip-ccmp', + 'tkip,ccmp': 'tkip-ccmp' + } + return wpa_enc_map.get(wpa_enc, 'auto') + + def _map_pmf_mode(self, pmf_mode): + """Map UniFi PMF mode to model selection value + + Args: + pmf_mode: PMF mode from UniFi API + + Returns: + str: Mapped PMF mode + """ + pmf_mode_map = { + '0': 'disabled', + '1': 'optional', + '2': 'required', + 'disabled': 'disabled', + 'optional': 'optional', + 'required': 'required' + } + return pmf_mode_map.get(str(pmf_mode), 'optional') diff --git a/unifi_integration/security/ir.model.access.csv b/unifi_integration/security/ir.model.access.csv index 60a1f84..526306e 100644 --- a/unifi_integration/security/ir.model.access.csv +++ b/unifi_integration/security/ir.model.access.csv @@ -1,24 +1,24 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -unifi_integration.access_unifi_site_manager,access_unifi_site_manager,unifi_integration.model_unifi_site_manager,base.group_user,1,1,1,0 -unifi_integration.access_unifi_auth_session,access_unifi_auth_session,unifi_integration.model_unifi_auth_session,base.group_user,1,0,0,0 +unifi_integration.access_unifi_auth_session,access_unifi_auth_session,unifi_integration.model_unifi_auth_session,base.group_user,1,1,1,1 unifi_integration.access_unifi_mfa,access_unifi_mfa,unifi_integration.model_unifi_mfa,base.group_user,1,0,0,0 -unifi_integration.access_unifi_api_config,access_unifi_api_config,unifi_integration.model_unifi_api_config,base.group_user,1,0,0,0 -unifi_integration.access_unifi_api_log,access_unifi_api_log,unifi_integration.model_unifi_api_log,base.group_user,1,0,0,0 -unifi_integration.access_unifi_sync_job,access_unifi_sync_job,unifi_integration.model_unifi_sync_job,base.group_user,1,0,0,0 -unifi_integration.access_unifi_device,access_unifi_device,unifi_integration.model_unifi_device,base.group_user,1,0,0,0 -unifi_integration.access_unifi_network,access_unifi_network,unifi_integration.model_unifi_network,base.group_user,1,0,0,0 -unifi_integration.access_unifi_vlan,access_unifi_vlan,unifi_integration.model_unifi_vlan,base.group_user,1,0,0,0 -unifi_integration.access_unifi_user,access_unifi_user,unifi_integration.model_unifi_user,base.group_user,1,0,0,0 -unifi_integration.access_unifi_firewall_rule,access_unifi_firewall_rule,unifi_integration.model_unifi_firewall_rule,base.group_user,1,0,0,0 -unifi_integration.access_unifi_port_forward,access_unifi_port_forward,unifi_integration.model_unifi_port_forward,base.group_user,1,0,0,0 -unifi_integration.access_unifi_system_info,access_unifi_system_info,unifi_integration.model_unifi_system_info,base.group_user,1,0,0,0 -unifi_integration.access_unifi_dns,access_unifi_dns,unifi_integration.model_unifi_dns,base.group_user,1,0,0,0 -unifi_integration.access_unifi_dns_config,access_unifi_dns_config,unifi_integration.model_unifi_dns_config,base.group_user,1,0,0,0 -unifi_integration.access_unifi_routing,access_unifi_routing,unifi_integration.model_unifi_routing,base.group_user,1,0,0,0 -unifi_integration.access_unifi_routing_config,access_unifi_routing_config,unifi_integration.model_unifi_routing_config,base.group_user,1,0,0,0 +unifi_integration.access_unifi_api_config,access_unifi_api_config,unifi_integration.model_unifi_api_config,base.group_user,1,1,1,0 +unifi_integration.access_unifi_api_log,access_unifi_api_log,unifi_integration.model_unifi_api_log,base.group_user,1,1,1,0 +unifi_integration.access_unifi_sync_job,access_unifi_sync_job,unifi_integration.model_unifi_sync_job,base.group_user,1,1,1,0 +unifi_integration.access_unifi_device,access_unifi_device,unifi_integration.model_unifi_device,base.group_user,1,1,1,1 +unifi_integration.access_unifi_network,access_unifi_network,unifi_integration.model_unifi_network,base.group_user,1,1,1,1 +unifi_integration.access_unifi_vlan,access_unifi_vlan,unifi_integration.model_unifi_vlan,base.group_user,1,1,1,1 +unifi_integration.access_unifi_user,access_unifi_user,unifi_integration.model_unifi_user,base.group_user,1,1,1,1 +unifi_integration.access_unifi_firewall_rule,access_unifi_firewall_rule,unifi_integration.model_unifi_firewall_rule,base.group_user,1,1,1,1 +unifi_integration.access_unifi_port_forward,access_unifi_port_forward,unifi_integration.model_unifi_port_forward,base.group_user,1,1,1,1 +unifi_integration.access_unifi_system_info,access_unifi_system_info,unifi_integration.model_unifi_system_info,base.group_user,1,1,1,1 +unifi_integration.access_unifi_dns,access_unifi_dns,unifi_integration.model_unifi_dns,base.group_user,1,1,1,1 +unifi_integration.access_unifi_dns_config,access_unifi_dns_config,unifi_integration.model_unifi_dns_config,base.group_user,1,1,1,1 +unifi_integration.access_unifi_routing,access_unifi_routing,unifi_integration.model_unifi_routing,base.group_user,1,1,1,1 +unifi_integration.access_unifi_routing_config,access_unifi_routing_config,unifi_integration.model_unifi_routing_config,base.group_user,1,1,1,1 unifi_integration.access_unifi_site_import_wizard,access_unifi_site_import_wizard,unifi_integration.model_unifi_site_import_wizard,base.group_user,1,1,1,0 unifi_integration.access_unifi_site_discovery,access_unifi_site_discovery,unifi_integration.model_unifi_site_discovery,base.group_user,1,0,0,0 unifi_integration.access_unifi_site,access_unifi_site,unifi_integration.model_unifi_site,base.group_user,1,1,1,0 -unifi_integration.access_unifi_site_controller,access_unifi_site_controller,unifi_integration.model_unifi_site_controller,base.group_user,1,1,1,0 unifi_integration.access_unifi_dashboard_metric,access_unifi_dashboard_metric,unifi_integration.model_unifi_dashboard_metric,base.group_user,1,0,0,0 +unifi_integration.access_unifi_wifi,access_unifi_wifi,unifi_integration.model_unifi_wifi,base.group_user,1,1,1,1 +unifi_integration.access_unifi_vpn,access_unifi_vpn,unifi_integration.model_unifi_vpn,base.group_user,1,1,1,1 unifi_integration.access_unifi_dashboard_stat,access_unifi_dashboard_stat,unifi_integration.model_unifi_dashboard_stat,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/unifi_integration/security/unifi_security.xml b/unifi_integration/security/unifi_security.xml index 74e56ad..c4624c0 100644 --- a/unifi_integration/security/unifi_security.xml +++ b/unifi_integration/security/unifi_security.xml @@ -56,5 +56,30 @@ + + + + + UniFi Sync Job User Access + + [(1, '=', 1)] + + + + + + + + + + UniFi Sync Job Manager Access + + [(1, '=', 1)] + + + + + + diff --git a/unifi_integration/views/unifi_api_config_views.xml b/unifi_integration/views/unifi_api_config_views.xml index 26cf509..927d12c 100644 --- a/unifi_integration/views/unifi_api_config_views.xml +++ b/unifi_integration/views/unifi_api_config_views.xml @@ -75,10 +75,10 @@ -
- - -
+ + + +
diff --git a/unifi_integration/views/unifi_api_log_views.xml b/unifi_integration/views/unifi_api_log_views.xml index 9a18af4..329724f 100644 --- a/unifi_integration/views/unifi_api_log_views.xml +++ b/unifi_integration/views/unifi_api_log_views.xml @@ -49,24 +49,24 @@ - - - + + + - - + + - + - + diff --git a/unifi_integration/views/unifi_device_views.xml b/unifi_integration/views/unifi_device_views.xml index 4ce3904..8afe425 100644 --- a/unifi_integration/views/unifi_device_views.xml +++ b/unifi_integration/views/unifi_device_views.xml @@ -54,7 +54,10 @@ - + + + + diff --git a/unifi_integration/views/unifi_firewall_views.xml b/unifi_integration/views/unifi_firewall_views.xml index d67503e..9760b76 100644 --- a/unifi_integration/views/unifi_firewall_views.xml +++ b/unifi_integration/views/unifi_firewall_views.xml @@ -21,11 +21,14 @@ - - - - + + + + + + + @@ -40,9 +43,10 @@ - - - + + + + @@ -62,8 +66,12 @@ + + + + + - @@ -76,8 +84,9 @@ - - + + + @@ -92,6 +101,9 @@ + + + diff --git a/unifi_integration/views/unifi_network_views.xml b/unifi_integration/views/unifi_network_views.xml index 4642ddd..a6e3741 100644 --- a/unifi_integration/views/unifi_network_views.xml +++ b/unifi_integration/views/unifi_network_views.xml @@ -15,6 +15,9 @@ +
+
@@ -68,7 +71,10 @@
- + + + + diff --git a/unifi_integration/views/unifi_port_forward_views.xml b/unifi_integration/views/unifi_port_forward_views.xml index 4bf28bb..bef524d 100644 --- a/unifi_integration/views/unifi_port_forward_views.xml +++ b/unifi_integration/views/unifi_port_forward_views.xml @@ -29,20 +29,25 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
diff --git a/unifi_integration/views/unifi_site_views.xml b/unifi_integration/views/unifi_site_views.xml index 408e70b..42b34fa 100644 --- a/unifi_integration/views/unifi_site_views.xml +++ b/unifi_integration/views/unifi_site_views.xml @@ -24,10 +24,7 @@ unifi.site
-
-
+
@@ -35,7 +32,7 @@ type="object" class="oe_stat_button" icon="fa-history"> -
+
API Logs
@@ -43,7 +40,7 @@ type="object" class="oe_stat_button" icon="fa-refresh"> -
+
Sync Jobs
@@ -53,44 +50,79 @@ type="object" class="oe_stat_button" icon="fa-sitemap" - invisible="context.get('api_type') != 'controller'"> + invisible="api_type != 'controller'"> + + + + +
+
-

- +

+

- + @@ -100,28 +132,28 @@ - + - - + + - - + + - + - + - + + invisible="api_type != 'site_manager' or not mfa_enabled" + required="api_type == 'site_manager' and mfa_enabled"/> @@ -133,113 +165,298 @@ - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- - - - - -
- - - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- -
- - - -
+ - - - - + + + + + +
+
+ + + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + + +
+
+ + + +
+
+ + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + + +
+
+ + + +
+
+ + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + +
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ L'automatisation s'exécutera tous les minutes +
+ +
+
+ + + + - - - - - - - - - -
- -
-

- -

-
- - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- - - - - -
- - - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- - - - - -
- - -
-
- - - - - - - - -
- - - - + +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + unifi.wifi.list + unifi.wifi + + + + + + + + + + + + + + + + + + + unifi.wifi.search + unifi.wifi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WiFi Networks + unifi.wifi + list,form + + +

+ No WiFi networks found +

+

+ WiFi networks will appear here after synchronizing with your UniFi controller. +

+
+
+ + + +