bemade-addons/unifi_integration/models/unifi_site.py
2025-05-05 10:29:07 -04:00

1864 lines
No EOL
66 KiB
Python

# -*- 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
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, 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).
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'
_order = 'name'
_inherit = ['mail.thread', 'mail.activity.mixin']
# Basic site information
name = fields.Char(
string='Name',
required=True,
help='Site name'
)
site_id = fields.Char(
string='Site ID',
help="Site identifier in UniFi (usually 'default')",
default='default',
readonly=True,
required=True
)
description = fields.Text(
string='Description',
help='Site description'
)
address = fields.Text(
string='Physical Address',
help='Physical location of this site'
)
active = fields.Boolean(
string='Active',
default=True,
help='Indicates if this site is currently active'
)
# API Type - New field to distinguish between Site Manager and Controller APIs
api_type = fields.Selection(
selection=[
('site_manager', 'Site Manager (Cloud)'),
('controller', 'Controller (Local)')
],
string='API Type',
required=True,
default='controller',
help='Type of API used to connect to this site'
)
# Relation avec la configuration API
api_config_id = fields.Many2one(
comodel_name='unifi.api.config',
string='Configuration API',
help='Configuration API utilisée pour ce site'
)
# 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'
)
# Connection information - Common fields
timestamp = fields.Datetime(
string='Created Date',
default=lambda self: fields.Datetime.now(),
readonly=True,
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'),
('failed', 'Failed'),
('pending', 'Pending')
],
string='Import Status',
default='pending',
help='Status of the last configuration import'
)
# SSL verification - Common for both API types
verify_ssl = fields.Boolean(
string='Verify SSL',
default=False,
help='Enable SSL certificate verification'
)
# Controller API specific fields
controller_type = fields.Selection(
selection=[
('standard', 'Standard Controller'),
('udm', 'UDM Pro / UCG Max')
],
string='Controller Type',
default='standard',
help='Type of UniFi controller (affects API endpoints)'
)
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'
)
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'
)
mfa_enabled = fields.Boolean(
string='MFA Enabled',
default=False,
help='Enable Multi-Factor Authentication'
)
mfa_token = fields.Char(
string='MFA Token',
help='Multi-Factor Authentication token'
)
# 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'
)
# 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'
)
# Relations avec d'autres modèles
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'
)
def action_sync_networks(self):
"""Synchronize networks from UniFi to Odoo
This method fetches the latest network data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
self._sync_networks()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
# Relations pour la configuration DNS
dns_config_ids = fields.One2many(
comodel_name='unifi.dns.config',
inverse_name='site_id',
string='DNS Configurations',
help='DNS configurations for 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'
)
# Champ pour le nombre de clients connectés
client_count = fields.Integer(
string='Connected Clients',
compute='_compute_client_count',
compute_sudo=True,
store=False,
help='Number of clients currently connected to this site'
)
# Champs pour les données brutes
raw_data = fields.Text(
string='Raw Data',
help='Raw data from the API in JSON format',
copy=False
)
raw_data_json = fields.Text(
string='Raw Data JSON',
help='Formatted JSON data for display',
compute='_compute_raw_data_json',
store=False
)
@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, 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
try:
site.network_count = len(site.network_ids) if site.network_ids else 0
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 = 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('raw_data')
def _compute_raw_data_json(self):
"""Format the raw data as JSON for display
This method takes the raw data from the API and formats it as JSON
for display in the UI.
"""
for site in self:
if site.raw_data:
try:
# Try to parse and pretty-print the JSON
import json
data = json.loads(site.raw_data)
site.raw_data_json = json.dumps(data, indent=4, sort_keys=True)
except Exception as e:
_logger.error('Error formatting raw data as JSON for site %s: %s', site.name, str(e))
site.raw_data_json = site.raw_data
else:
site.raw_data_json = False
@api.depends('user_ids')
def _compute_client_count(self):
"""Compute the number of connected clients
This method counts only users that are currently connected to the network.
It relies on the 'is_connected' flag on user records.
"""
for site in self:
try:
if site.user_ids:
# Filter users that have is_connected=True
site.client_count = len(site.user_ids.filtered(lambda u: u.is_connected if hasattr(u, 'is_connected') else False))
else:
site.client_count = 0
except Exception as e:
_logger.error('Error computing client count for site %s: %s', site.name, str(e))
site.client_count = 0
@api.depends('api_config_id', 'api_type')
@api.depends('api_config_id', 'api_type')
def _compute_connection_fields(self):
"""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
@api.depends('ssl_cert_file', 'ssl_cert_filename')
def _compute_ssl_cert_path(self):
"""Compute the path to the SSL certificate file
This method creates a temporary file with the SSL certificate content
and sets the ssl_cert_path field to the path of this file.
"""
for record in self:
if record.ssl_cert_file and record.ssl_cert_filename:
# Créer un fichier temporaire pour le certificat
try:
# Créer un répertoire temporaire s'il n'existe pas
temp_dir = tempfile.gettempdir()
cert_dir = os.path.join(temp_dir, 'unifi_certs')
os.makedirs(cert_dir, exist_ok=True)
# Créer le fichier de certificat
cert_path = os.path.join(cert_dir, record.ssl_cert_filename)
with open(cert_path, 'wb') as f:
f.write(base64.b64decode(record.ssl_cert_file))
# Définir le chemin du certificat
record.ssl_cert_path = cert_path
except Exception as e:
_logger.error("Error creating SSL certificate file: %s", str(e))
record.ssl_cert_path = False
else:
record.ssl_cert_path = False
def test_connection(self):
"""Test connection to the UniFi site
This method tests the connection to the UniFi site using the appropriate API.
Returns:
dict: Dictionary with connection test results
"""
self.ensure_one()
# Create an API log entry
api_log = self.env['unifi.api.log'].create({
'site_id': self.id,
'api_method': 'test_connection',
'message': _("Testing connection to UniFi site"),
'direction': 'outbound',
'status': 'pending',
})
# Test connection based on API type
if self.api_type == 'controller':
return self._test_controller_connection(api_log)
elif self.api_type == 'site_manager':
return self._test_site_manager_connection(api_log)
else:
# Update the API log
self._update_api_log(api_log, {
'status': 'error',
'message': _("Unknown API type: %s") % self.api_type,
})
# Return failure
return {
'success': False,
'message': _("Unknown API type: %s") % self.api_type,
'details': {},
}
def _test_controller_connection(self, api_log=None):
"""Test connection to the UniFi Controller
Args:
api_log: Optional API log record to update with results
Returns:
dict: Dictionary with connection test results
"""
# Delegate to the Controller API mixin
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._test_controller_connection(self, api_log)
def _test_site_manager_connection(self, api_log=None):
"""Test connection to the UniFi Site Manager API
Args:
api_log: Optional API log record to update with results
Returns:
dict: Dictionary with connection test results
"""
# Delegate to the Site Manager API mixin
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._test_site_manager_connection(self, api_log)
def _update_api_log(self, api_log, values):
"""Update an API log record with the provided values
Args:
api_log: API log record to update
values: Dictionary of values to update
Returns:
unifi.api.log: Updated API log record
"""
if not api_log:
return False
try:
# Update the API log
api_log.write(values)
return api_log
except Exception as e:
_logger.error("Error updating API log: %s", str(e))
return False
def _check_auth_session(self):
"""Check if the site has a valid authentication session
Returns:
bool: True if the site has a valid authentication session, False otherwise
"""
self.ensure_one()
# Check if we have an authentication session
if not self.auth_session_id:
return False
# Check if the session is still valid
if self.auth_session_id.is_expired:
return False
# Session is valid
return True
def _get_auth_cookies(self):
"""Get authentication cookies for the site
Returns:
dict: Dictionary of cookies or False if no valid session
"""
self.ensure_one()
# Check if we have a valid authentication session
if not self._check_auth_session():
return False
# Get the cookies from the session
cookies = {}
if self.auth_session_id.cookies:
try:
cookies = ast.literal_eval(self.auth_session_id.cookies)
except (ValueError, SyntaxError):
_logger.error("Error parsing cookies: %s", self.auth_session_id.cookies)
return False
# Return the cookies
return cookies
def _create_auth_session(self, cookies, endpoint=None):
"""Create or update an authentication session for the site
Args:
cookies: Dictionary of cookies
endpoint: Optional endpoint used for authentication
Returns:
unifi.auth.session: Created or updated authentication session
"""
self.ensure_one()
# Convert cookies to string
cookies_str = str(cookies)
# Check if we already have an authentication session
if self.auth_session_id:
# Update the existing session
self.auth_session_id.write({
'cookies': cookies_str,
'last_used': fields.Datetime.now(),
'is_expired': False,
'endpoint': endpoint or self.auth_session_id.endpoint,
})
return self.auth_session_id
else:
# Create a new session
session = self.env['unifi.auth.session'].create({
'site_id': self.id,
'cookies': cookies_str,
'created': fields.Datetime.now(),
'last_used': fields.Datetime.now(),
'is_expired': False,
'endpoint': endpoint,
})
# Update the site
self.auth_session_id = session.id
# Return the session
return session
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
# Méthodes refactorisées pour déléguer aux mixins
def get_device_data(self):
"""Récupère les données des appareils du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les appareils.
Returns:
list: Liste des données de tous les appareils
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_device_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_device_data(self)
else:
return False
def action_configure_controller(self):
"""Open a wizard to configure the Controller API settings
This method is called from the UI to configure the Controller API settings.
Returns:
dict: Action to open the configuration wizard
"""
self.ensure_one()
return {
'name': _('Configure Controller API'),
'type': 'ir.actions.act_window',
'res_model': 'unifi.site',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref('unifi_integration.view_unifi_site_form').id,
'target': 'new',
'context': {'default_api_type': 'controller'}
}
def action_configure_site_manager(self):
"""Open a wizard to configure the Site Manager API settings
This method is called from the UI to configure the Site Manager API settings.
Returns:
dict: Action to open the configuration wizard
"""
self.ensure_one()
return {
'name': _('Configure Site Manager API'),
'type': 'ir.actions.act_window',
'res_model': 'unifi.site',
'res_id': self.id,
'view_mode': 'form',
'view_id': self.env.ref('unifi_integration.view_unifi_site_form').id,
'target': 'new',
'context': {'default_api_type': 'site_manager'}
}
def action_test_connection(self):
"""Test the connection to the UniFi site and import if successful
This method is called from the UI to test the connection to the UniFi site
and import the site if the connection is successful.
Returns:
dict: Action to open the site form or a notification of failure
"""
self.ensure_one()
# Test the connection
result = self.test_connection()
if result.get('success'):
# Update the site status
self.write({
'import_status': 'success',
'last_import_date': fields.Datetime.now()
})
# Return an action to open the site form
return {
'type': 'ir.actions.act_window',
'res_model': 'unifi.site',
'res_id': self.id,
'view_mode': 'form',
'target': 'current',
'context': {'form_view_initial_mode': 'edit'}
}
else:
# Update the site status
self.write({
'import_status': 'failed'
})
# Return a notification of failure
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Connection Failed'),
'message': result.get('message', _('Failed to connect to the UniFi site.')),
'sticky': True,
'type': 'danger'
}
}
def _sync_networks(self):
"""Synchronize networks from UniFi to Odoo
This method fetches the latest network data from UniFi and updates
the corresponding records in Odoo.
"""
self.ensure_one()
try:
# Vérifier si nous avons une session d'authentification valide
if not self._check_auth_session():
_logger.debug("Pas de session d'authentification valide pour le site %s, tentative d'authentification", self.name)
# S'authentifier en fonction du type d'API
if self.api_type == 'controller':
# Créer un log d'API pour l'authentification
api_log = self._create_api_log(
api_method='authenticate_controller',
message_text=_("Authenticating to UniFi Controller"),
direction='outbound'
)
# Utiliser le mixin du contrôleur pour tester la connexion (qui authentifie aussi)
controller_api = self.env['unifi.controller.api.mixin']
auth_result = controller_api._test_controller_connection(self, api_log)
if not auth_result.get('success', False):
_logger.error("Échec de l'authentification au contrôleur pour le site %s: %s",
self.name, auth_result.get('message', 'Unknown error'))
return False
elif self.api_type == 'site_manager':
# Créer un log d'API pour l'authentification
api_log = self._create_api_log(
api_method='authenticate_site_manager',
message_text=_("Authenticating to UniFi Site Manager"),
direction='outbound'
)
# Utiliser le mixin du site manager pour tester la connexion (qui authentifie aussi)
site_manager_api = self.env['unifi.site.manager.api.mixin']
auth_result = site_manager_api._test_site_manager_connection(self, api_log)
if not auth_result.get('success', False):
_logger.error("Échec de l'authentification au site manager pour le site %s: %s",
self.name, auth_result.get('message', 'Unknown error'))
return False
else:
_logger.error("Type d'API non pris en charge pour le site %s: %s", self.name, self.api_type)
return False
_logger.debug("Authentification réussie pour le site %s", self.name)
else:
_logger.debug("Session d'authentification valide trouvée pour le site %s", self.name)
# Récupérer les données des réseaux
network_data = self.get_network_data()
_logger.debug("Données de réseau récupérées pour le site %s: %s", self.name, network_data)
if network_data:
# Mettre à jour la date de dernière synchronisation
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
# Créer/mettre à jour les réseaux
networks_created = 0
for network in network_data:
self.env['unifi.network'].create_or_update_from_data(self, network)
networks_created += 1
_logger.info("%d réseaux créés/mis à jour pour le site %s", networks_created, self.name)
return True
else:
_logger.warning("Aucune donnée de réseau récupérée pour le site %s", self.name)
return False
except Exception as e:
_logger.error("Error synchronizing networks for site %s: %s", self.name, str(e), exc_info=True)
raise
@api.model
def action_sync_devices(self):
"""Synchronize devices from UniFi to Odoo
This method fetches the latest device data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
device_data = self.get_device_data()
if device_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée d\'appareil trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des appareils: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des appareils: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_users(self):
"""Synchronize users from UniFi to Odoo
This method fetches the latest user data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
user_data = self.get_user_data()
if user_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée d\'utilisateur trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des utilisateurs: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des utilisateurs: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_vlans(self):
"""Synchronize VLANs from UniFi to Odoo
This method fetches the latest VLAN data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
vlan_data = self.get_vlan_data()
if vlan_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée de VLAN trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des VLANs: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des VLANs: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_firewall_rules(self):
"""Synchronize firewall rules from UniFi to Odoo
This method fetches the latest firewall rule data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
firewall_data = self.get_firewall_data()
if firewall_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée de règle de pare-feu trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des règles de pare-feu: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des règles de pare-feu: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_port_forwards(self):
"""Synchronize port forwards from UniFi to Odoo
This method fetches the latest port forward data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
port_forward_data = self.get_port_forward_data()
if port_forward_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée de redirection de port trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des redirections de port: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des redirections de port: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_system_info(self):
"""Synchronize system info from UniFi to Odoo
This method fetches the latest system info data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
system_info_data = self.get_system_info_data()
if system_info_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée système trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des informations système: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des informations système: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_dns(self):
"""Synchronize DNS entries from UniFi to Odoo
This method fetches the latest DNS data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
dns_data = self.get_dns_data()
if dns_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée DNS trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des entrées DNS: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des entrées DNS: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_wifi(self):
"""Synchronize WiFi networks from UniFi to Odoo
This method fetches the latest WiFi network data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
wifi_data = self.get_wifi_data()
if wifi_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée de réseau WiFi trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des réseaux WiFi: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des réseaux WiFi: %s') % str(e),
'type': 'danger',
}
}
@api.model
def action_sync_routing(self):
"""Synchronize routing configurations from UniFi to Odoo
This method fetches the latest routing configuration data from UniFi and updates
the corresponding records in Odoo.
Returns:
dict: Action to reload the view
"""
self.ensure_one()
try:
routing_data = self.get_routing_data()
if routing_data:
self.write({
'last_sync': fields.Datetime.now(),
'last_update': fields.Datetime.now()
})
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Synchronisation'),
'message': _('Aucune donnée de routage trouvée.'),
'type': 'warning',
}
}
except Exception as e:
_logger.error("Erreur lors de la synchronisation des configurations de routage: %s", str(e))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Erreur'),
'message': _('Erreur lors de la synchronisation des configurations de routage: %s') % str(e),
'type': 'danger',
}
}
def get_network_data(self):
"""Récupère les données des réseaux du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les réseaux.
Returns:
list: Liste des données de tous les réseaux
"""
self.ensure_one()
_logger.debug("Début de get_network_data pour le site %s (type: %s)", self.name, self.api_type)
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
_logger.debug("Utilisation de l'API Controller pour récupérer les réseaux")
controller_api = self.env['unifi.controller.api.mixin']
data = controller_api._get_controller_network_data(self)
_logger.debug("Données récupérées depuis l'API Controller: %s", data)
return data
elif self.api_type == 'site_manager':
_logger.debug("Utilisation de l'API Site Manager pour récupérer les réseaux")
site_manager_api = self.env['unifi.site.manager.api.mixin']
data = site_manager_api._get_site_manager_network_data(self)
_logger.debug("Données récupérées depuis l'API Site Manager: %s", data)
return data
else:
_logger.debug("Type d'API non reconnu: %s", self.api_type)
return False
def get_dns_data(self):
"""Récupère les données DNS du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations DNS.
Returns:
list: Liste des données DNS
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_dns_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_dns_data(self)
else:
return False
def get_wifi_data(self):
"""Récupère les données des réseaux WiFi du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les réseaux WiFi.
Returns:
list: Liste des données des réseaux WiFi
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_wifi_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_wifi_data(self)
else:
return False
def get_routing_data(self):
"""Récupère les données de routage du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations de routage.
Returns:
list: Liste des données de routage
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_routing_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_routing_data(self)
else:
return False
"""Récupère les données des réseaux du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les réseaux.
Returns:
list: Liste des données de tous les réseaux
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_network_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_network_data(self)
else:
return False
def get_vlan_data(self):
"""Récupère les données des VLANs du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les VLANs.
Returns:
list: Liste des données de tous les VLANs
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_vlan_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_vlan_data(self)
else:
return False
def get_user_data(self):
"""Récupère les données des utilisateurs du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les utilisateurs.
Returns:
list: Liste des données de tous les utilisateurs
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_user_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_user_data(self)
else:
return False
def get_firewall_data(self):
"""Récupère les données des règles de pare-feu du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les règles de pare-feu.
Returns:
list: Liste des données de toutes les règles de pare-feu
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_firewall_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_firewall_data(self)
else:
return False
def get_port_forward_data(self):
"""Récupère les données des redirections de port du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations sur les redirections de port.
Returns:
list: Liste des données de toutes les redirections de port
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_port_forward_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_port_forward_data(self)
else:
return False
def get_system_info_data(self):
"""Récupère les données d'information système du site
Cette méthode utilise l'API appropriée en fonction du type de site
pour obtenir les informations système.
Returns:
list: Liste des données d'information système
"""
self.ensure_one()
# Delegate to the appropriate API mixin
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
return controller_api._get_controller_system_info_data(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
return site_manager_api._get_site_manager_system_info_data(self)
else:
return False
def action_sync_now(self):
"""Action to synchronize the site now
This method is called from the UI to synchronize the site immediately.
Returns:
dict: Dictionary with action result
"""
self.ensure_one()
# Synchronize the site
if self.api_type == 'controller':
controller_api = self.env['unifi.controller.api.mixin']
result = controller_api._sync_controller(self)
elif self.api_type == 'site_manager':
site_manager_api = self.env['unifi.site.manager.api.mixin']
result = site_manager_api._sync_site_manager(self)
else:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Synchronization"),
'message': _("Unknown API type: %s") % self.api_type,
'sticky': True,
'type': 'danger',
'next': {
'type': 'ir.actions.act_window_close',
},
},
}
# Show a notification with the result
if result['success']:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Synchronization"),
'message': result['message'],
'sticky': False,
'type': 'success',
'next': {
'type': 'ir.actions.act_window_close',
},
},
}
else:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _("Synchronization"),
'message': result['message'],
'sticky': True,
'type': 'danger',
'next': {
'type': 'ir.actions.act_window_close',
},
},
}
def action_import_site(self):
"""Lance directement l'import pour le site sélectionné
Cette méthode est appelée lorsque l'utilisateur clique sur le bouton
'Importer' dans la vue liste des sites UniFi. Elle déclenche
immédiatement le processus d'importation pour le site sélectionné.
Returns:
dict: Notification de succès ou d'échec
"""
self.ensure_one()
# Vérifier si le site a déjà été configuré pour une API
if not self.api_type:
# Si aucun type d'API n'est défini, ouvrir l'assistant d'importation
return {
'name': _('Import UniFi Site'),
'type': 'ir.actions.act_window',
'res_model': 'unifi.site.import.wizard',
'view_mode': 'form',
'target': 'new',
'context': {'default_name': self.name, 'default_site_id': self.site_id, 'default_api_type': self.api_type}
}
# Action methods for viewing related records
def action_view_networks(self):
"""Open the networks view filtered for this site"""
self.ensure_one()
return {
'name': _('Networks'),
'view_mode': 'list,form',
'res_model': 'unifi.network',
'domain': [('site_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_site_id': self.id}
}
def action_view_devices(self):
"""Open the devices view filtered for this site"""
self.ensure_one()
return {
'name': _('Devices'),
'view_mode': 'list,form',
'res_model': 'unifi.device',
'domain': [('site_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_site_id': self.id}
}
def action_view_users(self):
"""Open the users view filtered for this site"""
self.ensure_one()
return {
'name': _('Users'),
'view_mode': 'list,form',
'res_model': 'unifi.user',
'domain': [('site_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_site_id': self.id}
}
def action_view_firewall_rules(self):
"""Open the firewall rules view filtered for this site"""
self.ensure_one()
return {
'name': _('Firewall Rules'),
'view_mode': 'list,form',
'res_model': 'unifi.firewall.rule',
'domain': [('site_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_site_id': self.id}
}
def action_view_api_logs(self):
"""Open the API logs view filtered for this site"""
self.ensure_one()
return {
'name': _('API Logs'),
'view_mode': 'list,form',
'res_model': 'unifi.api.log',
'domain': [('site_id', '=', self.id)],
'type': 'ir.actions.act_window',
'context': {'default_site_id': self.id}
}
def action_view_sync_jobs(self):
"""Open the sync jobs view filtered for this site"""
self.ensure_one()
return {
'name': _('Sync Jobs'),
'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}
}
# Tester la connexion
connection_test = self.test_connection()
connection_success = connection_test.get('success', False)
if connection_success:
# Déclencher la synchronisation
self.action_sync_now()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('Connection successful! Synchronization started for site %s.') % self.name,
'sticky': False,
'type': 'success',
}
}
else:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Error'),
'message': _('Failed to connect to site %s. Please check your connection settings.') % self.name,
'sticky': True,
'type': 'danger',
}
}