bemade-addons/unifi_integration/models/unifi_vpn.py
2025-03-27 15:34:13 -04:00

241 lines
10 KiB
Python

# -*- 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 = "<p><em>Aucune donnée disponible</em></p>"
continue
try:
# Analyser les données JSON
data = json.loads(record.raw_data)
# Créer une représentation HTML formatée
html = "<div style='font-family: monospace;'>"
# Informations générales
html += "<h3 style='color: #2C3E50;'>Informations générales</h3>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Nom:</td><td>{data.get('name', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Type:</td><td>{data.get('vpn_type', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Activé:</td><td>{'Oui' if data.get('enabled') else 'Non'}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Interface:</td><td>{data.get('ifname', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>But:</td><td>{data.get('purpose', 'N/A')}</td></tr>"
html += "</table>"
# Configuration IPsec
if data.get('vpn_type') == 'ipsec-vpn':
html += "<h3 style='color: #2C3E50; margin-top: 15px;'>Configuration IPsec</h3>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>IP Pair:</td><td>{data.get('ipsec_peer_ip', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>IP Locale:</td><td>{data.get('ipsec_local_ip', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Interface:</td><td>{data.get('ipsec_interface', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Échange de clés:</td><td>{data.get('ipsec_key_exchange', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Profil:</td><td>{data.get('ipsec_profile', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>PFS:</td><td>{'Oui' if data.get('ipsec_pfs') else 'Non'}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Routage dynamique:</td><td>{'Oui' if data.get('ipsec_dynamic_routing') else 'Non'}</td></tr>"
html += "</table>"
# Paramètres IKE
html += "<h4 style='color: #2C3E50; margin-top: 10px;'>Paramètres IKE</h4>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Groupe DH:</td><td>{data.get('ipsec_ike_dh_group', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Chiffrement:</td><td>{data.get('ipsec_ike_encryption', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Hash:</td><td>{data.get('ipsec_ike_hash', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Durée de vie (s):</td><td>{data.get('ipsec_ike_lifetime', 'N/A')}</td></tr>"
html += "</table>"
# Paramètres ESP
html += "<h4 style='color: #2C3E50; margin-top: 10px;'>Paramètres ESP</h4>"
html += "<table style='width: 100%; border-collapse: collapse;'>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Groupe DH:</td><td>{data.get('ipsec_esp_dh_group', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Chiffrement:</td><td>{data.get('ipsec_esp_encryption', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Hash:</td><td>{data.get('ipsec_esp_hash', 'N/A')}</td></tr>"
html += f"<tr><td style='padding: 4px; font-weight: bold;'>Durée de vie (s):</td><td>{data.get('ipsec_esp_lifetime', 'N/A')}</td></tr>"
html += "</table>"
# Sous-réseaux distants
remote_subnets = data.get('remote_vpn_subnets', [])
if remote_subnets:
html += "<h3 style='color: #2C3E50; margin-top: 15px;'>Sous-réseaux distants</h3>"
html += "<ul style='margin-top: 5px;'>"
for subnet in remote_subnets:
html += f"<li>{subnet}</li>"
html += "</ul>"
html += "</div>"
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"<p><em>Erreur lors du formatage des données: {str(e)}</em></p>"
@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)