bemade-addons/unifi_integration/controllers/main.py
2025-03-10 10:09:39 -04:00

691 lines
29 KiB
Python

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