231 lines
9.7 KiB
Python
231 lines
9.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
import requests
|
|
import json
|
|
import logging
|
|
import random
|
|
from datetime import timedelta
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
class UdmSite(models.Model):
|
|
"""Represents a UniFi site managed by one or more UDM Pro
|
|
|
|
This model is the central entity that groups all UniFi configurations and devices.
|
|
Each site can have multiple configurations, devices, networks, and users.
|
|
"""
|
|
_name = 'udm.site'
|
|
_description = 'UniFi Site'
|
|
_order = 'name'
|
|
_inherit = ['mail.thread']
|
|
|
|
# Basic site information
|
|
name = fields.Char(string='Name', required=True, tracking=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', tracking=True,
|
|
help='Site description')
|
|
address = fields.Text(string='Physical Address', tracking=True,
|
|
help='Physical location of this site')
|
|
active = fields.Boolean(string='Active', default=True, tracking=True,
|
|
help='Indicates if this site is currently active')
|
|
|
|
# Controller configuration
|
|
controller_type = fields.Selection([
|
|
('udm', 'UDM/UDR'),
|
|
('software', 'Software Controller')
|
|
], string='Controller Type', required=True, default='udm', tracking=True,
|
|
help='Type of UniFi controller managing this site')
|
|
|
|
host = fields.Char(string='Host', required=True, tracking=True,
|
|
help='IP address or hostname of the controller')
|
|
port = fields.Integer(string='Port', default=443, required=True,
|
|
help='Port number (default: 443)')
|
|
username = fields.Char(string='Username', required=True, tracking=True)
|
|
password = fields.Char(string='Password', required=True)
|
|
mfa_token = fields.Char(string='MFA Token',
|
|
help='Two-factor authentication token if enabled')
|
|
|
|
# Configuration data
|
|
timestamp = fields.Datetime(string='Last Update', tracking=True)
|
|
raw_data = fields.Text(string='Raw Data',
|
|
help='Raw configuration data in JSON format')
|
|
|
|
# Related Records
|
|
configuration_ids = fields.One2many('udm.configuration', 'site_id',
|
|
string='Configurations',
|
|
help='Configurations for this site')
|
|
network_ids = fields.One2many('udm.network', 'site_id',
|
|
string='Networks',
|
|
help='Networks in this site')
|
|
vlan_ids = fields.One2many('udm.vlan', 'site_id',
|
|
string='VLANs',
|
|
help='VLANs in this site')
|
|
device_ids = fields.One2many('udm.device', 'site_id',
|
|
string='Devices',
|
|
help='Devices in this site')
|
|
user_ids = fields.One2many('udm.user', 'site_id',
|
|
string='Users',
|
|
help='Users in this site')
|
|
firewall_rule_ids = fields.One2many('udm.firewall.rule', 'site_id',
|
|
string='Firewall Rules',
|
|
help='Firewall rules for this site')
|
|
port_forward_ids = fields.One2many('udm.port.forward', 'site_id',
|
|
string='Port Forwards',
|
|
help='Port forwarding rules for this site')
|
|
dns_config_ids = fields.One2many('udm.dns.config', 'site_id',
|
|
string='DNS Configurations',
|
|
help='DNS configurations for this site')
|
|
routing_config_ids = fields.One2many('udm.routing.config', 'site_id',
|
|
string='Routing Configurations',
|
|
help='Routing configurations for this site')
|
|
|
|
# Dashboard Metrics
|
|
dashboard_metric_ids = fields.One2many('udm.dashboard.metric', 'site_id',
|
|
string='Dashboard Metrics',
|
|
help='Real-time metrics for this site')
|
|
dashboard_stat_ids = fields.One2many('udm.dashboard.stat', 'site_id',
|
|
string='Statistics',
|
|
help='Historical statistics for this site')
|
|
|
|
# Computed Fields
|
|
config_count = fields.Integer(compute='_compute_counts',
|
|
string='Configuration Count',
|
|
help='Number of configurations in this site')
|
|
device_count = fields.Integer(compute='_compute_device_count',
|
|
string='Total Devices',
|
|
help='Total number of devices in this site')
|
|
client_count = fields.Integer(compute='_compute_client_count',
|
|
string='Connected Clients',
|
|
help='Number of currently connected clients')
|
|
|
|
@api.depends('configuration_ids')
|
|
def _compute_counts(self):
|
|
"""Compute the number of configurations in this site"""
|
|
for site in self:
|
|
site.config_count = len(site.configuration_ids)
|
|
|
|
@api.depends('device_ids')
|
|
def _compute_device_count(self):
|
|
"""Compute the total number of devices in this site"""
|
|
for site in self:
|
|
site.device_count = len(site.device_ids)
|
|
|
|
@api.depends('user_ids')
|
|
def _compute_client_count(self):
|
|
"""Compute the number of connected clients"""
|
|
for site in self:
|
|
site.client_count = len(site.user_ids.filtered(lambda u: u.is_connected))
|
|
|
|
def action_view_configurations(self):
|
|
"""Open the configurations view filtered for this site"""
|
|
self.ensure_one()
|
|
return {
|
|
'name': _('Configurations'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'udm.configuration',
|
|
'view_mode': 'tree,form',
|
|
'domain': [('site_id', '=', self.id)],
|
|
'context': {'default_site_id': self.id},
|
|
}
|
|
|
|
def action_view_dashboard(self):
|
|
"""Display the dashboard for this site"""
|
|
self.ensure_one()
|
|
return {
|
|
'name': _('Site Dashboard'),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'udm.dashboard',
|
|
'view_mode': 'kanban,form',
|
|
'domain': [('site_id', '=', self.id)],
|
|
'context': {'default_site_id': self.id},
|
|
}
|
|
|
|
def action_refresh_metrics(self):
|
|
"""Refresh the dashboard metrics and statistics for this site"""
|
|
self.ensure_one()
|
|
for config in self.configuration_ids:
|
|
config.action_update_dashboard_metrics()
|
|
|
|
_sql_constraints = [
|
|
('name_uniq', 'unique(name)',
|
|
'Site name must be unique!'),
|
|
]
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
"""Override create to verify connection before saving"""
|
|
for vals in vals_list:
|
|
if not self._verify_connection(vals):
|
|
raise UserError(_('Could not connect to the UniFi controller. '
|
|
'Please verify your credentials and connection details.'))
|
|
return super(UdmSite, self).create(vals_list)
|
|
|
|
def write(self, vals):
|
|
"""Override write to verify connection if connection details change"""
|
|
if any(field in vals for field in ['host', 'port', 'username',
|
|
'password', 'mfa_token']):
|
|
for record in self:
|
|
test_vals = {**record.copy_data()[0], **vals}
|
|
if not self._verify_connection(test_vals):
|
|
raise UserError(_('Could not connect to the UniFi controller. '
|
|
'Please verify your credentials and connection details.'))
|
|
return super(UdmSite, self).write(vals)
|
|
|
|
def _verify_connection(self, vals):
|
|
"""Verify connection to the UniFi controller
|
|
|
|
Args:
|
|
vals (dict): Values to use for connection test
|
|
|
|
Returns:
|
|
bool: True if connection successful, False otherwise
|
|
"""
|
|
try:
|
|
# Build base URL based on controller type
|
|
base_url = f"https://{vals['host']}:{vals.get('port', 443)}"
|
|
if vals.get('controller_type') == 'software':
|
|
base_url += '/api'
|
|
else:
|
|
base_url += '/proxy/network'
|
|
|
|
# Disable SSL verification warning
|
|
import urllib3
|
|
urllib3.disable_warnings()
|
|
|
|
# Try to authenticate
|
|
session = requests.Session()
|
|
response = session.post(
|
|
f"{base_url}/api/auth/login",
|
|
json={
|
|
'username': vals['username'],
|
|
'password': vals['password'],
|
|
'remember': True,
|
|
'token': vals.get('mfa_token', '')
|
|
},
|
|
verify=False
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
return True
|
|
|
|
_logger.error(
|
|
"Authentication failed for UniFi controller at %s: %s",
|
|
vals['host'],
|
|
response.text
|
|
)
|
|
return False
|
|
|
|
except Exception as e:
|
|
_logger.error(
|
|
"Error connecting to UniFi controller at %s: %s",
|
|
vals['host'],
|
|
str(e)
|
|
)
|
|
return False
|