Unifi to test

This commit is contained in:
xtremxpert 2025-01-06 08:02:03 -05:00
parent 010752b01f
commit 5050839056
56 changed files with 5086 additions and 643 deletions

View file

@ -5,18 +5,45 @@
'website': 'https://www.bemade.org',
'license': 'LGPL-3',
'depends': [
'base'
'base',
'web',
'base_setup'
],
'external_dependencies': {
'python': ['pyunifi'],
},
'data': [
'views/controller_views.xml',
'views/firewall_rule_action.xml',
'views/firewall_rule_views.xml',
'views/firewall_rule_template_views.xml',
'views/network_views.xml',
'views/wifi_views.xml',
'views/unify_menu.xml',
'security/ir.model.access.csv'
'security/ir.model.access.csv',
'security/security.xml',
'views/unifi_action.xml',
'views/unifi_controller_views.xml',
'views/unifi_client_views.xml',
'views/unifi_site_views.xml',
'views/unifi_device_views.xml',
'views/unifi_network_views.xml',
'views/unifi_wifi_views.xml',
'views/unifi_firewall_group_views.xml',
'views/unifi_port_forward_views.xml',
'views/unifi_firewall_rule_template_views.xml',
'views/unifi_firewall_rule_views.xml',
'views/unifi_dashboard.xml',
'views/unifi_menu.xml',
],
'assets': {
'web.assets_backend': [
# External Libraries
'odoo_unifi_manager/static/lib/chart.js/Chart.bundle.min.js',
# Styles
'odoo_unifi_manager/static/src/scss/dashboard.scss',
# Dashboard Components
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'odoo_unifi_manager/static/src/js/dashboard_view.js',
'odoo_unifi_manager/static/src/xml/dashboard.xml',
],
},
'installable': True,
'application': True,
}

View file

@ -1,6 +1,13 @@
from . import ctrl
from . import firewall_rule
from . import firewall_rule_template
from . import network
from . import wifi
from . import unifi_client
from . import unifi_controller
from . import unifi_api_client
from . import unifi_firewall_rule_template
from . import unifi_firewall_rule
from . import unifi_firewall_group
from . import unifi_dpi_category
from . import unifi_device
from . import unifi_client_model
from . import unifi_system_health
from . import unifi_wifi
from . import unifi_network
from . import unifi_site
from . import unifi_port_forward

View file

@ -1,113 +0,0 @@
from odoo import models, fields, api
from .unifi_client import Unifi
from odoo.exceptions import ValidationError
class UnifiCtrl(models.Model):
_name = 'unifi.ctrl'
_description = 'Unifi Controller'
name = fields.Char(
string='Name',
required=True
)
ip_address = fields.Char(
string='IP Address',
required=True,
help="IP address of the Unifi Controller."
)
api_port = fields.Integer(
string='API Port',
default=8443,
help="Port used for the Unifi API."
)
username = fields.Char(
string='Username',
required=True,
help="Username for API authentication."
)
password = fields.Char(
string='Password',
required=True,
help="Password for API authentication."
)
controller_type = fields.Selection(
[
('udm', 'UDM Pro'),
('controller', 'Controller')
],
string="Controller Type",
default='udm',
required=True,
help="Select whether this is a UDM Pro or a Unifi Controller."
)
def action_test_connection(self):
"""Test connection to Unifi Controller or UDM Pro."""
self.ensure_one()
client = Unifi(
host=self.ip_address,
username=self.username,
password=self.password,
is_udm=(self.controller_type == 'udm')
)
try:
client.login()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Connection successful!',
'type': 'success',
},
}
except Exception as e:
raise ValidationError(f"Connection failed: {e}")
def action_fetch_networks_and_fill_rules(self):
"""Fetch networks and populate firewall rules."""
self.ensure_one()
client = Unifi(
host=self.ip_address,
username=self.username,
password=self.password,
is_udm=(self.controller_type == 'udm')
)
try:
client.login()
networks = client.get_networks()
firewall_rule_model = self.env['firewall.rule']
for network in networks:
rule_name = network.get('name', 'Unnamed Rule')
src_ip = network.get('subnet', None) # Example: Adjust based on API data
if not src_ip:
continue
firewall_rule_model.create({
'name': f"Network: {rule_name}",
'action': 'accept',
'enabled': True,
'src_ip': src_ip,
'dst_ip': None,
'log': False,
'controller_id': self.id,
})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Networks Fetched',
'message': f"{len(networks)} networks fetched and rules created.",
'type': 'success',
},
}
except Exception as e:
raise ValidationError(f"Failed to fetch networks: {e}")

View file

@ -1,50 +0,0 @@
from odoo import models, fields, api
class FirewallRule(models.Model):
_name = 'unifi.firewall.rule'
_description = 'Unifi Firewall Rule'
name = fields.Char(
string='Rule Name',
required=True
)
controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address (can be left empty)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address (can be left empty)"
)
protocol = fields.Selection(
selection=[
('tcp', 'TCP'),
('udp', 'UDP')
],
string='Protocol',
required=True
)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)

View file

@ -1,49 +0,0 @@
from odoo import models, fields
class FirewallRuleTemplate(models.Model):
_name = 'unifi.firewall.rule.template'
_description = 'Firewall Rule Template'
name = fields.Char(
string='Template Name',
required=True
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address (can be left empty)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address (can be left empty)"
)
protocol = fields.Selection(
selection=[
('tcp', 'TCP'),
('udp', 'UDP')
],
string='Protocol',
required=True)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)

View file

@ -1,37 +0,0 @@
from odoo import models, fields
class Network(models.Model):
_name = 'unifi.network'
_description = 'Unifi Network'
name = fields.Char(
string='Network Name',
required=True
)
cidr = fields.Char(
string='CIDR',
required=True,
help="CIDR notation for the network (e.g., 192.168.1.0/24)."
)
gateway = fields.Char(
string='Gateway',
help="Gateway IP address for the network."
)
vlan_id = fields.Integer(
string='VLAN ID',
help="VLAN ID for the network."
)
description = fields.Text(
string='Description'
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)

View file

@ -0,0 +1,619 @@
import json
import logging
import requests
import urllib3
from odoo.exceptions import ValidationError, UserError
_logger = logging.getLogger(__name__)
class UnifiApiClient:
"""Custom UniFi API Client implementation."""
def __init__(self, host: str, username: str, password: str, port: int = 8443,
is_udm: bool = False, verify_ssl: bool = False, timeout: int = 10):
"""Initialize the UniFi API client.
Args:
host: Hostname or IP address of the UniFi Controller
username: Username for API authentication
password: Password for API authentication
port: Port number for the API (default: 8443)
is_udm: Whether this is a UDM Pro device
verify_ssl: Whether to verify SSL certificates
timeout: Request timeout in seconds
"""
if not host or not username or not password:
raise ValidationError("Host, username and password are required")
self.host = host
self.port = port
self.is_udm = is_udm
self.verify_ssl = verify_ssl
self.timeout = timeout
self.site_id = 'default'
# Disable SSL warnings if verify_ssl is False
if not verify_ssl:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# Setup session
self.session = requests.Session()
self.session.verify = verify_ssl
# Base URLs
if is_udm:
self.base_url = f"https://{host}"
self.api_url = f"https://{host}/proxy/network"
self.auth_url = f"https://{host}/api/auth/login"
else:
self.base_url = f"https://{host}:{port}"
self.api_url = self.base_url
self.auth_url = f"{self.base_url}/api/login"
_logger.info(f"Initialized with base_url={self.base_url}, api_url={self.api_url}")
try:
self._login(username, password)
_logger.info("Successfully authenticated with UniFi controller")
# Only try to discover sites for non-UDM controllers
if not is_udm:
try:
sites = self.get_sites()
_logger.info(f"Available sites: {sites}")
if sites:
# If 'default' site doesn't exist, use the first available site
site_exists = any(site.get('name') == 'default' for site in sites)
if not site_exists and sites:
self.site_id = sites[0].get('name')
_logger.info(f"Using site: {self.site_id}")
except Exception as e:
_logger.warning(f"Failed to get sites, using default site: {str(e)}")
except Exception as e:
_logger.error(f"Failed to initialize UniFi controller: {str(e)}")
raise ValidationError(f"Failed to initialize UniFi controller: {str(e)}")
def _login(self, username: str, password: str):
"""Authenticate with the UniFi Controller."""
_logger.info(f"Attempting login to {self.auth_url}")
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
# Add CSRF token for UDM Pro if available
if self.is_udm and hasattr(self, 'csrf_token'):
headers['X-CSRF-Token'] = self.csrf_token
data = {
'username': username,
'password': password,
'remember': True
}
try:
_logger.info(f"Making login request to {self.auth_url}")
response = self.session.post(
self.auth_url,
headers=headers,
json=data,
timeout=self.timeout
)
_logger.info(f"Login response status: {response.status_code}")
_logger.info(f"Login response headers: {response.headers}")
_logger.info(f"Login response content: {response.text}")
if not response.ok:
_logger.error(f"Login failed with status {response.status_code}")
_logger.error(f"Response content: {response.text}")
response.raise_for_status()
# Store cookies and CSRF token for subsequent requests
self.session.cookies.update(response.cookies)
if self.is_udm:
self.csrf_token = response.headers.get('X-CSRF-Token')
_logger.info(f"Stored CSRF token: {self.csrf_token}")
_logger.info("Successfully logged in and stored session cookies")
except requests.exceptions.RequestException as e:
_logger.error(f"Login request failed: {str(e)}")
if hasattr(e, 'response') and e.response is not None:
_logger.error(f"Error response content: {e.response.text}")
raise ValidationError(f"Login failed: {str(e)}")
def _api_request(self, method: str, endpoint: str, data: dict = None) -> dict:
"""Make an API request to the UniFi Controller."""
if self.is_udm:
# For UDM Pro, check if endpoint already has proxy/network prefix
if endpoint.startswith('proxy/network/'):
url = f"{self.base_url}/{endpoint}"
else:
# If no prefix, use api_url which includes the proxy/network prefix
url = f"{self.api_url}/{endpoint.lstrip('/')}"
else:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
_logger.info(f"Making {method} request to {url}")
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
if self.is_udm and hasattr(self, 'csrf_token'):
headers['X-CSRF-Token'] = self.csrf_token
try:
if data and method in ['POST', 'PUT', 'PATCH']:
response = self.session.request(
method,
url,
headers=headers,
json=data, # Use json parameter for requests that send data
timeout=self.timeout
)
else:
response = self.session.request(
method,
url,
headers=headers,
timeout=self.timeout
)
_logger.info(f"Response status: {response.status_code}")
_logger.info(f"Response headers: {response.headers}")
_logger.info(f"Response content: {response.text}")
response.raise_for_status()
if response.text:
return response.json()
return None
except requests.exceptions.RequestException as e:
_logger.error(f"API request failed with status {e.response.status_code if hasattr(e, 'response') and e.response else 'unknown'}")
if hasattr(e, 'response') and e.response is not None:
_logger.error(f"Error response content: {e.response.text}")
raise
def get_sites(self) -> list:
"""Get list of available sites."""
try:
if self.is_udm:
return [{'name': 'default'}]
response = self._api_request('GET', 'api/self/sites')
return response.get('data', [])
except Exception as e:
_logger.error(f"Failed to get sites: {str(e)}")
return []
def get_firewall_rules(self, site_id=None):
"""Get all firewall rules for a site."""
site = site_id or self.site_id
if self.is_udm:
# For UDM Pro, use the network proxy endpoint
return self._api_request('GET', f'proxy/network/api/s/{site}/firewallrule')
return self._api_request('GET', f'api/s/{site}/rest/firewallrule')
def get_active_firewall_rules(self, site_id=None):
"""Get active firewall rules for a site."""
site = site_id or self.site_id
return self._api_request('GET', f'api/s/{site}/stat/firewallrule')
def get_firewall_groups(self, site_id=None):
"""Get firewall groups for a site."""
site = site_id or self.site_id
return self._api_request('GET', f'api/s/{site}/rest/firewallgroup')
def update_firewall_rule(self, rule_id: str, data: dict, site_id=None):
"""Update a firewall rule."""
site = site_id or self.site_id
return self._api_request('PUT', f'api/s/{site}/rest/firewallrule/{rule_id}', data)
def toggle_firewall_rule(self, rule_id: str, enabled: bool, site_id=None):
"""Enable or disable a firewall rule."""
return self.update_firewall_rule(rule_id, {'enabled': enabled}, site_id)
def get_firewall_rules(self) -> list:
"""Get firewall rules from the controller."""
try:
if self.is_udm:
# Try UDM Pro Network Settings endpoint first
try:
_logger.info("Trying network settings endpoint")
response = self._api_request('GET', 'proxy/network/api/s/default/rest/setting/firewall')
_logger.info(f"Network settings response: {response}")
if response and isinstance(response, dict):
firewall_settings = response.get('data', [{}])[0]
if firewall_settings:
_logger.info(f"Found firewall settings: {firewall_settings}")
rules = []
# Get WAN rules
wan_in = firewall_settings.get('wan_in', [])
for rule in wan_in:
rule['ruleset'] = 'WAN_IN'
rules.append(rule)
wan_out = firewall_settings.get('wan_out', [])
for rule in wan_out:
rule['ruleset'] = 'WAN_OUT'
rules.append(rule)
wan_local = firewall_settings.get('wan_local', [])
for rule in wan_local:
rule['ruleset'] = 'WAN_LOCAL'
rules.append(rule)
# Get LAN rules
lan_in = firewall_settings.get('lan_in', [])
for rule in lan_in:
rule['ruleset'] = 'LAN_IN'
rules.append(rule)
lan_out = firewall_settings.get('lan_out', [])
for rule in lan_out:
rule['ruleset'] = 'LAN_OUT'
rules.append(rule)
lan_local = firewall_settings.get('lan_local', [])
for rule in lan_local:
rule['ruleset'] = 'LAN_LOCAL'
rules.append(rule)
_logger.info(f"Found {len(rules)} firewall rules")
return rules
except Exception as e:
_logger.warning(f"Failed to get rules from network settings: {str(e)}")
# Try site-specific endpoint for non-UDM or as fallback
endpoint = f'api/s/{self.site_id}/rest/setting/firewall'
_logger.info(f"Trying {endpoint} endpoint")
response = self._api_request('GET', endpoint)
_logger.info(f"Response from {endpoint}: {response}")
if response and isinstance(response, dict):
firewall_settings = response.get('data', [{}])[0]
if firewall_settings:
rules = []
# Get WAN rules
wan_in = firewall_settings.get('wan_in', [])
for rule in wan_in:
rule['ruleset'] = 'WAN_IN'
rules.append(rule)
wan_out = firewall_settings.get('wan_out', [])
for rule in wan_out:
rule['ruleset'] = 'WAN_OUT'
rules.append(rule)
wan_local = firewall_settings.get('wan_local', [])
for rule in wan_local:
rule['ruleset'] = 'WAN_LOCAL'
rules.append(rule)
# Get LAN rules
lan_in = firewall_settings.get('lan_in', [])
for rule in lan_in:
rule['ruleset'] = 'LAN_IN'
rules.append(rule)
lan_out = firewall_settings.get('lan_out', [])
for rule in lan_out:
rule['ruleset'] = 'LAN_OUT'
rules.append(rule)
lan_local = firewall_settings.get('lan_local', [])
for rule in lan_local:
rule['ruleset'] = 'LAN_LOCAL'
rules.append(rule)
_logger.info(f"Found {len(rules)} firewall rules")
return rules
_logger.warning("Could not retrieve firewall rules from any known endpoint")
return []
except Exception as e:
_logger.error(f"Failed to retrieve firewall rules: {str(e)}")
raise UserError(f"Failed to retrieve firewall rules: {str(e)}")
def create_firewall_group(self, data: dict, site_id=None) -> dict:
"""Create a firewall group.
Args:
data: Dictionary containing the firewall group data
site_id: Optional site ID. If not provided, uses the default site
Returns:
dict: Created firewall group data
"""
try:
_logger.info(f"Creating firewall group: {data}")
site = site_id or self.site_id
if self.is_udm:
endpoint = f'proxy/network/api/s/{site}/rest/firewallgroup'
else:
endpoint = f'api/s/{site}/rest/firewallgroup'
return self._api_request('POST', endpoint, data=data)
except Exception as e:
_logger.error(f"Failed to create firewall group: {str(e)}")
raise UserError(f"Failed to create firewall group: {str(e)}")
def ensure_default_firewall_groups(self, site_id=None):
"""Ensure default firewall groups exist.
Creates default LAN and WAN groups if they don't exist.
"""
try:
_logger.info("Checking for default firewall groups")
groups = self.get_firewall_groups(site_id)
if not groups:
_logger.info("No firewall groups found, creating defaults")
defaults = [
{
'name': 'Default LAN',
'group_type': 'address-group',
'group_members': ['192.168.0.0/16', '172.16.0.0/12', '10.0.0.0/8'],
'group_description': 'Default LAN networks'
},
{
'name': 'Default WAN',
'group_type': 'address-group',
'group_members': ['0.0.0.0/0'],
'group_description': 'Default WAN networks'
}
]
for group in defaults:
try:
_logger.info(f"Creating firewall group: {group}")
self.create_firewall_group(group, site_id)
except Exception as e:
_logger.warning(f"Failed to create firewall group: {str(e)}")
except Exception as e:
_logger.error(f"Failed to ensure default firewall groups: {str(e)}")
def list_devices(self) -> list:
"""Get a list of all devices from the UniFi Controller.
Returns:
list: List of device dictionaries containing device information
"""
try:
_logger.info("Attempting to get all devices")
devices = []
# Try primary endpoint first
try:
if self.is_udm:
endpoint = 'proxy/network/api/s/default/stat/device' # UDM Pro endpoint
else:
endpoint = f'api/s/{self.site_id}/stat/device'
_logger.info(f"Getting devices from endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
devices = response.get('data', [])
else:
devices = response
_logger.info(f"Found {len(devices)} devices")
except Exception as e:
_logger.warning(f"Failed to get devices using primary endpoint: {str(e)}")
# Try fallback endpoint for UDM Pro
try:
if self.is_udm:
endpoint = 'proxy/network/v2/api/site/default/devices' # New UDM Pro endpoint
else:
endpoint = f'api/s/{self.site_id}/stat/device/basic'
_logger.info(f"Getting devices from endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
devices = response.get('data', [])
else:
devices = response
_logger.info(f"Found {len(devices)} devices using fallback endpoint")
except Exception as e:
_logger.warning(f"Failed to get devices using fallback endpoint: {str(e)}")
_logger.info(f"Total devices retrieved: {len(devices)}")
if devices:
_logger.info("Sample device data:")
_logger.info(str(devices[0]))
return devices
except Exception as e:
_logger.error(f"Failed to get devices: {str(e)}")
raise UserError(f"Failed to get devices: {str(e)}")
def list_clients(self) -> list:
"""Get a list of all clients from the UniFi Controller.
Returns:
list: List of client dictionaries containing client information
"""
try:
_logger.info("Attempting to get all clients")
clients = []
# Try to get all clients at once
try:
if self.is_udm:
endpoint = 'proxy/network/api/s/default/stat/sta'
else:
endpoint = f'api/s/{self.site_id}/stat/sta'
_logger.info(f"Attempting to get all clients using endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
clients = response.get('data', [])
else:
clients = response
_logger.info(f"Retrieved {len(clients)} clients")
except Exception as e:
_logger.warning(f"Failed to get clients using primary endpoint: {str(e)}")
# Fallback to alternative endpoint
try:
if self.is_udm:
endpoint = 'proxy/network/v2/api/site/default/clients'
else:
endpoint = f'api/s/{self.site_id}/stat/alluser'
_logger.info(f"Trying to get clients from endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
clients = response.get('data', [])
else:
clients = response
_logger.info(f"Found {len(clients)} clients using fallback endpoint")
except Exception as e:
_logger.warning(f"Failed to get clients using fallback endpoint: {str(e)}")
_logger.info(f"Total clients retrieved: {len(clients)}")
if clients:
_logger.info("Sample client data:")
_logger.info(str(clients[0]))
return clients
except Exception as e:
_logger.error(f"Failed to get clients: {str(e)}")
raise UserError(f"Failed to get clients: {str(e)}")
def list_networks(self) -> list:
"""Get a list of all networks from the UniFi Controller.
Returns:
list: List of network dictionaries containing network information
"""
try:
_logger.info("Attempting to get all networks")
if self.is_udm:
endpoint = 'proxy/network/api/s/default/rest/networkconf'
else:
endpoint = f'api/s/{self.site_id}/rest/networkconf'
_logger.info(f"Attempting to get networks using endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
networks = response.get('data', [])
else:
networks = response
_logger.info(f"Retrieved {len(networks)} networks")
if networks:
_logger.info("Sample network data:")
_logger.info(str(networks[0]))
return networks
except Exception as e:
_logger.error(f"Failed to get networks: {str(e)}")
raise UserError(f"Failed to get networks: {str(e)}")
def list_sites(self) -> list:
"""Get a list of all sites from the UniFi Controller.
Returns:
list: List of site dictionaries containing site information
"""
try:
_logger.info("Attempting to get all sites")
if self.is_udm:
# UDM Pro typically has only one site
return [{
'name': 'default',
'desc': 'Default',
'_id': 'default',
'role': 'admin'
}]
endpoint = 'api/self/sites'
_logger.info(f"Attempting to get sites using endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
sites = response.get('data', [])
else:
sites = response
_logger.info(f"Retrieved {len(sites)} sites")
if sites:
_logger.info("Sample site data:")
_logger.info(str(sites[0]))
return sites
except Exception as e:
_logger.error(f"Failed to get sites: {str(e)}")
raise UserError(f"Failed to get sites: {str(e)}")
def get_wifis(self, site_id=None) -> list:
"""Get a list of all WiFi networks from the UniFi Controller.
Args:
site_id: Optional site ID. If not provided, uses the default site.
Returns:
list: List of WiFi network configurations
"""
try:
_logger.info("Attempting to get WiFi networks")
site = site_id or self.site_id
# Try primary endpoint first
try:
if self.is_udm:
endpoint = f'proxy/network/api/s/{site}/rest/wlanconf'
else:
endpoint = f'api/s/{site}/rest/wlanconf'
_logger.info(f"Getting WiFi networks from endpoint: {endpoint}")
response = self._api_request('GET', endpoint)
if isinstance(response, dict):
networks = response.get('data', [])
else:
networks = response or []
_logger.info(f"Found {len(networks)} WiFi networks")
if networks:
_logger.info("Sample WiFi network data:")
_logger.info(str(networks[0]))
return networks
except Exception as e:
_logger.error(f"Failed to get WiFi networks: {str(e)}")
raise
except Exception as e:
_logger.error(f"Failed to get WiFi networks: {str(e)}")
raise UserError(f"Failed to get WiFi networks: {str(e)}")
def get_firewall_rules(self, site_id=None):
"""Get all firewall rules for a site."""
site = site_id or self.site_id
if self.is_udm:
# For UDM Pro, use the network proxy endpoint
return self._api_request('GET', f'proxy/network/api/s/{site}/firewallrule')
return self._api_request('GET', f'api/s/{site}/rest/firewallrule')
def get_port_forward_rules(self, site_id=None):
"""Get all port forwarding rules for a site."""
site = site_id or self.site_id
_logger.info("Trying port forward rules endpoint")
if self.is_udm:
# For UDM Pro, use the network proxy endpoint
return self._api_request('GET', f'proxy/network/api/s/{site}/rest/portforward')
return self._api_request('GET', f'api/s/{site}/rest/portforward')

View file

@ -1,110 +0,0 @@
import requests
import urllib3
from odoo.exceptions import ValidationError
# API Endpoints
UNIFI_LOGIN_PATH = '/api/auth/login'
UNIFI_SITES_PATH = '/api/self/sites'
UNIFI_FIREWALL_RULES_PATH = '/api/s/default/rest/firewallrule'
UNIFI_NETWORKS_PATH = '/api/s/default/rest/network'
UNIFI_WIFIS_PATH = '/api/s/default/rest/wlanconf'
# Prefix for UDM Pro endpoints
UDM_PROXY_PREFIX = '/proxy/network'
# Disable SSL verification warnings
urllib3.disable_warnings()
class Unifi:
def __init__(self, host, username, password, is_udm=False):
"""Initialize Unifi client."""
self.host = host
self.username = username
self.password = password
self.is_udm = is_udm
self.session = requests.Session()
self.csrf = ""
def _get_url(self, path):
"""Construct the correct URL based on the type of controller."""
prefix = UDM_PROXY_PREFIX if self.is_udm else ""
return f"https://{self.host}{prefix}{path}"
def login(self):
"""Authenticate with the Unifi Controller or UDM Pro."""
payload = {
"username": self.username,
"password": self.password,
}
try:
r = self.request(UNIFI_LOGIN_PATH, payload)
if r.ok:
return True
else:
raise ValidationError(f"Login failed: {r.text}")
except requests.RequestException as e:
raise ValidationError(f"Failed to connect to Unifi Controller: {e}")
def request(self, path, data=None, method='POST'):
"""Send a request to the Unifi API."""
if data is None:
data = {}
uri = self._get_url(path)
headers = {
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8",
}
if self.csrf:
headers["X-CSRF-Token"] = self.csrf
try:
r = getattr(self.session, method.lower())(uri, json=data, verify=False, headers=headers)
if 'X-CSRF-Token' in r.headers:
self.csrf = r.headers['X-CSRF-Token']
return r
except requests.RequestException as e:
raise ValidationError(f"Request to Unifi Controller failed: {e}")
def get_sites(self):
"""Retrieve available sites."""
try:
r = self.request(UNIFI_SITES_PATH, method='GET')
if r.ok:
return r.json().get('data', [])
else:
raise ValidationError(f"Failed to retrieve sites: {r.text}")
except requests.RequestException as e:
raise ValidationError(f"Failed to retrieve sites: {e}")
def get_firewall_rules(self):
"""Retrieve all firewall rules."""
try:
r = self.request(UNIFI_FIREWALL_RULES_PATH, method='GET')
if r.ok:
return r.json().get('data', [])
else:
raise ValidationError(f"Failed to retrieve firewall rules: {r.text}")
except requests.RequestException as e:
raise ValidationError(f"Request to fetch firewall rules failed: {e}")
def get_networks(self):
"""Retrieve all networks."""
try:
r = self.request(UNIFI_NETWORKS_PATH, method='GET')
if r.ok:
return r.json().get('data', [])
else:
raise ValidationError(f"Failed to retrieve networks: {r.text}")
except requests.RequestException as e:
raise ValidationError(f"Request to fetch networks failed: {e}")
def get_wifis(self):
"""Retrieve all WiFi configurations."""
try:
r = self.request(UNIFI_WIFIS_PATH, method='GET')
if r.ok:
return r.json().get('data', [])
else:
raise ValidationError(f"Failed to retrieve WiFi configurations: {r.text}")
except requests.RequestException as e:
raise ValidationError(f"Request to fetch WiFi configurations failed: {e}")

View file

@ -0,0 +1,313 @@
"""UniFi Client Device Model for Odoo."""
from odoo import models, fields, api
from odoo.exceptions import UserError
from datetime import datetime
class UnifiClient(models.Model):
"""UniFi Client Device Model."""
_name = 'unifi.client'
_description = 'UniFi Client Device'
_rec_name = 'display_name'
name = fields.Char(
string='Name',
required=True,
help="Client device name or hostname"
)
display_name = fields.Char(
string='Display Name',
compute='_compute_display_name',
store=True,
help="Human-readable device name"
)
mac = fields.Char(
string='MAC Address',
required=True,
help="MAC address of the client device"
)
last_ip = fields.Char(
string='IP Address',
help="Current IP address of the client device"
)
hostname = fields.Char(
string='Hostname',
help="Device hostname"
)
oui = fields.Char(
string='Manufacturer',
help="Device manufacturer based on OUI"
)
device_name = fields.Char(
string='Device Model',
help="Device model name"
)
controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="UniFi controller managing this client"
)
is_wired = fields.Boolean(
string='Wired Connection',
default=False,
help="Whether this client is connected via ethernet"
)
is_guest = fields.Boolean(
string='Guest Network',
default=False,
help="Whether this client is on a guest network"
)
network = fields.Char(
string='Network',
help="Network name the client is connected to"
)
essid = fields.Char(
string='SSID',
help="Wireless network SSID"
)
last_seen = fields.Datetime(
string='Last Seen',
help="Last time this client was seen on the network"
)
uptime = fields.Integer(
string='Uptime',
help="Client uptime in seconds"
)
tx_bytes = fields.Float(
string='TX Bytes',
help="Total bytes transmitted"
)
rx_bytes = fields.Float(
string='RX Bytes',
help="Total bytes received"
)
satisfaction = fields.Integer(
string='Satisfaction',
help="Client satisfaction score (0-100)"
)
signal = fields.Integer(
string='Signal Strength',
help="Signal strength in dBm"
)
noise = fields.Integer(
string='Noise Floor',
help="Noise floor in dBm"
)
tx_rate = fields.Integer(
string='TX Rate',
help="Transmit rate in Kbps"
)
rx_rate = fields.Integer(
string='RX Rate',
help="Receive rate in Kbps"
)
# Device categorization fields
dev_cat = fields.Integer(
string='Device Category',
help="UniFi device category identifier"
)
dev_family = fields.Integer(
string='Device Family',
help="UniFi device family identifier"
)
dev_vendor = fields.Integer(
string='Device Vendor',
help="UniFi device vendor identifier"
)
os_name = fields.Integer(
string='Operating System',
help="UniFi operating system identifier"
)
# Wireless connection details
channel = fields.Integer(
string='Channel',
help="WiFi channel number"
)
radio = fields.Char(
string='Radio',
help="Radio type (ng/na)"
)
radio_proto = fields.Char(
string='Radio Protocol',
help="Radio protocol (g/ac/ax)"
)
channel_width = fields.Integer(
string='Channel Width',
help="Channel width in MHz"
)
bssid = fields.Char(
string='BSSID',
help="Connected access point BSSID"
)
ap_mac = fields.Char(
string='AP MAC',
help="Connected access point MAC address"
)
# Performance metrics
tx_retries = fields.Integer(
string='TX Retries',
help="Number of transmission retries"
)
wifi_tx_attempts = fields.Integer(
string='TX Attempts',
help="Total transmission attempts"
)
wifi_tx_dropped = fields.Integer(
string='TX Dropped',
help="Number of dropped transmissions"
)
tx_retries_percentage = fields.Float(
string='TX Retry Rate',
help="Transmission retry rate percentage"
)
bytes_r = fields.Float(
string='Transfer Rate',
help="Current transfer rate in bytes/sec"
)
site_id = fields.Char(
string='Site ID',
help="UniFi site identifier"
)
device_id = fields.Integer(
string='Device ID',
help="UniFi device identifier"
)
network_id = fields.Char(
string='Network ID',
help="UniFi network identifier"
)
status = fields.Selection(
[
('online', 'Online'),
('offline', 'Offline')
],
string='Status',
default='offline',
help="Client connection status"
)
_sql_constraints = [
('unique_mac_controller', 'unique(mac,controller_id)',
'A client with this MAC address already exists for this controller!')
]
@api.depends('name', 'hostname', 'device_name', 'mac')
def _compute_display_name(self):
"""Compute a human-readable display name."""
for client in self:
if client.device_name and client.hostname:
client.display_name = f"{client.hostname} ({client.device_name})"
elif client.hostname:
client.display_name = client.hostname
elif client.device_name:
client.display_name = client.device_name
elif client.name:
client.display_name = client.name
else:
client.display_name = client.mac
@api.model
def sync_from_controller(self, client, controller_id):
"""Synchronize clients from UniFi controller."""
try:
# Get all clients from the controller
clients_data = client.list_clients()
for client_data in clients_data:
vals = {
'name': client_data.get('hostname') or client_data.get('device_name') or client_data.get('mac'),
'mac': client_data.get('mac'),
'last_ip': client_data.get('last_ip') or client_data.get('ip'),
'hostname': client_data.get('hostname'),
'oui': client_data.get('oui'),
'device_name': client_data.get('device_name'),
'controller_id': controller_id,
'is_wired': bool(client_data.get('is_wired')),
'is_guest': bool(client_data.get('is_guest')),
'network': client_data.get('network'),
'essid': client_data.get('essid'),
'last_seen': datetime.fromtimestamp(client_data.get('last_seen', 0)),
'uptime': client_data.get('uptime', 0),
'tx_bytes': float(client_data.get('tx_bytes', 0)),
'rx_bytes': float(client_data.get('rx_bytes', 0)),
'satisfaction': client_data.get('satisfaction', 0),
'signal': client_data.get('signal', 0),
'noise': client_data.get('noise', 0),
'tx_rate': client_data.get('tx_rate', 0),
'rx_rate': client_data.get('rx_rate', 0),
# Device categorization
'dev_cat': client_data.get('dev_cat'),
'dev_family': client_data.get('dev_family'),
'dev_vendor': client_data.get('dev_vendor'),
'os_name': client_data.get('os_name'),
# Wireless details
'channel': client_data.get('channel'),
'radio': client_data.get('radio'),
'radio_proto': client_data.get('radio_proto'),
'channel_width': client_data.get('channel_width'),
'bssid': client_data.get('bssid'),
'ap_mac': client_data.get('ap_mac'),
# Performance metrics
'tx_retries': client_data.get('tx_retries', 0),
'wifi_tx_attempts': client_data.get('wifi_tx_attempts', 0),
'wifi_tx_dropped': client_data.get('wifi_tx_dropped', 0),
'tx_retries_percentage': client_data.get('tx_retries_percentage', 0.0),
'bytes_r': client_data.get('bytes-r', 0.0),
'site_id': client_data.get('site_id'),
'device_id': client_data.get('dev_id'),
'network_id': client_data.get('network_id'),
'status': 'online' if client_data.get('last_seen') else 'offline',
}
# Try to find existing client
existing = self.search([
('mac', '=', vals['mac']),
('controller_id', '=', vals['controller_id'])
])
if existing:
existing.write(vals)
else:
self.create(vals)
except Exception as e:
raise UserError(f"Failed to sync clients: {str(e)}")

View file

@ -0,0 +1,966 @@
"""UniFi Controller Model."""
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, UserError
from .unifi_api_client import UnifiApiClient
import logging
from datetime import timedelta
import json
_logger = logging.getLogger(__name__)
class UnifiController(models.Model):
_name = 'unifi.ctrl' # Gardé le même nom pour la compatibilité
_description = 'UniFi Controller Configuration'
name = fields.Char(
string='Name',
required=True
)
ip_address = fields.Char(
string='IP Address',
required=True,
help="IP address of the UniFi Controller."
)
api_port = fields.Integer(
string='API Port',
default=8443,
help="Port used for the UniFi API."
)
username = fields.Char(
string='Username',
required=True,
help="Username for API authentication."
)
password = fields.Char(
string='Password',
required=True,
help="Password for API authentication."
)
controller_type = fields.Selection(
[
('udm', 'UDM Pro'),
('controller', 'Controller')
],
string="Controller Type",
default='udm',
required=True,
help="Select whether this is a UDM Pro or a UniFi Controller."
)
verify_ssl = fields.Boolean(
string='Verify SSL',
default=False,
help="Enable SSL certificate verification."
)
last_sync = fields.Datetime(
string='Last Synchronization',
readonly=True
)
sync_interval = fields.Integer(
string='Sync Interval (minutes)',
default=60,
help="How often to synchronize with the UniFi controller"
)
# Dashboard Fields
total_clients = fields.Integer(
string='Total Clients',
compute='_compute_dashboard_data'
)
active_clients = fields.Integer(
string='Active Clients',
compute='_compute_dashboard_data'
)
total_devices = fields.Integer(
string='Total Devices',
compute='_compute_dashboard_data'
)
offline_devices = fields.Integer(
string='Offline Devices',
compute='_compute_dashboard_data'
)
total_bandwidth = fields.Float(
string='Total Bandwidth (Mbps)',
compute='_compute_dashboard_data'
)
bandwidth_usage = fields.Float(
string='Bandwidth Usage (%)',
compute='_compute_dashboard_data'
)
active_alerts = fields.Integer(
string='Active Alerts',
compute='_compute_dashboard_data'
)
critical_alerts = fields.Integer(
string='Critical Alerts',
compute='_compute_dashboard_data'
)
client_type_chart = fields.Json(
string='Client Type Chart Data',
compute='_compute_dashboard_data'
)
bandwidth_chart = fields.Json(
string='Bandwidth Chart Data',
compute='_compute_dashboard_data'
)
active_firewall_rules = fields.Integer(
string='Active Firewall Rules',
compute='_compute_dashboard_data'
)
blocked_connections = fields.Integer(
string='Blocked Connections',
compute='_compute_dashboard_data'
)
vpn_status = fields.Selection([
('up', 'Up'),
('down', 'Down'),
('partial', 'Partial')
], string='VPN Status',
compute='_compute_dashboard_data'
)
threat_count = fields.Integer(
string='Threat Count',
compute='_compute_dashboard_data'
)
@api.model
def get_dashboard_data(self):
"""Get dashboard data for the first controller."""
controller = self.search([], limit=1)
if not controller:
return {
'total_clients': 0,
'active_clients': 0,
'total_devices': 0,
'offline_devices': 0,
'total_bandwidth': 0,
'bandwidth_usage': 0,
'active_alerts': 0,
'critical_alerts': 0,
'client_type_chart': self._get_empty_client_chart(),
'bandwidth_chart': self._get_empty_bandwidth_chart(),
'active_firewall_rules': 0,
'blocked_connections': 0,
'vpn_status': 'No VPN',
'threat_count': 0
}
try:
with controller._get_unifi_client() as client:
# Get clients data
clients = client.list_clients() or []
active_clients = [c for c in clients if c.get('is_wired') or c.get('is_wireless')]
# Get devices data
devices = client.list_devices() or []
offline_devices = [d for d in devices if not d.get('state', 1)]
# Get alerts
alerts = client.list_alerts() or []
critical_alerts = [a for a in alerts if a.get('severity', 'info') == 'critical']
# Get firewall rules
firewall_rules = client.list_firewall_rules() or []
active_rules = [r for r in firewall_rules if r.get('enabled', False)]
# Prepare client type data for chart
client_types = {
'Wired': len([c for c in clients if c.get('is_wired')]),
'Wireless': len([c for c in clients if c.get('is_wireless')]),
'Guest': len([c for c in clients if c.get('is_guest')])
}
# Prepare bandwidth data (example data)
bandwidth_data = {
'labels': ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
'datasets': [
{
'label': 'Download',
'data': [30, 45, 60, 75, 90, 100],
'borderColor': '#00ff00',
'fill': False
},
{
'label': 'Upload',
'data': [20, 35, 45, 55, 65, 75],
'borderColor': '#0000ff',
'fill': False
}
]
}
return {
'total_clients': len(clients),
'active_clients': len(active_clients),
'total_devices': len(devices),
'offline_devices': len(offline_devices),
'total_bandwidth': sum(d.get('tx_bytes', 0) + d.get('rx_bytes', 0) for d in devices),
'bandwidth_usage': sum(d.get('tx_rate', 0) + d.get('rx_rate', 0) for d in devices) / 1000000, # Convert to Mbps
'active_alerts': len(alerts),
'critical_alerts': len(critical_alerts),
'client_type_chart': {
'labels': list(client_types.keys()),
'datasets': [{
'data': list(client_types.values()),
'backgroundColor': ['#FF6384', '#36A2EB', '#FFCE56']
}]
},
'bandwidth_chart': bandwidth_data,
'active_firewall_rules': len(active_rules),
'blocked_connections': sum(r.get('counters', {}).get('blocked', 0) for r in firewall_rules),
'vpn_status': 'Active' if any(d.get('vpn', False) for d in devices) else 'Inactive',
'threat_count': sum(1 for a in alerts if a.get('category') == 'security')
}
except Exception as e:
_logger.error("Error fetching dashboard data: %s", str(e))
return self._get_empty_dashboard_data()
def _get_empty_client_chart(self):
"""Return empty client chart data structure."""
return {
'labels': ['Wired', 'Wireless', 'Guest'],
'datasets': [{
'data': [0, 0, 0],
'backgroundColor': ['#FF6384', '#36A2EB', '#FFCE56']
}]
}
def _get_empty_bandwidth_chart(self):
"""Return empty bandwidth chart data structure."""
return {
'labels': ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
'datasets': [
{
'label': 'Download',
'data': [0, 0, 0, 0, 0, 0],
'borderColor': '#00ff00',
'fill': False
},
{
'label': 'Upload',
'data': [0, 0, 0, 0, 0, 0],
'borderColor': '#0000ff',
'fill': False
}
]
}
def _get_empty_dashboard_data(self):
"""Return empty dashboard data structure."""
return {
'total_clients': 0,
'active_clients': 0,
'total_devices': 0,
'offline_devices': 0,
'total_bandwidth': 0,
'bandwidth_usage': 0,
'active_alerts': 0,
'critical_alerts': 0,
'client_type_chart': self._get_empty_client_chart(),
'bandwidth_chart': self._get_empty_bandwidth_chart(),
'active_firewall_rules': 0,
'blocked_connections': 0,
'vpn_status': 'No Connection',
'threat_count': 0
}
def get_client(self):
"""Get an authenticated UniFi client instance"""
_logger.info(f"Creating UniFi client for controller {self.name}")
_logger.info(f"IP Address: {self.ip_address}, Port: {self.api_port}")
_logger.info(f"Controller Type: {self.controller_type}")
return UnifiApiClient(
host=self.ip_address,
port=self.api_port,
username=self.username,
password=self.password,
is_udm=(self.controller_type == 'udm'),
verify_ssl=self.verify_ssl,
timeout=30 # Increase timeout to 30 seconds
)
def action_test_connection(self):
"""Test connection to UniFi Controller or UDM Pro."""
self.ensure_one()
try:
client = self.get_client()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Connection successful!',
'type': 'success',
},
}
except Exception as e:
raise ValidationError(f"Connection failed: {e}")
def sync_all(self):
"""Synchronize all UniFi data"""
self.ensure_one()
try:
client = self.get_client()
# Synchronize different types of data
self.env['unifi.wifi'].sync_from_controller(client, self.id)
self.env['unifi.network'].sync_from_controller(client, self.id)
self.env['unifi.client'].sync_from_controller(client, self.id)
self.env['unifi.system.health'].sync_from_controller(client, self.id)
# Add other models sync here
self.last_sync = fields.Datetime.now()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Synchronization completed successfully!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Synchronization failed: {str(e)}")
def action_fetch_networks_and_fill_rules(self):
"""Fetch networks and populate firewall rules."""
self.ensure_one()
client = self.get_client()
try:
networks = client.get_networks()
firewall_rule_model = self.env['unifi.firewall.rule']
for network in networks:
rule_name = network.get('name')
src_ip = network.get('subnet')
if not src_ip:
continue
firewall_rule_model.create({
'name': f"Network: {rule_name}",
'action': 'accept',
'enabled': True,
'src_ip': src_ip,
'dst_ip': False,
'log': False,
'controller_id': self.id,
})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Network rules created successfully!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Failed to create network rules: {str(e)}")
def action_sync_all(self):
"""Synchronize all objects from the UniFi Controller."""
self.ensure_one()
try:
self.action_sync_sites()
self.action_sync_devices()
self.action_sync_clients()
self.action_sync_networks()
self.action_sync_firewall_rules()
self.last_sync = fields.Datetime.now()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'All objects synchronized successfully!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Synchronization failed: {e}")
def action_sync_devices(self):
"""Synchronize devices from the UniFi Controller."""
self.ensure_one()
try:
_logger.info(f"Starting device sync for controller {self.name} ({self.ip_address})")
_logger.info(f"Controller type: {self.controller_type}, Port: {self.api_port}")
client = self.get_client()
_logger.info("Successfully created API client")
devices = client.list_devices()
_logger.info(f"Retrieved {len(devices)} devices from controller")
# Create or update device records
for device in devices:
mac = device.get('mac')
if not mac:
_logger.warning("Found device without MAC address, skipping")
continue
# Determine device type
model = device.get('model', '')
_logger.info(f"Processing device: MAC={mac}, Model={model}")
# Prepare device values
vals = {
'name': device.get('name') or device.get('model', 'Unknown Device'),
'mac': mac,
'model': model,
'type': 'uap' if model.startswith('U6') or model.startswith('UAP') else 'ugw' if model.startswith('UGW') or model.startswith('UXG') else 'usw' if model.startswith('USW') else 'udm' if model.startswith('UDM') else 'other',
'ip_address': device.get('ip'),
'version': device.get('version'),
'status': 'online' if device.get('state', 1) == 1 else 'offline',
'last_seen': fields.Datetime.now(), # Current time as devices are currently connected
'controller_id': self.id,
'site_id': device.get('site_id')
}
# Find existing device or create new one
existing_device = self.env['unifi.device'].search([
('mac', '=', mac),
('controller_id', '=', self.id)
])
if existing_device:
existing_device.write(vals)
_logger.info(f"Updated device: {vals['name']} ({mac})")
else:
self.env['unifi.device'].create(vals)
_logger.info(f"Created new device: {vals['name']} ({mac})")
# Mark devices not in the response as offline
all_devices = self.env['unifi.device'].search([('controller_id', '=', self.id)])
current_macs = [d.get('mac') for d in devices if d.get('mac')]
offline_devices = all_devices.filtered(lambda d: d.mac not in current_macs)
if offline_devices:
offline_devices.write({'status': 'offline'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': f'Successfully synchronized {len(devices)} devices!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Device synchronization failed: {e}")
def action_sync_clients(self):
"""Synchronize clients from the UniFi Controller."""
self.ensure_one()
try:
client = self.get_client()
clients = client.list_clients()
# Create or update client records
for client_data in clients:
mac = client_data.get('mac')
if not mac:
continue
# Prepare client values
vals = {
'name': client_data.get('name', client_data.get('hostname', 'Unknown Client')),
'mac': mac,
'last_ip': client_data.get('ip') or client_data.get('last_ip'),
'hostname': client_data.get('hostname'),
'last_seen': fields.Datetime.now(), # Current time as clients are currently connected
'controller_id': self.id,
'site_id': client_data.get('site_id'),
'device_id': client_data.get('dev_id'),
'network_id': client_data.get('network_id'),
'status': 'online',
'is_wired': client_data.get('is_wired', False)
}
# Find existing client or create new one
existing_client = self.env['unifi.client'].search([
('mac', '=', mac),
('controller_id', '=', self.id)
])
if existing_client:
existing_client.write(vals)
_logger.info(f"Updated client: {vals['name']} ({mac})")
else:
self.env['unifi.client'].create(vals)
_logger.info(f"Created new client: {vals['name']} ({mac})")
# Mark clients not in the response as offline
all_clients = self.env['unifi.client'].search([('controller_id', '=', self.id)])
current_macs = [c.get('mac') for c in clients if c.get('mac')]
offline_clients = all_clients.filtered(lambda c: c.mac not in current_macs)
if offline_clients:
offline_clients.write({'status': 'offline'})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': f'Successfully synchronized {len(clients)} clients!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Client synchronization failed: {e}")
def action_sync_networks(self):
"""Synchronize networks from the UniFi Controller."""
self.ensure_one()
try:
client = self.get_client()
networks = client.list_networks()
# Create or update network records
for network in networks:
network_id = network.get('_id')
if not network_id:
continue
# Prepare network values
vals = {
'name': network.get('name', 'Unknown Network'),
'purpose': network.get('purpose', 'corporate'),
'subnet': network.get('ip_subnet'),
'vlan_id': network.get('vlan', ''),
'network_group': network.get('networkgroup', 'LAN'),
'dhcp_enabled': network.get('dhcp_enabled', False),
'dhcp_start': network.get('dhcp_start'),
'dhcp_stop': network.get('dhcp_stop'),
'domain_name': network.get('domain_name'),
'ctrl_id': self.id,
'site_id': network.get('site_id'),
'unifi_id': network_id
}
# Find existing network or create new one
existing_network = self.env['unifi.network'].search([
('unifi_id', '=', network_id),
('ctrl_id', '=', self.id)
])
if existing_network:
existing_network.write(vals)
_logger.info(f"Updated network: {vals['name']} ({network_id})")
else:
self.env['unifi.network'].create(vals)
_logger.info(f"Created new network: {vals['name']} ({network_id})")
# Remove networks that no longer exist
all_networks = self.env['unifi.network'].search([('ctrl_id', '=', self.id)])
current_ids = [n.get('_id') for n in networks if n.get('_id')]
deleted_networks = all_networks.filtered(lambda n: n.unifi_id not in current_ids)
if deleted_networks:
deleted_networks.unlink()
_logger.info(f"Removed {len(deleted_networks)} deleted networks")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': f'Successfully synchronized {len(networks)} networks!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Network synchronization failed: {e}")
def action_sync_sites(self):
"""Synchronize sites from the UniFi Controller."""
self.ensure_one()
try:
client = self.get_client()
sites = client.list_sites()
# Just log the sites for now since we don't need to store them
for site in sites:
site_id = site.get('name') # UniFi uses site name as ID
if site_id:
_logger.info(f"Found site: {site.get('desc', site_id)} (ID: {site_id}, Role: {site.get('role', 'admin')})")
_logger.info(f"Found {len(sites)} sites on controller {self.name}")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': f'Successfully found {len(sites)} sites!',
'type': 'success',
},
}
except Exception as e:
raise UserError(f"Site synchronization failed: {e}")
def action_sync_firewall_rules(self):
"""Synchronize firewall rules from the UniFi Controller."""
self.ensure_one()
try:
_logger.info("Synchronizing port forwarding rules from UniFi controller")
# Get port forward rules from UniFi controller
response = self.get_client().get_port_forward_rules()
_logger.info("Port forward rules raw response: %s", response)
# Extract rules from data field
if isinstance(response, dict):
rules = response.get('data', [])
else:
rules = response or []
_logger.info(f"Got {len(rules)} rules from data field")
for rule in rules:
_logger.info(f"Processing rule: {rule}")
_logger.info(f"Rule type: dst_port={type(rule.get('dst_port'))}, fwd={type(rule.get('fwd'))}")
_logger.info(f"Rule values: dst_port={rule.get('dst_port')}, fwd={rule.get('fwd')}")
# Get existing rules for this controller
existing_rules = self.env['unifi.port.forward'].search([
('controller_id', '=', self.id)
])
existing_rule_map = {r.unifi_id: r for r in existing_rules if r.unifi_id}
# Create or update rules
synced_rule_ids = []
for rule_data in rules:
rule_id = rule_data.get('_id')
# Convert UniFi data to Odoo values
values = self.env['unifi.port.forward'].from_unifi_dict(self.env, self, rule_data)
if rule_id in existing_rule_map:
# Update existing rule
rule = existing_rule_map[rule_id]
rule.write(values)
else:
# Create new rule
rule = self.env['unifi.port.forward'].create(values)
synced_rule_ids.append(rule.id)
# Delete rules that no longer exist on the controller
rules_to_delete = existing_rules.filtered(lambda r: r.id not in synced_rule_ids)
if rules_to_delete:
rules_to_delete.unlink()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': 'Successfully synchronized firewall rules',
'type': 'success'
}
}
except Exception as e:
_logger.error(f"Failed to sync firewall rules: {str(e)}")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Error',
'message': f'Failed to sync firewall rules: {str(e)}',
'type': 'danger'
}
}
def action_sync_wifi(self):
"""Synchronize WiFi networks from the UniFi Controller."""
self.ensure_one()
try:
client = self.get_client()
wifi_model = self.env['unifi.wifi']
wifi_model.sync_from_controller(client, self.id)
# Get count of synced networks
networks_count = wifi_model.search_count([('ctrl_id', '=', self.id)])
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Success',
'message': f'Successfully synchronized {networks_count} WiFi networks!',
'type': 'success'
}
}
except Exception as e:
_logger.error(f"Failed to sync WiFi networks: {str(e)}")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': 'Error',
'message': f'Failed to sync WiFi networks: {str(e)}',
'type': 'danger'
}
}
def ensure_default_firewall_groups(self):
"""Ensure default firewall groups exist in both UniFi and Odoo."""
self.ensure_one()
try:
client = self.get_client()
FirewallGroup = self.env['unifi.firewall.group']
# Get existing groups from UniFi
groups = client.get_firewall_groups()
unifi_groups = groups.get('data', []) if groups else []
# Define default groups
defaults = [
{
'name': 'Default LAN',
'group_type': 'address-group',
'members': '192.168.0.0/16\n172.16.0.0/12\n10.0.0.0/8',
'description': 'Default LAN networks'
},
{
'name': 'Default WAN',
'group_type': 'address-group',
'members': '0.0.0.0/0',
'description': 'Default WAN networks'
}
]
# Create or update groups
for group_data in defaults:
# Check if group exists in UniFi
unifi_group = next((g for g in unifi_groups if g.get('name') == group_data['name']), None)
# Create in UniFi if needed
if not unifi_group:
try:
unifi_data = {
'name': group_data['name'],
'group_type': group_data['group_type'],
'group_members': [m.strip() for m in group_data['members'].split('\n') if m.strip()],
'group_description': group_data.get('description', '')
}
unifi_group = client.create_firewall_group(unifi_data)
if unifi_group:
unifi_group = unifi_group.get('data', {})
except Exception as e:
_logger.warning(f"Failed to create UniFi firewall group: {str(e)}")
# Create or update in Odoo
existing_group = FirewallGroup.search([
('ctrl_id', '=', self.id),
('name', '=', group_data['name'])
], limit=1)
vals = {
'name': group_data['name'],
'ctrl_id': self.id,
'group_type': group_data['group_type'],
'members': group_data['members'],
'description': group_data.get('description', '')
}
if existing_group:
existing_group.write(vals)
_logger.info(f"Updated Odoo firewall group: {group_data['name']}")
else:
FirewallGroup.create(vals)
_logger.info(f"Created Odoo firewall group: {group_data['name']}")
except Exception as e:
_logger.error(f"Failed to ensure default firewall groups: {str(e)}")
@api.model
def _sync_scheduled(self):
"""Scheduled synchronization of all controllers"""
controllers = self.search([])
for controller in controllers:
if controller.should_sync():
controller.sync_all()
def should_sync(self):
"""Check if synchronization is needed based on interval"""
if not self.last_sync:
return True
next_sync = self.last_sync + timedelta(minutes=self.sync_interval)
return fields.Datetime.now() >= next_sync
def action_sync_port_forward(self):
"""Synchronize port forwarding rules from UniFi controller."""
self.ensure_one()
try:
_logger.info("Synchronizing port forwarding rules from UniFi controller")
# Get port forward rules from UniFi controller
response = self.get_client().get_port_forward_rules()
_logger.info("Port forward rules raw response: %s", response)
# Extract rules from data field
if isinstance(response, dict):
rules = response.get('data', [])
else:
rules = response or []
rules_count = len(rules)
_logger.info(f"Got {rules_count} rules from data field")
# Get existing rules for this controller
existing_rules = self.env['unifi.port.forward'].search([
('controller_id', '=', self.id)
])
existing_rule_map = {r.unifi_id: r for r in existing_rules if r.unifi_id}
# Create or update rules
synced_rule_ids = []
for rule_data in rules:
rule_id = rule_data.get('_id')
# Convert UniFi data to Odoo values
values = self.env['unifi.port.forward'].from_unifi_dict(self.env, self, rule_data)
if rule_id in existing_rule_map:
# Update existing rule
rule = existing_rule_map[rule_id]
rule.write(values)
else:
# Create new rule
rule = self.env['unifi.port.forward'].create(values)
synced_rule_ids.append(rule.id)
# Delete rules that no longer exist on the controller
rules_to_delete = existing_rules.filtered(lambda r: r.id not in synced_rule_ids)
if rules_to_delete:
rules_to_delete.unlink()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
'message': _('{} port forwarding rules synchronized successfully').format(rules_count),
'type': 'success',
}
}
except Exception as e:
_logger.error(f"Failed to sync port forwarding rules: {str(e)}")
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Error'),
'message': _('Failed to sync port forwarding rules: {}').format(str(e)),
'type': 'danger'
}
}
@api.depends('last_sync')
def _compute_dashboard_data(self):
"""Compute all dashboard metrics."""
for controller in self:
try:
client = controller.get_client()
# Get clients data
clients = client.list_clients()
devices = client.list_devices()
# Basic metrics
controller.total_clients = len(clients)
controller.active_clients = len([c for c in clients if c.get('status') == 'online'])
controller.total_devices = len(devices)
controller.offline_devices = len([d for d in devices if not d.get('state', True)])
# Bandwidth calculations
wan_stats = client._api_request('GET', f'api/s/{client.site_id}/stat/sta')
total_rx = sum(stat.get('rx_bytes', 0) for stat in wan_stats.get('data', []))
total_tx = sum(stat.get('tx_bytes', 0) for stat in wan_stats.get('data', []))
controller.total_bandwidth = (total_rx + total_tx) / (1024 * 1024) # Convert to Mbps
controller.bandwidth_usage = min(100, (controller.total_bandwidth / 1000) * 100) # Assuming 1Gbps capacity
# Client type chart data
wired_clients = len([c for c in clients if c.get('is_wired')])
wireless_clients = len(clients) - wired_clients
controller.client_type_chart = {
'labels': ['Filaire', 'Sans Fil'],
'datasets': [{
'data': [wired_clients, wireless_clients],
'backgroundColor': ['#36A2EB', '#FF6384']
}]
}
# Bandwidth chart data (last 24h)
bandwidth_stats = client._api_request('GET', f'api/s/{client.site_id}/stat/report/hourly.site')
controller.bandwidth_chart = {
'labels': [stat.get('time') for stat in bandwidth_stats.get('data', [])[-24:]],
'datasets': [{
'label': 'Download',
'data': [stat.get('rx_bytes', 0) / (1024 * 1024) for stat in bandwidth_stats.get('data', [])[-24:]],
'borderColor': '#36A2EB'
}, {
'label': 'Upload',
'data': [stat.get('tx_bytes', 0) / (1024 * 1024) for stat in bandwidth_stats.get('data', [])[-24:]],
'borderColor': '#FF6384'
}]
}
# Security metrics
firewall_rules = client.get_firewall_rules()
controller.active_firewall_rules = len([r for r in firewall_rules.get('data', []) if r.get('enabled')])
events = client._api_request('GET', f'api/s/{client.site_id}/stat/event')
controller.blocked_connections = len([e for e in events.get('data', [])
if e.get('key') == 'EVT_FW_Blocked'])
threats = client._api_request('GET', f'api/s/{client.site_id}/stat/ips/event')
controller.threat_count = len(threats.get('data', []))
# Alerts
system_health = client._api_request('GET', f'api/s/{client.site_id}/stat/health')
alerts = [h for h in system_health.get('data', []) if h.get('severity', 'info') != 'info']
controller.active_alerts = len(alerts)
controller.critical_alerts = len([a for a in alerts if a.get('severity') == 'critical'])
# VPN Status
vpn_status = client._api_request('GET', f'api/s/{client.site_id}/stat/vpn')
active_vpns = [v for v in vpn_status.get('data', []) if v.get('status') == 'up']
if len(active_vpns) == len(vpn_status.get('data', [])):
controller.vpn_status = 'up'
elif len(active_vpns) == 0:
controller.vpn_status = 'down'
else:
controller.vpn_status = 'partial'
except Exception as e:
_logger.error(f"Error computing dashboard data: {str(e)}")
# Set default values in case of error
controller.total_clients = 0
controller.active_clients = 0
controller.total_devices = 0
controller.offline_devices = 0
controller.total_bandwidth = 0
controller.bandwidth_usage = 0
controller.active_alerts = 0
controller.critical_alerts = 0
controller.client_type_chart = {'labels': [], 'datasets': [{'data': []}]}
controller.bandwidth_chart = {'labels': [], 'datasets': []}
controller.active_firewall_rules = 0
controller.blocked_connections = 0
controller.vpn_status = 'down'
controller.threat_count = 0

View file

@ -0,0 +1,69 @@
"""UniFi Device Model."""
from odoo import models, fields, api
class UnifiDevice(models.Model):
_name = 'unifi.device'
_description = 'UniFi Network Device'
name = fields.Char(
string='Name',
required=True,
help="Device name"
)
mac = fields.Char(
string='MAC Address',
required=True,
help="Device MAC address"
)
model = fields.Char(
string='Model',
help="Device model"
)
type = fields.Selection([
('uap', 'Access Point'),
('ugw', 'Gateway'),
('usw', 'Switch'),
('udm', 'Dream Machine'),
('other', 'Other')
], string='Type', required=True)
ip_address = fields.Char(
string='IP Address',
help="Device IP address"
)
version = fields.Char(
string='Firmware Version',
help="Device firmware version"
)
status = fields.Selection([
('online', 'Online'),
('offline', 'Offline'),
('unknown', 'Unknown')
], string='Status', default='unknown')
last_seen = fields.Datetime(
string='Last Seen',
help="Last time the device was seen online"
)
controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
ondelete='cascade'
)
site_id = fields.Char(
string='Site ID',
help="UniFi site identifier"
)
_sql_constraints = [
('unique_mac_per_controller', 'unique(mac,controller_id)',
'A device with this MAC address already exists for this controller!')
]

View file

@ -0,0 +1,47 @@
from odoo import models, fields
class UnifiDPICategory(models.Model):
_name = 'unifi.dpi.category'
_description = 'UniFi DPI Category'
name = fields.Char(string='Name', required=True)
category_id = fields.Integer(string='Category ID', required=True)
description = fields.Text(string='Description')
_sql_constraints = [
('unique_category_id', 'unique(category_id)', 'Category ID must be unique!')
]
def _init_dpi_categories(self):
"""Initialize default DPI categories."""
categories = [
(0, 'Instant messaging', 'Instant messaging applications'),
(1, 'P2P', 'Peer-to-peer file sharing'),
(3, 'File Transfer', 'File transfer protocols'),
(4, 'Streaming Media', 'Streaming media services'),
(5, 'Mail and Collaboration', 'Email and collaboration tools'),
(6, 'Voice over IP', 'Voice over IP services'),
(7, 'Database', 'Database applications'),
(8, 'Games', 'Online gaming'),
(9, 'Network Management', 'Network management protocols'),
(10, 'Remote Access Terminals', 'Remote access services'),
(11, 'Bypass Proxies and Tunnels', 'Proxy and tunnel services'),
(12, 'Stock Market', 'Stock market and trading'),
(13, 'Web', 'Web browsing'),
(14, 'Security Update', 'Security updates'),
(15, 'Web IM', 'Web-based instant messaging'),
(17, 'Business', 'Business applications'),
(18, 'Network Protocols', 'Common network protocols'),
(19, 'Network Protocols', 'Additional network protocols'),
(20, 'Network Protocols', 'More network protocols'),
(23, 'Private Protocol', 'Private protocols'),
(24, 'Social Network', 'Social networking'),
(255, 'Unknown', 'Unknown applications')
]
for cat_id, name, description in categories:
self.env['unifi.dpi.category'].create({
'category_id': cat_id,
'name': name,
'description': description
})

View file

@ -0,0 +1,93 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import ipaddress
class UnifiFirewallGroup(models.Model):
_name = 'unifi.firewall.group'
_description = 'UniFi Firewall Group'
name = fields.Char(
string='Name',
required=True
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True
)
group_type = fields.Selection([
('address-group', 'Address Group'),
('port-group', 'Port Group'),
('ipv6-address-group', 'IPv6 Address Group')
], string='Group Type', required=True)
members = fields.Text(
string='Members',
help="One entry per line. For address groups: IP/CIDR. For port groups: port or port range"
)
description = fields.Text(
string='Description'
)
rule_ids = fields.One2many(
'unifi.firewall.rule.template',
'firewall_group_id',
string='Associated Rules'
)
@api.constrains('members', 'group_type')
def _validate_members(self):
"""Validate member format based on group type."""
for record in self:
if not record.members:
continue
members = record.members.split('\n')
if record.group_type in ['address-group', 'ipv6-address-group']:
for member in members:
member = member.strip()
if not member:
continue
try:
ipaddress.ip_network(member)
except ValueError:
raise ValidationError(f"Invalid IP address or network: {member}")
elif record.group_type == 'port-group':
for member in members:
member = member.strip()
if not member:
continue
if '-' in member:
try:
start, end = map(int, member.split('-'))
if not (1 <= start <= end <= 65535):
raise ValidationError(f"Invalid port range: {member}")
except ValueError:
raise ValidationError(f"Invalid port range format: {member}")
else:
try:
port = int(member)
if not (1 <= port <= 65535):
raise ValidationError(f"Invalid port number: {member}")
except ValueError:
raise ValidationError(f"Invalid port format: {member}")
def copy_to_controller(self):
"""Copy this group to the UniFi controller."""
self.ensure_one()
client = self.ctrl_id.get_client()
group_data = {
'name': self.name,
'group_type': self.group_type,
'group_members': [m.strip() for m in self.members.split('\n') if m.strip()],
}
if self.description:
group_data['group_description'] = self.description
return client.create_firewall_group(group_data)

View file

@ -0,0 +1,245 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
from datetime import datetime
class FirewallRule(models.Model):
_name = 'unifi.firewall.rule'
_description = 'Unifi Firewall Rule'
_order = 'sequence'
name = fields.Char(
string='Rule Name',
required=True
)
unifi_id = fields.Char(
string='UniFi ID',
readonly=True
)
last_sync = fields.Datetime(
string='Last Synchronization',
readonly=True
)
sequence = fields.Integer(
string='Sequence',
default=2000,
help="Rules are processed in sequence order"
)
enabled = fields.Boolean(
string='Enabled',
default=True,
help="Enable or disable this rule"
)
description = fields.Text(
string='Description',
help="Detailed description of the rule's purpose"
)
controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied"
)
direction = fields.Selection([
('in', 'Inbound'),
('out', 'Outbound'),
('local', 'Local'),
('both', 'Both Directions')
], string='Direction', required=True, default='in')
action = fields.Selection([
('accept', 'Accept'),
('drop', 'Drop'),
('reject', 'Reject')
], string='Action', required=True, default='drop')
ruleset = fields.Selection([
('LAN_IN', 'LAN Inbound'),
('LAN_OUT', 'LAN Outbound'),
('LAN_LOCAL', 'LAN Local'),
('WAN_IN', 'WAN Inbound'),
('WAN_OUT', 'WAN Outbound'),
('WAN_LOCAL', 'WAN Local'),
('GUEST_IN', 'Guest Inbound'),
('GUEST_OUT', 'Guest Outbound'),
('GUEST_LOCAL', 'Guest Local')
], string='Rule Set', required=True, default='LAN_IN')
rule_index = fields.Integer(
string='Rule Index',
default=2000,
help="Position of the rule within its ruleset"
)
src_network_id = fields.Many2one(
'unifi.network',
string='Source Network',
help="Source network for this rule"
)
src_address = fields.Char(
string='Source Address',
help="Source IP address or CIDR (e.g., 192.168.1.0/24)"
)
dst_network_id = fields.Many2one(
'unifi.network',
string='Destination Network',
help="Destination network for this rule"
)
dst_address = fields.Char(
string='Destination Address',
help="Destination IP address or CIDR (e.g., 192.168.1.0/24)"
)
protocol = fields.Selection([
('all', 'All'),
('tcp', 'TCP'),
('udp', 'UDP'),
('icmp', 'ICMP')
], string='Protocol', required=True, default='all')
src_port = fields.Char(
string='Source Port',
help="Source port or port range (e.g., 80 or 8000-9000)"
)
dst_port = fields.Char(
string='Destination Port',
help="Destination port or port range (e.g., 80 or 8000-9000)"
)
icmp_type = fields.Selection([
('0', 'Echo Reply'),
('3', 'Destination Unreachable'),
('8', 'Echo Request'),
('11', 'Time Exceeded')
], string='ICMP Type')
state_new = fields.Boolean(string='New Connections', default=True)
state_established = fields.Boolean(string='Established Connections', default=True)
state_invalid = fields.Boolean(string='Invalid Connections', default=False)
state_related = fields.Boolean(string='Related Connections', default=True)
@api.constrains('src_address', 'dst_address')
def _check_ip_format(self):
for record in self:
for field, value in [('src_address', record.src_address), ('dst_address', record.dst_address)]:
if value:
# Check if it's a CIDR notation
if '/' in value:
try:
ip, mask = value.split('/')
if not (0 <= int(mask) <= 32):
raise ValidationError(f'Invalid CIDR mask in {field}. Must be between 0 and 32.')
except ValueError:
raise ValidationError(f'Invalid CIDR format in {field}. Use format like 192.168.1.0/24')
ip_to_check = ip
else:
ip_to_check = value
# Validate IP format
parts = ip_to_check.split('.')
if len(parts) != 4:
raise ValidationError(f'Invalid IP format in {field}. Must be like 192.168.1.0')
for part in parts:
try:
if not (0 <= int(part) <= 255):
raise ValidationError(f'IP parts must be between 0 and 255 in {field}')
except ValueError:
raise ValidationError(f'Invalid IP format in {field}')
@api.constrains('src_port', 'dst_port')
def _check_port_format(self):
for record in self:
for field, value in [('src_port', record.src_port), ('dst_port', record.dst_port)]:
if value:
# Check if it's a port range
if '-' in value:
try:
start, end = map(int, value.split('-'))
if not (0 <= start <= 65535 and 0 <= end <= 65535 and start <= end):
raise ValidationError(f'Invalid port range in {field}. Must be between 0-65535 and start must be <= end')
except ValueError:
raise ValidationError(f'Invalid port range format in {field}. Use format like 8000-9000')
else:
try:
port = int(value)
if not (0 <= port <= 65535):
raise ValidationError(f'Port must be between 0 and 65535 in {field}')
except ValueError:
raise ValidationError(f'Invalid port number in {field}')
@api.model
def sync_from_controller(self, client, controller_id):
"""Synchronize firewall rules from UniFi Controller."""
rules = client.get_firewall_rules()
for rule in rules:
values = {
'name': rule.get('name', 'Unnamed Rule'),
'unifi_id': rule.get('_id'),
'enabled': rule.get('enabled', True),
'action': rule.get('action', 'drop'),
'protocol': rule.get('protocol', 'all'),
'src_address': rule.get('src_address') or rule.get('src_ip', ''),
'dst_address': rule.get('dst_address') or rule.get('dst_ip', ''),
'src_port': rule.get('src_port', ''),
'dst_port': rule.get('dst_port', ''),
'direction': rule.get('direction', 'in'),
'ruleset': rule.get('ruleset', 'LAN_IN'),
'rule_index': rule.get('rule_index', 2000),
'description': rule.get('description', ''),
'last_sync': fields.Datetime.now(),
'controller_id': controller_id,
}
# Update existing or create new
existing = self.search([
('unifi_id', '=', values['unifi_id']),
('controller_id', '=', controller_id)
])
if existing:
existing.write(values)
else:
self.create(values)
def sync_to_controller(self):
"""Synchronize firewall rule to UniFi Controller."""
self.ensure_one()
client = self.controller_id.get_client()
rule_data = {
'name': self.name,
'enabled': self.enabled,
'action': self.action,
'protocol': self.protocol,
'src_address': self.src_address,
'dst_address': self.dst_address,
'src_port': self.src_port,
'dst_port': self.dst_port,
'direction': self.direction,
'ruleset': self.ruleset,
'rule_index': self.rule_index,
'description': self.description,
}
if self.unifi_id:
# Update existing rule
client.update_firewall_rule(self.unifi_id, rule_data)
else:
# Create new rule
result = client.create_firewall_rule(rule_data)
if result and result.get('_id'):
self.write({
'unifi_id': result['_id'],
'last_sync': fields.Datetime.now()
})

View file

@ -0,0 +1,157 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
import ipaddress
class FirewallRuleTemplate(models.Model):
_name = 'unifi.firewall.rule.template'
_description = 'Firewall Rule Template'
_order = 'rule_index'
name = fields.Char(
string='Template Name',
required=True
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)
enabled = fields.Boolean(
string='Enabled',
default=True,
help="Whether this rule is active"
)
ruleset = fields.Selection([
('LAN_IN', 'LAN In'),
('LAN_OUT', 'LAN Out'),
('LAN_LOCAL', 'LAN Local'),
('WAN_IN', 'WAN In'),
('WAN_OUT', 'WAN Out'),
('WAN_LOCAL', 'WAN Local')
], string='Rule Set', required=True, default='LAN_IN')
rule_index = fields.Integer(
string='Rule Index',
help="Order in which the rule is applied",
default=2000
)
action = fields.Selection(
selection=[
('accept', 'Accept'),
('drop', 'Drop'),
('reject', 'Reject')
],
string='Action',
required=True
)
src_ip = fields.Char(
string='Source IP',
help="Source IP address or network (e.g., 192.168.1.0/24)"
)
dst_ip = fields.Char(
string='Destination IP',
help="Destination IP address or network (e.g., 192.168.1.0/24)"
)
protocol = fields.Selection(
selection=[
('all', 'All'),
('tcp', 'TCP'),
('udp', 'UDP'),
('icmp', 'ICMP')
],
string='Protocol',
required=True,
default='all'
)
port = fields.Char(
string='Port',
help="Port or port range (e.g., 80 or 8000-9000)"
)
firewall_group_id = fields.Many2one(
'unifi.firewall.group',
string='Firewall Group',
help="Associated firewall group"
)
categories = fields.Many2many(
'unifi.dpi.category',
string='DPI Categories',
help="Deep Packet Inspection categories this rule applies to"
)
@api.constrains('port')
def _check_port(self):
"""Validate port format."""
for record in self:
if not record.port:
continue
# Check single port
if record.port.isdigit():
port = int(record.port)
if not (1 <= port <= 65535):
raise ValidationError("Port must be between 1 and 65535")
continue
# Check port range
if '-' in record.port:
try:
start, end = map(int, record.port.split('-'))
if not (1 <= start <= end <= 65535):
raise ValidationError("Port range must be between 1 and 65535")
except ValueError:
raise ValidationError("Invalid port range format. Use format: start-end")
else:
raise ValidationError("Invalid port format. Use a single port or range (e.g., 80 or 8000-9000)")
@api.constrains('src_ip', 'dst_ip')
def _check_ip_addresses(self):
"""Validate IP address formats."""
for record in self:
for field in [record.src_ip, record.dst_ip]:
if not field:
continue
try:
# Handle CIDR notation
if '/' in field:
ipaddress.ip_network(field)
else:
ipaddress.ip_address(field)
except ValueError:
raise ValidationError(f"Invalid IP address format: {field}")
def copy_to_controller(self):
"""Copy this template to the actual firewall rules on the controller."""
self.ensure_one()
client = self.ctrl_id.get_client()
rule_data = {
'name': self.name,
'enabled': self.enabled,
'ruleset': self.ruleset,
'rule_index': self.rule_index,
'protocol': self.protocol,
'action': self.action,
}
if self.port:
rule_data['port'] = self.port
if self.src_ip:
rule_data['src'] = self.src_ip
if self.dst_ip:
rule_data['dst'] = self.dst_ip
if self.categories:
rule_data['categories'] = self.categories.mapped('category_id')
return client.create_firewall_rule(rule_data)

View file

@ -0,0 +1,72 @@
"""Data models for UniFi Controller API responses."""
from dataclasses import dataclass
from typing import List, Optional, Dict
from datetime import datetime
@dataclass
class NetworkConfig:
"""Network configuration data model."""
_id: str
name: str
purpose: str
subnet: str
vlan_enabled: bool
vlan_id: Optional[int] = None
dhcp_enabled: bool = True
dhcp_start: Optional[str] = None
dhcp_stop: Optional[str] = None
@dataclass
class WifiConfig:
"""WiFi configuration data model."""
_id: str
name: str
security: str
wpa_mode: str
wpa_enc: str
enabled: bool
is_guest: bool = False
hide_ssid: bool = False
vlan_enabled: bool = False
vlan_id: Optional[int] = None
@dataclass
class ClientDevice:
"""Connected client device data model."""
_id: str
mac: str
hostname: Optional[str]
ip: Optional[str]
network_id: str
last_seen: datetime
is_guest: bool = False
blocked: bool = False
data_usage: Optional[Dict] = None
@dataclass
class SystemHealth:
"""System health statistics data model."""
subsystem: str
status: str
num_user: int
num_guest: int
lan_throughput: float
wlan_throughput: float
wan_throughput: float
cpu_usage: float
mem_usage: float
@dataclass
class FirewallRule:
"""Firewall rule data model."""
_id: str
name: str
enabled: bool
action: str
protocol: str
src_address: Optional[str] = None
dst_address: Optional[str] = None
src_port: Optional[str] = None
dst_port: Optional[str] = None
ruleset: str = "LAN_IN"
rule_index: int = 2000

View file

@ -0,0 +1,234 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
from datetime import datetime
class Network(models.Model):
_name = 'unifi.network'
_description = 'Unifi Network'
name = fields.Char(
string='Network Name',
required=True
)
unifi_id = fields.Char(
string='UniFi ID',
readonly=True
)
last_sync = fields.Datetime(
string='Last Synchronization',
readonly=True
)
cidr = fields.Char(
string='CIDR',
help="CIDR notation for the network (e.g., 192.168.1.0/24)."
)
gateway = fields.Char(
string='Gateway',
help="Gateway IP address for the network."
)
vlan_id = fields.Integer(
string='VLAN ID',
help="VLAN ID for the network (1-4094)."
)
description = fields.Text(
string='Description'
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)
purpose = fields.Selection([
('corporate', 'Corporate'),
('guest', 'Guest'),
('wan', 'WAN'),
('vlan-only', 'VLAN Only'),
('remote-user-vpn', 'Remote User VPN'),
('site-vpn', 'Site VPN'),
('vlan', 'VLAN'),
('wan2', 'WAN2'),
('sip', 'SIP'),
('unifionly', 'UniFi Only')
], string='Purpose', default='corporate')
network_group = fields.Selection([
('LAN', 'LAN'),
('WAN', 'WAN'),
('CORP', 'Corporate'),
('GUEST', 'Guest'),
('VPN', 'VPN'),
('VLAN', 'VLAN')
], string='Network Group', default='LAN')
firewall_enabled = fields.Boolean(
string='Enable Firewall',
default=True,
help="Enable or disable firewall for this network"
)
firewall_type = fields.Selection([
('auto', 'Auto'),
('custom', 'Custom')
], string='Firewall Type', default='auto')
firewall_default_action = fields.Selection([
('accept', 'Accept'),
('drop', 'Drop'),
('reject', 'Reject')
], string='Default Action', default='drop')
inter_client_routing = fields.Boolean(
string='Inter-Client Routing',
default=True,
help="Allow clients on this network to communicate with each other"
)
dhcp_enabled = fields.Boolean(
string='DHCP Enabled',
default=True
)
dhcp_start = fields.Char(
string='DHCP Start',
help="Start of DHCP range"
)
dhcp_stop = fields.Char(
string='DHCP Stop',
help="End of DHCP range"
)
domain_name = fields.Char(
string='Domain Name'
)
site_id = fields.Char(
string='Site ID',
readonly=True
)
subnet = fields.Char(
string='Subnet',
help="Network subnet in CIDR notation"
)
@api.constrains('cidr')
def _check_cidr_format(self):
for record in self:
if record.cidr:
# Validate CIDR format (e.g., 192.168.1.0/24)
cidr_pattern = r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$'
if not re.match(cidr_pattern, record.cidr):
raise ValidationError('Invalid CIDR format. Example of valid format: 192.168.1.0/24')
# Validate IP address parts
ip = record.cidr.split('/')[0]
parts = ip.split('.')
for part in parts:
if not 0 <= int(part) <= 255:
raise ValidationError('IP address parts must be between 0 and 255')
# Validate subnet mask
mask = int(record.cidr.split('/')[1])
if not 0 <= mask <= 32:
raise ValidationError('Subnet mask must be between 0 and 32')
@api.constrains('gateway')
def _check_gateway_format(self):
for record in self:
if record.gateway:
# Validate IP format
ip_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(ip_pattern, record.gateway):
raise ValidationError('Invalid Gateway IP format. Example of valid format: 192.168.1.1')
# Validate IP parts
parts = record.gateway.split('.')
for part in parts:
if not 0 <= int(part) <= 255:
raise ValidationError('Gateway IP parts must be between 0 and 255')
@api.constrains('vlan_id')
def _check_vlan_id(self):
for record in self:
if record.vlan_id and not (1 <= record.vlan_id <= 4094):
raise ValidationError('VLAN ID must be between 1 and 4094')
@api.model
def sync_from_controller(self, client, controller_id):
"""Synchronize networks from UniFi Controller."""
networks = client.get_networks()
for network in networks:
# Extract network details
values = {
'name': network.get('name', ''),
'unifi_id': network.get('_id', ''),
'description': network.get('purpose', ''),
'last_sync': fields.Datetime.now(),
'ctrl_id': controller_id,
'purpose': network.get('purpose', 'corporate'),
'network_group': network.get('networkgroup', 'LAN'),
'dhcp_enabled': network.get('dhcp_enabled', True),
'dhcp_start': network.get('dhcp_start', ''),
'dhcp_stop': network.get('dhcp_stop', ''),
'domain_name': network.get('domain_name', ''),
'site_id': network.get('site_id', ''),
'subnet': network.get('subnet', ''),
'gateway': network.get('gateway_ip', ''),
'vlan_id': network.get('vlan', 0),
}
# Handle CIDR for different network types
if network.get('purpose') == 'wan':
values['cidr'] = '0.0.0.0/0' # Default CIDR for WAN
else:
values['cidr'] = network.get('ip_subnet', '0.0.0.0/0')
# Update existing or create new
existing = self.search([('unifi_id', '=', values['unifi_id'])])
if existing:
existing.write(values)
else:
self.create(values)
def sync_to_unifi(self):
"""Push network changes to UniFi Controller"""
self.ensure_one()
client = self.env['unifi.client'].get_client()
network_data = {
'name': self.name,
'subnet': self.cidr,
'gateway_ip': self.gateway,
'vlan': self.vlan_id,
'purpose': self.description,
'purpose': self.purpose,
'network_group': self.network_group,
'dhcp_enabled': self.dhcp_enabled,
'dhcp_start': self.dhcp_start,
'dhcp_stop': self.dhcp_stop,
'domain_name': self.domain_name,
}
if self.unifi_id:
# Update existing network
client.update_network(self.unifi_id, network_data)
else:
# Create new network
result = client.create_network(network_data)
if result and result.get('_id'):
self.write({
'unifi_id': result['_id'],
'last_sync': fields.Datetime.now()
})

View file

@ -0,0 +1,210 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError
import re
from datetime import datetime
import logging
class PortForwardRule(models.Model):
_name = 'unifi.port.forward'
_description = 'UniFi Port Forward Rule'
_order = 'name'
name = fields.Char(
string='Rule Name',
required=True
)
unifi_id = fields.Char(
string='UniFi ID',
readonly=True
)
last_sync = fields.Datetime(
string='Last Synchronization',
readonly=True
)
enabled = fields.Boolean(
string='Enabled',
default=True,
help="Enable or disable this rule"
)
description = fields.Text(
string='Description',
help="Detailed description of the rule's purpose"
)
controller_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied"
)
# Port Forward specific fields
dst_port = fields.Char(
string='Destination Port',
required=True,
help="Port or port range (e.g., '80' or '80:85')"
)
fwd_port = fields.Char(
string='Forward Port',
required=True,
help="Port or port range to forward to"
)
fwd_ip = fields.Char(
string='Forward IP',
required=True,
help="IP address to forward to"
)
protocol = fields.Selection([
('tcp', 'TCP'),
('udp', 'UDP'),
('tcp_udp', 'TCP & UDP')
], string='Protocol', required=True, default='tcp')
src = fields.Char(
string='Source',
help="Source address/network (e.g., '192.168.1.0/24' or 'any')"
)
dst = fields.Char(
string='Destination',
help="Destination address/network"
)
log = fields.Boolean(
string='Log',
default=False,
help="Enable logging for this rule"
)
@api.constrains('fwd_ip')
def _check_fwd_ip(self):
"""Validate forward IP format."""
for record in self:
if record.fwd_ip:
try:
# Vérifier que c'est une adresse IPv4 valide
parts = record.fwd_ip.split('.')
if len(parts) != 4:
raise ValueError
for part in parts:
num = int(part)
if not (0 <= num <= 255):
raise ValueError
except (ValueError, AttributeError):
raise ValidationError(f"Invalid forward IP: {record.fwd_ip}. Must be a valid IPv4 address (e.g., 192.168.1.100)")
@api.constrains('dst_port', 'fwd_port')
def _check_ports(self):
"""Validate port format."""
_logger = logging.getLogger(__name__)
for record in self:
# Log les valeurs pour le débogage
_logger.info(f"Validating ports - dst_port: '{record.dst_port}', fwd_port: '{record.fwd_port}'")
def is_valid_port(port_str):
"""Vérifie si un port est valide."""
if not port_str or port_str == 'any':
return True
# Nettoyer la chaîne
port_str = port_str.strip()
# Remplacer le tiret par deux-points si présent
if '-' in port_str:
port_str = port_str.replace('-', ':')
try:
# Essayer de convertir en nombre unique
port = int(port_str)
return 1 <= port <= 65535
except ValueError:
try:
# Essayer de traiter comme une plage
start, end = map(int, port_str.split(':'))
return 1 <= start <= 65535 and 1 <= end <= 65535 and start <= end
except (ValueError, TypeError):
return False
if record.dst_port and not is_valid_port(record.dst_port):
raise ValidationError(f"Invalid destination port: {record.dst_port}. Must be a number between 1-65535 or a range (e.g., '80:85' or '80-85')")
if record.fwd_port and not is_valid_port(record.fwd_port):
raise ValidationError(f"Invalid forward port: {record.fwd_port}. Must be a number between 1-65535 or a range (e.g., '80:85' or '80-85')")
@classmethod
def from_unifi_dict(cls, env, controller, data):
"""Create a port forward rule from UniFi data."""
# Log pour le débogage
_logger = logging.getLogger(__name__)
_logger.info(f"Converting UniFi data to port forward: {data}")
def format_port(port_value):
"""Format port value to string, handling various input types."""
_logger.info(f"Formatting port value: {port_value} of type {type(port_value)}")
if port_value is None:
return ''
# Convertir en chaîne et nettoyer
port_str = str(port_value).strip()
# Convertir le format tiret en format deux-points pour les plages
if '-' in port_str:
port_str = port_str.replace('-', ':')
return port_str
# Dans l'API UniFi:
# - dst_port : port de destination externe
# - fwd : IP de destination interne
# - fwd_port : port de destination interne (si différent de dst_port)
values = {
'controller_id': controller.id,
'unifi_id': data.get('_id'),
'name': data.get('name', 'Unnamed Rule'),
'enabled': data.get('enabled', True),
'dst_port': format_port(data.get('dst_port')),
'fwd_port': format_port(data.get('dst_port')), # Par défaut, même port que dst_port
'fwd_ip': data.get('fwd', ''), # L'IP est dans le champ 'fwd'
'protocol': data.get('proto', 'tcp'),
'src': data.get('src', 'any'),
'dst': data.get('dst', 'any'),
'log': data.get('log', False),
'last_sync': datetime.now()
}
# Si un port de destination interne spécifique est défini, l'utiliser
if 'fwd_port' in data:
values['fwd_port'] = format_port(data['fwd_port'])
_logger.info(f"Converted values: {values}")
return values
def to_unifi_dict(self):
"""Convert the record to a UniFi-compatible dictionary."""
self.ensure_one()
def format_port_for_unifi(port_str):
"""Convert port format back to UniFi format."""
if port_str and ':' in port_str:
return port_str.replace(':', '-')
return port_str
return {
'_id': self.unifi_id or None,
'name': self.name,
'enabled': self.enabled,
'dst_port': format_port_for_unifi(self.dst_port),
'fwd': self.fwd_ip,
'fwd_port': format_port_for_unifi(self.fwd_port) if self.fwd_port != self.dst_port else None,
'proto': self.protocol,
'src': self.src or 'any',
'dst': self.dst or 'any',
'log': self.log
}

View file

@ -0,0 +1,16 @@
from odoo import models, fields, api
class UnifiSite(models.Model):
_name = 'unifi.site'
_description = 'UniFi Site'
name = fields.Char(string='Name', required=True)
site_id = fields.Char(string='Site ID', required=True)
description = fields.Text(string='Description')
controller_id = fields.Many2one('unifi.ctrl', string='Controller', required=True)
device_ids = fields.One2many('unifi.device', 'site_id', string='Devices')
active = fields.Boolean(default=True)
_sql_constraints = [
('site_id_controller_uniq', 'unique(site_id,controller_id)', 'Site ID must be unique per controller!')
]

View file

@ -0,0 +1,54 @@
"""UniFi System Health Model."""
from odoo import models, fields, api
from odoo.exceptions import ValidationError
from datetime import datetime
class UnifiSystemHealth(models.Model):
"""System health statistics from UniFi Controller."""
_name = 'unifi.system.health'
_description = 'UniFi System Health'
subsystem = fields.Char(string='Subsystem', required=True)
status = fields.Selection([
('ok', 'OK'),
('warning', 'Warning'),
('error', 'Error'),
], string='Status', required=True)
num_user = fields.Integer(string='Number of Users')
num_guest = fields.Integer(string='Number of Guests')
lan_throughput = fields.Float(string='LAN Throughput (bytes/s)')
wlan_throughput = fields.Float(string='WLAN Throughput (bytes/s)')
wan_throughput = fields.Float(string='WAN Throughput (bytes/s)')
cpu_usage = fields.Float(string='CPU Usage (%)')
mem_usage = fields.Float(string='Memory Usage (%)')
timestamp = fields.Datetime(string='Timestamp', default=fields.Datetime.now)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this data is from."
)
@api.model
def sync_from_controller(self, client, controller_id):
"""Synchronize system health from UniFi Controller."""
health_data = client.get_system_health()
for data in health_data:
vals = {
'subsystem': data.get('subsystem', ''),
'status': data.get('status', 'error').lower(),
'num_user': data.get('num_user', 0),
'num_guest': data.get('num_guest', 0),
'lan_throughput': data.get('lan_throughput', 0.0),
'wlan_throughput': data.get('wlan_throughput', 0.0),
'wan_throughput': data.get('wan_throughput', 0.0),
'cpu_usage': data.get('cpu_usage', 0.0),
'mem_usage': data.get('mem_usage', 0.0),
'timestamp': fields.Datetime.now(),
'ctrl_id': controller_id,
}
# Create new record each time as this is time-series data
self.create(vals)

View file

@ -0,0 +1,117 @@
from odoo import models, fields, api
from datetime import datetime
import logging
_logger = logging.getLogger(__name__)
class Wifi(models.Model):
_name = 'unifi.wifi'
_description = 'Unifi WiFi Network'
name = fields.Char(string='WiFi Name (SSID)', required=True)
unifi_id = fields.Char(string='UniFi ID', readonly=True)
last_sync = fields.Datetime(string='Last Synchronization', readonly=True)
security_mode = fields.Selection(
[('open', 'Open'), ('wpa', 'WPA/WPA2 Personal'), ('wpa3', 'WPA3')],
string='Security Mode',
required=True,
default='wpa'
)
password = fields.Char(
string='WiFi Password',
help="Password for the WiFi network (if applicable)."
)
vlan_id = fields.Many2one(
comodel_name='unifi.network',
string='VLAN'
)
description = fields.Text(
string='Description'
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)
@api.model
def sync_from_controller(self, client, controller_id):
"""Synchronize WiFi networks from UniFi Controller."""
try:
_logger.info(f"Starting WiFi sync for controller {controller_id}")
wifi_configs = client.get_wifis()
synced_ids = []
for wifi in wifi_configs:
# Map security mode
security = wifi.get('security', '').lower()
if 'wpa3' in security:
security_mode = 'wpa3'
elif 'wpa' in security:
security_mode = 'wpa'
else:
security_mode = 'open'
values = {
'name': wifi.get('name', ''),
'unifi_id': wifi.get('_id', ''),
'security_mode': security_mode,
'password': wifi.get('x_passphrase', ''), # Note: This might be encrypted
'description': wifi.get('name_combine', '') or wifi.get('name', ''),
'last_sync': fields.Datetime.now(),
'ctrl_id': controller_id,
}
# Try to find VLAN if specified
vlan_id = wifi.get('vlan_id')
if vlan_id:
network = self.env['unifi.network'].search([
('ctrl_id', '=', controller_id),
('vlan', '=', vlan_id)
], limit=1)
if network:
values['vlan_id'] = network.id
# Update existing or create new
existing = self.search([
('unifi_id', '=', values['unifi_id']),
('ctrl_id', '=', controller_id)
])
if existing:
_logger.info(f"Updating existing WiFi network: {values['name']}")
existing.write(values)
else:
_logger.info(f"Creating new WiFi network: {values['name']}")
self.create(values)
synced_ids.append(values['unifi_id'])
# Disable WiFi networks that no longer exist in the controller
outdated = self.search([
('ctrl_id', '=', controller_id),
('unifi_id', 'not in', synced_ids)
])
if outdated:
_logger.info(f"Disabling {len(outdated)} outdated WiFi networks")
outdated.unlink()
_logger.info("WiFi synchronization completed successfully")
except Exception as e:
_logger.error(f"Error during WiFi synchronization: {str(e)}")
raise api.UserError(f"Failed to sync WiFi networks: {str(e)}")
def sync_to_controller(self):
"""Synchronize WiFi network to UniFi Controller."""
self.ensure_one()
client = self.ctrl_id.get_client()
# Implementation of pushing changes to UniFi
# This would need to be implemented based on UniFi's API
pass

View file

@ -1,35 +0,0 @@
from odoo import models, fields
class Wifi(models.Model):
_name = 'unifi.wifi'
_description = 'Unifi WiFi Network'
name = fields.Char(string='WiFi Name (SSID)', required=True)
security_mode = fields.Selection(
[('open', 'Open'), ('wpa', 'WPA/WPA2 Personal'), ('wpa3', 'WPA3')],
string='Security Mode',
required=True,
default='wpa'
)
password = fields.Char(
string='WiFi Password',
help="Password for the WiFi network (if applicable)."
)
vlan_id = fields.Many2one(
comodel_name='unifi.network',
string='VLAN',
help="VLAN associated with this WiFi network."
)
description = fields.Text(
string='Description'
)
ctrl_id = fields.Many2one(
'unifi.ctrl',
string='Controller',
required=True,
help="Controller where this rule is applied."
)

View file

@ -0,0 +1 @@
pyunifi==2.21.1

View file

@ -1,7 +1,18 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
odoo_unifi_manager.access_unifi_firewall_rule,access_unifi_firewall_rule,odoo_unifi_manager.model_unifi_firewall_rule,base.group_user,1,0,0,0
odoo_unifi_manager.access_unifi_firewall_rule_template,access_unifi_firewall_rule_template,odoo_unifi_manager.model_unifi_firewall_rule_template,base.group_user,1,0,0,0
odoo_unifi_manager.access_unifi_firewall_rule_wizard,access_unifi_firewall_rule_wizard,odoo_unifi_manager.model_unifi_firewall_rule_wizard,base.group_user,1,0,0,0
odoo_unifi_manager.access_unifi_network,access_unifi_network,odoo_unifi_manager.model_unifi_network,base.group_user,1,1,1,0
odoo_unifi_manager.access_unifi_wifi,access_unifi_wifi,odoo_unifi_manager.model_unifi_wifi,base.group_user,1,1,1,0
odoo_unifi_manager.access_unifi_ctrl,access_unifi_ctrl,odoo_unifi_manager.model_unifi_ctrl,base.group_system,1,1,1,1
access_unifi_ctrl_user,unifi.ctrl.user,model_unifi_ctrl,base.group_user,1,1,1,1
access_unifi_firewall_rule_template_user,unifi.firewall.rule.template.user,model_unifi_firewall_rule_template,base.group_user,1,1,1,1
access_unifi_system_health_user,access_unifi_system_health,model_unifi_system_health,base.group_user,1,1,1,1
access_unifi_client_user,access_unifi_client,model_unifi_client,base.group_user,1,1,1,1
access_unifi_device_user,access_unifi_device,model_unifi_device,base.group_user,1,1,1,1
access_unifi_firewall_group_user,unifi.firewall.group.user,model_unifi_firewall_group,odoo_unifi_manager.group_unifi_user,1,0,0,0
access_unifi_firewall_group_manager,unifi.firewall.group.manager,model_unifi_firewall_group,odoo_unifi_manager.group_unifi_manager,1,1,1,1
access_unifi_dpi_category_user,unifi.dpi.category.user,model_unifi_dpi_category,odoo_unifi_manager.group_unifi_user,1,0,0,0
access_unifi_dpi_category_manager,unifi.dpi.category.manager,model_unifi_dpi_category,odoo_unifi_manager.group_unifi_manager,1,1,1,1
access_unifi_firewall_rule,access_unifi_firewall_rule,model_unifi_firewall_rule,base.group_user,1,1,1,1
access_unifi_network,access_unifi_network,model_unifi_network,base.group_user,1,1,1,1
access_unifi_wifi,access_unifi_wifi,model_unifi_wifi,base.group_user,1,1,1,1
access_unifi_firewall_rule_wizard,access_unifi_firewall_rule_wizard,model_unifi_firewall_rule_wizard,base.group_user,1,1,1,1
access_unifi_site_user,unifi.site.user,model_unifi_site,odoo_unifi_manager.group_unifi_user,1,0,0,0
access_unifi_site_manager,unifi.site.manager,model_unifi_site,odoo_unifi_manager.group_unifi_manager,1,1,1,1
access_unifi_port_forward_system,access_unifi_port_forward_system,model_unifi_port_forward,base.group_system,1,1,1,1
access_unifi_port_forward_user,access_unifi_port_forward_user,model_unifi_port_forward,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 odoo_unifi_manager.access_unifi_firewall_rule access_unifi_ctrl_user access_unifi_firewall_rule unifi.ctrl.user odoo_unifi_manager.model_unifi_firewall_rule model_unifi_ctrl base.group_user 1 0 1 0 1 0 1
3 odoo_unifi_manager.access_unifi_firewall_rule_template access_unifi_firewall_rule_template_user access_unifi_firewall_rule_template unifi.firewall.rule.template.user odoo_unifi_manager.model_unifi_firewall_rule_template model_unifi_firewall_rule_template base.group_user 1 0 1 0 1 0 1
4 odoo_unifi_manager.access_unifi_firewall_rule_wizard access_unifi_system_health_user access_unifi_firewall_rule_wizard access_unifi_system_health odoo_unifi_manager.model_unifi_firewall_rule_wizard model_unifi_system_health base.group_user 1 0 1 0 1 0 1
5 odoo_unifi_manager.access_unifi_network access_unifi_client_user access_unifi_network access_unifi_client odoo_unifi_manager.model_unifi_network model_unifi_client base.group_user 1 1 1 0 1
6 odoo_unifi_manager.access_unifi_wifi access_unifi_device_user access_unifi_wifi access_unifi_device odoo_unifi_manager.model_unifi_wifi model_unifi_device base.group_user 1 1 1 0 1
7 odoo_unifi_manager.access_unifi_ctrl access_unifi_firewall_group_user access_unifi_ctrl unifi.firewall.group.user odoo_unifi_manager.model_unifi_ctrl model_unifi_firewall_group base.group_system odoo_unifi_manager.group_unifi_user 1 1 0 1 0 1 0
8 access_unifi_firewall_group_manager unifi.firewall.group.manager model_unifi_firewall_group odoo_unifi_manager.group_unifi_manager 1 1 1 1
9 access_unifi_dpi_category_user unifi.dpi.category.user model_unifi_dpi_category odoo_unifi_manager.group_unifi_user 1 0 0 0
10 access_unifi_dpi_category_manager unifi.dpi.category.manager model_unifi_dpi_category odoo_unifi_manager.group_unifi_manager 1 1 1 1
11 access_unifi_firewall_rule access_unifi_firewall_rule model_unifi_firewall_rule base.group_user 1 1 1 1
12 access_unifi_network access_unifi_network model_unifi_network base.group_user 1 1 1 1
13 access_unifi_wifi access_unifi_wifi model_unifi_wifi base.group_user 1 1 1 1
14 access_unifi_firewall_rule_wizard access_unifi_firewall_rule_wizard model_unifi_firewall_rule_wizard base.group_user 1 1 1 1
15 access_unifi_site_user unifi.site.user model_unifi_site odoo_unifi_manager.group_unifi_user 1 0 0 0
16 access_unifi_site_manager unifi.site.manager model_unifi_site odoo_unifi_manager.group_unifi_manager 1 1 1 1
17 access_unifi_port_forward_system access_unifi_port_forward_system model_unifi_port_forward base.group_system 1 1 1 1
18 access_unifi_port_forward_user access_unifi_port_forward_user model_unifi_port_forward base.group_user 1 0 0 0

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- UniFi Manager Security Category -->
<record id="module_category_unifi" model="ir.module.category">
<field name="name">UniFi Manager</field>
<field name="description">Manage UniFi network devices and configurations</field>
<field name="sequence">20</field>
</record>
<!-- UniFi User Group -->
<record id="group_unifi_user" model="res.groups">
<field name="name">User</field>
<field name="category_id" ref="module_category_unifi"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- UniFi Manager Group -->
<record id="group_unifi_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id" ref="module_category_unifi"/>
<field name="implied_ids" eval="[(4, ref('group_unifi_user'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
</data>
</odoo>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,90 @@
.o_unifi_dashboard {
padding: 1rem;
}
.o_unifi_dashboard_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.o_unifi_dashboard_header h1 {
margin: 0;
color: #2b2b2b;
}
.o_unifi_dashboard .card {
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 1rem;
border: none;
}
.o_unifi_dashboard .card-body {
padding: 1.5rem;
}
.o_unifi_dashboard .card-title {
color: #666;
font-size: 0.9rem;
margin-bottom: 1rem;
font-weight: 600;
}
.o_unifi_dashboard .metric {
font-size: 1.8rem;
font-weight: bold;
color: #2b2b2b;
line-height: 1;
}
.o_unifi_dashboard .text-success {
color: #28a745;
}
.o_unifi_dashboard .text-danger {
color: #dc3545;
}
.o_unifi_dashboard .chart-container {
height: 300px;
position: relative;
margin-top: 1rem;
}
.o_unifi_dashboard .security-stat {
text-align: center;
padding: 1rem;
}
.o_unifi_dashboard .security-stat label {
display: block;
color: #666;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
.o_unifi_dashboard .badge {
font-size: 0.9rem;
padding: 0.4rem 0.8rem;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.o_unifi_dashboard .card {
margin-bottom: 1rem;
}
.o_unifi_dashboard .metric {
font-size: 1.4rem;
}
.o_unifi_dashboard .chart-container {
height: 200px;
}
.o_unifi_dashboard .security-stat {
padding: 0.5rem;
}
}

View file

@ -0,0 +1,123 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl";
import { KanbanController } from "@web/views/kanban/kanban_controller";
import { KanbanRenderer } from "@web/views/kanban/kanban_renderer";
import { kanbanView } from "@web/views/kanban/kanban_view";
class UnifiDashboardController extends KanbanController {
setup() {
super.setup();
this.orm = useService("orm");
this.action = useService("action");
}
/**
* @override
*/
getRendererProps() {
const props = super.getRendererProps();
return {
...props,
onRefreshDashboard: this.onRefreshDashboard.bind(this),
};
}
async onRefreshDashboard() {
await this.model.load();
await this.model.root.load();
this.render(true);
}
}
class UnifiDashboardRenderer extends KanbanRenderer {
static template = 'odoo_unifi_manager.UnifiDashboard';
static components = { Layout };
static props = {
...KanbanRenderer.props,
onRefreshDashboard: Function,
};
setup() {
super.setup();
this.chartInstances = {};
onMounted(() => {
this.initCharts();
});
onWillUnmount(() => {
// Cleanup charts
Object.values(this.chartInstances).forEach(chart => chart.destroy());
});
}
initCharts() {
if (!this.props.list.records.length) return;
const record = this.props.list.records[0];
if (!record) return;
// Clean up existing charts
Object.values(this.chartInstances).forEach(chart => chart.destroy());
this.chartInstances = {};
// Initialize Client Type Chart
const ctxClients = document.getElementById('clientTypeChart')?.getContext('2d');
if (ctxClients) {
this.chartInstances.clientType = new Chart(ctxClients, {
type: 'doughnut',
data: record.data.client_type_chart,
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
// Initialize Bandwidth Chart
const ctxBandwidth = document.getElementById('bandwidthChart')?.getContext('2d');
if (ctxBandwidth) {
this.chartInstances.bandwidth = new Chart(ctxBandwidth, {
type: 'line',
data: record.data.bandwidth_chart,
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Mbps'
}
}
},
plugins: {
legend: {
position: 'bottom'
}
}
}
});
}
}
}
export const unifiDashboardView = {
...kanbanView,
Controller: UnifiDashboardController,
Renderer: UnifiDashboardRenderer,
buttonTemplate: 'odoo_unifi_manager.UnifiDashboard.Buttons',
};
registry.category("views").add("unifi_dashboard", unifiDashboardView);

View file

@ -0,0 +1,54 @@
odoo.define('odoo_unifi_manager.firewall_rule_wizard', function (require) {
"use strict";
var FormController = require('web.FormController');
var FormView = require('web.FormView');
var viewRegistry = require('web.view_registry');
var FirewallRuleWizardController = FormController.extend({
_onFieldChanged: function (event) {
var self = this;
var fieldName = event.name;
if (fieldName === 'template_id') {
var templateId = event.data.changes.template_id.id;
if (templateId) {
// Récupérer les données du template
this._rpc({
model: 'unifi.firewall.rule.template',
method: 'read',
args: [[templateId], ['action', 'src_ip', 'dst_ip', 'protocol', 'port']]
}).then(function (result) {
if (result && result.length > 0) {
var template = result[0];
// Mettre à jour les champs seulement si ils ont des valeurs
var changes = {};
if (template.action) changes.action = template.action;
if (template.src_ip) changes.src_ip = template.src_ip;
if (template.dst_ip) changes.dst_ip = template.dst_ip;
if (template.protocol) changes.protocol = template.protocol;
if (template.port) changes.port = template.port;
// Appliquer les changements au modèle
self.model.setData(changes);
}
});
}
}
return this._super.apply(this, arguments);
},
});
var FirewallRuleWizardFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: FirewallRuleWizardController,
}),
});
viewRegistry.add('firewall_rule_wizard_form', FirewallRuleWizardFormView);
return {
FirewallRuleWizardController: FirewallRuleWizardController,
FirewallRuleWizardFormView: FirewallRuleWizardFormView,
};
});

View file

@ -0,0 +1,86 @@
.o_unifi_dashboard {
padding: 16px;
background-color: #f8f9fa;
.o_unifi_dashboard_header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
h1 {
margin: 0;
color: #212529;
}
}
.o_unifi_dashboard_stats,
.o_unifi_dashboard_status,
.o_unifi_dashboard_security {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.o_stat_box {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease;
&:hover {
transform: translateY(-2px);
}
.o_stat_title {
color: #6c757d;
font-size: 14px;
margin-bottom: 8px;
}
.o_stat_value {
color: #212529;
font-size: 24px;
font-weight: 600;
}
}
.o_unifi_dashboard_charts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 24px;
margin-bottom: 24px;
.o_chart_container {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
h3 {
margin-bottom: 16px;
color: #212529;
}
canvas {
width: 100% !important;
height: 300px !important;
}
}
}
// Responsive adjustments
@media (max-width: 768px) {
.o_unifi_dashboard_charts {
grid-template-columns: 1fr;
}
.o_stat_box {
.o_stat_value {
font-size: 20px;
}
}
}
}

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="odoo_unifi_manager.UnifiDashboard" owl="1">
<div class="o_kanban_renderer o_unifi_dashboard" t-att-class="props.class">
<!-- Header with refresh button -->
<div class="o_unifi_dashboard_header">
<h1>UniFi Network Dashboard</h1>
<button class="btn btn-primary" t-on-click="() => props.onRefreshDashboard()">
<i class="fa fa-refresh"/> Refresh
</button>
</div>
<!-- Main stats grid -->
<div class="o_unifi_dashboard_stats">
<div t-foreach="props.list.records" t-as="record" t-key="record.id">
<div class="o_stat_box">
<div class="o_stat_title">Total Clients</div>
<div class="o_stat_value"><t t-esc="record.data.total_clients"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Active Clients</div>
<div class="o_stat_value"><t t-esc="record.data.active_clients"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Total Devices</div>
<div class="o_stat_value"><t t-esc="record.data.total_devices"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Offline Devices</div>
<div class="o_stat_value"><t t-esc="record.data.offline_devices"/></div>
</div>
</div>
</div>
<!-- Charts section -->
<div class="o_unifi_dashboard_charts">
<div class="o_chart_container">
<h3>Client Types</h3>
<canvas id="clientTypeChart"/>
</div>
<div class="o_chart_container">
<h3>Bandwidth Usage</h3>
<canvas id="bandwidthChart"/>
</div>
</div>
<!-- Network status -->
<div class="o_unifi_dashboard_status">
<div t-foreach="props.list.records" t-as="record" t-key="record.id">
<div class="o_stat_box">
<div class="o_stat_title">Bandwidth Usage</div>
<div class="o_stat_value"><t t-esc="record.data.bandwidth_usage"/> Mbps</div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Active Alerts</div>
<div class="o_stat_value"><t t-esc="record.data.active_alerts"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Critical Alerts</div>
<div class="o_stat_value"><t t-esc="record.data.critical_alerts"/></div>
</div>
</div>
</div>
<!-- Security section -->
<div class="o_unifi_dashboard_security">
<div t-foreach="props.list.records" t-as="record" t-key="record.id">
<div class="o_stat_box">
<div class="o_stat_title">Active Firewall Rules</div>
<div class="o_stat_value"><t t-esc="record.data.active_firewall_rules"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Blocked Connections</div>
<div class="o_stat_value"><t t-esc="record.data.blocked_connections"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">VPN Status</div>
<div class="o_stat_value"><t t-esc="record.data.vpn_status"/></div>
</div>
<div class="o_stat_box">
<div class="o_stat_title">Threats Detected</div>
<div class="o_stat_value"><t t-esc="record.data.threat_count"/></div>
</div>
</div>
</div>
</div>
</t>
<t t-name="odoo_unifi_manager.UnifiDashboard.Buttons" owl="1">
<div class="o_cp_buttons">
<button class="btn btn-primary" t-on-click="() => props.onRefreshDashboard()">
<i class="fa fa-refresh"/> Refresh Dashboard
</button>
</div>
</t>
</templates>

View file

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="UnifiDashboard">
<div class="o_unifi_dashboard">
<!-- Header -->
<div class="o_unifi_dashboard_header">
<h1>UniFi Dashboard</h1>
<button class="btn btn-primary o_unifi_dashboard_refresh">
<i class="fa fa-refresh"/> Refresh
</button>
</div>
<!-- Main Stats -->
<div class="row mt-3">
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Clients</h5>
<div class="d-flex justify-content-between">
<div class="metric">
<span t-esc="data.total_clients"/>
</div>
<div class="text-success">
<span t-esc="data.active_clients"/> active
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Devices</h5>
<div class="d-flex justify-content-between">
<div class="metric">
<span t-esc="data.total_devices"/>
</div>
<div class="text-danger">
<span t-esc="data.offline_devices"/> offline
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Bandwidth</h5>
<div class="d-flex justify-content-between">
<div class="metric">
<span t-esc="data.total_bandwidth"/> Mbps
</div>
<div>
<span t-esc="data.bandwidth_usage"/>%
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card">
<div class="card-body">
<h5 class="card-title">Alerts</h5>
<div class="d-flex justify-content-between">
<div class="metric">
<span t-esc="data.active_alerts"/>
</div>
<div class="text-danger">
<span t-esc="data.critical_alerts"/> critical
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Charts -->
<div class="row mt-4">
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Client Distribution</h5>
<div class="chart-container">
<canvas id="clientTypeChart"/>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-body">
<h5 class="card-title">Bandwidth Usage (24h)</h5>
<div class="chart-container">
<canvas id="bandwidthChart"/>
</div>
</div>
</div>
</div>
</div>
<!-- Security Stats -->
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">Security Overview</h5>
<div class="row">
<div class="col-md-3">
<div class="security-stat">
<label>Firewall Rules</label>
<div class="metric">
<span t-esc="data.active_firewall_rules"/>
</div>
</div>
</div>
<div class="col-md-3">
<div class="security-stat">
<label>Blocked Connections</label>
<div class="metric">
<span t-esc="data.blocked_connections"/>
</div>
</div>
</div>
<div class="col-md-3">
<div class="security-stat">
<label>VPN Status</label>
<div class="metric">
<t t-if="data.vpn_status == 'up'">
<span class="badge badge-success">Up</span>
</t>
<t t-elif="data.vpn_status == 'down'">
<span class="badge badge-danger">Down</span>
</t>
<t t-else="">
<span class="badge badge-warning">Partial</span>
</t>
</div>
</div>
</div>
<div class="col-md-3">
<div class="security-stat">
<label>Threats Detected</label>
<div class="metric">
<span t-esc="data.threat_count"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1 @@
from . import test_unifi_client

View file

@ -0,0 +1,142 @@
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from unittest.mock import patch, MagicMock
from .models.unifi_client import UnifiApiClient
class TestUnifiClient(TransactionCase):
def setUp(self):
super().setUp()
self.test_host = "192.168.1.1"
self.test_username = "admin"
self.test_password = "password"
def test_init_valid_params(self):
"""Test initialization with valid parameters"""
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password
)
self.assertEqual(client.host, self.test_host)
self.assertEqual(client.username, self.test_username)
self.assertEqual(client.password, self.test_password)
def test_init_invalid_host(self):
"""Test initialization with invalid host"""
with self.assertRaises(ValidationError):
UnifiApiClient(
host="invalid host",
username=self.test_username,
password=self.test_password
)
def test_init_missing_credentials(self):
"""Test initialization with missing credentials"""
with self.assertRaises(ValidationError):
UnifiApiClient(
host=self.test_host,
username="",
password=self.test_password
)
with self.assertRaises(ValidationError):
UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=""
)
@patch('requests.Session')
def test_login_success(self, mock_session):
"""Test successful login"""
mock_response = MagicMock()
mock_response.ok = True
mock_response.headers = {}
mock_session.return_value.post.return_value = mock_response
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password
)
self.assertTrue(client.login())
@patch('requests.Session')
def test_login_failure(self, mock_session):
"""Test login failure"""
mock_response = MagicMock()
mock_response.ok = False
mock_response.text = "Invalid credentials"
mock_session.return_value.post.return_value = mock_response
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password
)
with self.assertRaises(ValidationError):
client.login()
@patch('requests.Session')
def test_get_networks(self, mock_session):
"""Test getting networks"""
mock_response = MagicMock()
mock_response.ok = True
mock_response.json.return_value = {
'data': [{
'_id': 'test_id',
'name': 'Test Network',
'subnet': '192.168.1.0/24',
'vlan': 10
}]
}
mock_session.return_value.get.return_value = mock_response
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password
)
networks = client.get_networks()
self.assertEqual(len(networks), 1)
self.assertEqual(networks[0]['name'], 'Test Network')
@patch('requests.Session')
def test_rate_limits(self, mock_session):
"""Test rate limit handling"""
mock_response = MagicMock()
mock_response.ok = True
mock_response.headers = {
'X-RateLimit-Remaining': '0',
'X-RateLimit-Reset': '60'
}
mock_session.return_value.get.return_value = mock_response
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password
)
client.get_networks()
self.assertEqual(client._rate_limit_remaining, 0)
self.assertEqual(client._rate_limit_reset, 60)
@patch('requests.Session')
def test_ssl_verification(self, mock_session):
"""Test SSL verification settings"""
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password,
verify_ssl=True
)
self.assertTrue(client.verify_ssl)
client = UnifiApiClient(
host=self.test_host,
username=self.test_username,
password=self.test_password,
verify_ssl=False
)
self.assertFalse(client.verify_ssl)

View file

@ -1,48 +0,0 @@
<odoo>
<!-- Action pour afficher les contrôleurs -->
<record id="action_unifi_ctrls_list" model="ir.actions.act_window">
<field name="name">Controllers</field>
<field name="res_model">unifi.ctrl</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Manage your Unifi Controllers here.
</p>
</field>
</record>
<!-- Vue Liste pour les contrôleurs -->
<record id="view_ctrl_tree" model="ir.ui.view">
<field name="name">unifi.ctrl.tree</field>
<field name="model">unifi.ctrl</field>
<field name="arch" type="xml">
<tree string="Controllers">
<field name="name"/>
<field name="ip_address"/>
<field name="api_port"/>
</tree>
</field>
</record>
<!-- Vue Formulaire pour les contrôleurs -->
<record id="view_ctrl_form" model="ir.ui.view">
<field name="name">unifi.ctrl.form</field>
<field name="model">unifi.ctrl</field>
<field name="arch" type="xml">
<form string="Controller">
<sheet>
<group>
<field name="name"/>
<field name="ip_address"/>
<field name="api_port"/>
<field name="username"/>
<field name="password" password="True"/>
<field name="controller_type"/>
<button name="action_test_connection" type="object" string="Test Connection" class="btn-primary"/>
<button name="action_fetch_networks_and_fill_rules" type="object" string="Fetch Networks and Create Rules" class="btn-secondary"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -1,48 +0,0 @@
<odoo>
<!-- Vue Liste pour les Templates -->
<record id="view_firewall_rule_template_tree" model="ir.ui.view">
<field name="name">firewall.rule.template.tree</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<tree string="Firewall Rule Templates">
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</tree>
</field>
</record>
<!-- Vue Formulaire pour les Templates -->
<record id="view_firewall_rule_template_form" model="ir.ui.view">
<field name="name">firewall.rule.template.form</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<form string="Firewall Rule Template">
<sheet>
<group>
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_firewall_templates_list" model="ir.actions.act_window">
<field name="name">Firewall Rule Templates</field>
<field name="res_model">unifi.firewall.rule.template</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create and manage templates for your firewall rules here.
</p>
</field>
</record>
</odoo>

View file

@ -1,16 +0,0 @@
<odoo>
<record id="view_firewall_rule_tree" model="ir.ui.view">
<field name="name">firewall.rule.tree</field>
<field name="model">unifi.firewall.rule</field>
<field name="arch" type="xml">
<tree string="Firewall Rules">
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</tree>
</field>
</record>
</odoo>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Dashboard Action -->
<record id="action_unifi_dashboard" model="ir.actions.act_window">
<field name="name">UniFi Dashboard</field>
<field name="res_model">unifi.ctrl</field>
<field name="view_mode">kanban</field>
<field name="view_id" ref="unifi_dashboard_view"/>
<field name="target">main</field>
</record>
<!-- Client Actions -->
<record id="action_unifi_clients_list" model="ir.actions.act_window">
<field name="name">UniFi Clients</field>
<field name="res_model">unifi.client</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Network Actions -->
<record id="action_unifi_networks_list" model="ir.actions.act_window">
<field name="name">UniFi Networks</field>
<field name="res_model">unifi.network</field>
<field name="view_mode">tree,form</field>
</record>
<!-- WiFi Actions -->
<record id="action_unifi_wifi_list" model="ir.actions.act_window">
<field name="name">UniFi WiFi Networks</field>
<field name="res_model">unifi.wifi</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Device Actions -->
<record id="action_unifi_device" model="ir.actions.act_window">
<field name="name">UniFi Devices</field>
<field name="res_model">unifi.device</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Firewall Group Actions -->
<record id="action_unifi_firewall_group" model="ir.actions.act_window">
<field name="name">UniFi Firewall Groups</field>
<field name="res_model">unifi.firewall.group</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Firewall Rule Actions -->
<record id="action_unifi_firewall_rules_list" model="ir.actions.act_window">
<field name="name">UniFi Firewall Rules</field>
<field name="res_model">unifi.firewall.rule</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Port Forward Actions -->
<record id="action_unifi_port_forward" model="ir.actions.act_window">
<field name="name">UniFi Port Forward Rules</field>
<field name="res_model">unifi.port.forward</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Site Actions -->
<record id="action_unifi_site" model="ir.actions.act_window">
<field name="name">UniFi Sites</field>
<field name="res_model">unifi.site</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Controller Actions -->
<record id="action_unifi_controller" model="ir.actions.act_window">
<field name="name">UniFi Controllers</field>
<field name="res_model">unifi.ctrl</field>
<field name="view_mode">tree,form</field>
</record>
<!-- Firewall Rule Template Actions -->
<record id="action_unifi_firewall_rule_template" model="ir.actions.act_window">
<field name="name">UniFi Firewall Rule Templates</field>
<field name="res_model">unifi.firewall.rule.template</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_unifi_client_tree" model="ir.ui.view">
<field name="name">unifi.client.tree</field>
<field name="model">unifi.client</field>
<field name="arch" type="xml">
<tree string="UniFi Clients" decoration-success="status=='online'" decoration-danger="status=='offline'">
<field name="display_name"/>
<field name="last_ip"/>
<field name="status"/>
<field name="network"/>
<field name="mac" optional="show"/>
<field name="is_wired" optional="show"/>
<field name="hostname" optional="hide"/>
<field name="device_name" optional="hide"/>
<field name="satisfaction" optional="show"/>
<field name="signal" optional="hide"/>
<field name="last_seen" optional="hide"/>
<field name="dev_cat" optional="hide"/>
<field name="os_name" optional="hide"/>
<field name="controller_id" optional="hide"/>
<field name="site_id" optional="hide"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_unifi_client_form" model="ir.ui.view">
<field name="name">unifi.client.form</field>
<field name="model">unifi.client</field>
<field name="arch" type="xml">
<form string="UniFi Client">
<header>
<field name="status" widget="statusbar"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="display_name"/>
</h1>
</div>
<group>
<group string="Basic Information">
<field name="name"/>
<field name="hostname"/>
<field name="device_name"/>
<field name="mac"/>
<field name="last_ip"/>
<field name="controller_id"/>
<field name="last_seen"/>
</group>
<group string="Device Information">
<field name="oui"/>
<field name="dev_cat"/>
<field name="dev_family"/>
<field name="dev_vendor"/>
<field name="os_name"/>
<field name="device_id"/>
</group>
</group>
<notebook>
<page string="Network Connection" name="network">
<group>
<group string="Connection Details">
<field name="is_wired"/>
<field name="is_guest"/>
<field name="network"/>
<field name="network_id"/>
<field name="site_id"/>
<field name="uptime" widget="float_time"/>
</group>
<group string="Wireless Details" invisible="is_wired">
<field name="essid"/>
<field name="bssid"/>
<field name="ap_mac"/>
<field name="channel"/>
<field name="channel_width"/>
<field name="radio"/>
<field name="radio_proto"/>
</group>
</group>
</page>
<page string="Performance" name="performance">
<group>
<group string="Signal Quality" invisible="is_wired">
<field name="satisfaction"/>
<field name="signal"/>
<field name="noise"/>
</group>
<group string="Transfer Rates">
<field name="tx_rate"/>
<field name="rx_rate"/>
<field name="bytes_r"/>
</group>
<group string="Data Transfer">
<field name="tx_bytes" widget="float_time"/>
<field name="rx_bytes" widget="float_time"/>
</group>
<group string="Wireless Statistics" invisible="is_wired">
<field name="tx_retries"/>
<field name="wifi_tx_attempts"/>
<field name="wifi_tx_dropped"/>
<field name="tx_retries_percentage" widget="percentage"/>
</group>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_unifi_client_search" model="ir.ui.view">
<field name="name">unifi.client.search</field>
<field name="model">unifi.client</field>
<field name="arch" type="xml">
<search string="Search UniFi Clients">
<field name="name"/>
<field name="hostname"/>
<field name="device_name"/>
<field name="mac"/>
<field name="last_ip"/>
<field name="network"/>
<field name="essid"/>
<separator/>
<filter string="Online" name="online" domain="[('status', '=', 'online')]"/>
<filter string="Offline" name="offline" domain="[('status', '=', 'offline')]"/>
<separator/>
<filter string="Wired" name="wired" domain="[('is_wired', '=', True)]"/>
<filter string="Wireless" name="wireless" domain="[('is_wired', '=', False)]"/>
<filter string="Guest" name="guest" domain="[('is_guest', '=', True)]"/>
<separator/>
<filter string="Low Signal" name="low_signal"
domain="[('is_wired', '=', False), ('signal', '&lt;', -70)]"/>
<filter string="Poor Satisfaction" name="poor_satisfaction"
domain="[('satisfaction', '&lt;', 70)]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Network" name="group_network" context="{'group_by': 'network'}"/>
<filter string="Connection Type" name="group_connection" context="{'group_by': 'is_wired'}"/>
<filter string="Device Category" name="group_category" context="{'group_by': 'dev_cat'}"/>
<filter string="Operating System" name="group_os" context="{'group_by': 'os_name'}"/>
<filter string="Controller" name="group_controller" context="{'group_by': 'controller_id'}"/>
<filter string="Site" name="group_site" context="{'group_by': 'site_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -0,0 +1,53 @@
<odoo>
<!-- Vue Liste pour les contrôleurs -->
<record id="view_ctrl_tree" model="ir.ui.view">
<field name="name">unifi.ctrl.tree</field>
<field name="model">unifi.ctrl</field>
<field name="arch" type="xml">
<tree string="UniFi Controllers">
<field name="name"/>
<field name="ip_address"/>
<field name="api_port"/>
<field name="username"/>
<field name="controller_type"/>
</tree>
</field>
</record>
<!-- Vue Formulaire pour les contrôleurs -->
<record id="view_ctrl_form" model="ir.ui.view">
<field name="name">unifi.ctrl.form</field>
<field name="model">unifi.ctrl</field>
<field name="arch" type="xml">
<form string="UniFi Controller">
<header>
<button name="action_test_connection" type="object" string="Test Connection" class="oe_highlight"/>
<button name="action_sync_all" type="object" string="Sync All" class="oe_highlight"/>
<button name="action_sync_devices" type="object" string="Sync Devices" class="btn-secondary"/>
<button name="action_sync_clients" type="object" string="Sync Clients" class="btn-secondary"/>
<button name="action_sync_networks" type="object" string="Sync Networks" class="btn-secondary"/>
<button name="action_sync_wifi" type="object" string="Sync WiFi" class="btn-secondary"/>
<button name="action_sync_sites" type="object" string="Sync Sites" class="btn-secondary"/>
<button name="action_sync_firewall_rules" type="object" string="Sync Firewall Rules" class="btn-secondary"/>
<button name="action_sync_port_forward" type="object" string="Sync Port Forward Rules" class="oe_highlight"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="ip_address"/>
<field name="api_port"/>
</group>
<group>
<field name="username"/>
<field name="password" password="True"/>
<field name="controller_type"/>
<field name="verify_ssl"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="unifi_dashboard_view" model="ir.ui.view">
<field name="name">unifi.dashboard.view</field>
<field name="model">unifi.ctrl</field>
<field name="arch" type="xml">
<kanban class="o_unifi_dashboard" create="false" js_class="unifi_dashboard">
<field name="name"/>
<field name="total_clients"/>
<field name="active_clients"/>
<field name="total_devices"/>
<field name="offline_devices"/>
<field name="total_bandwidth"/>
<field name="bandwidth_usage"/>
<field name="active_alerts"/>
<field name="critical_alerts"/>
<field name="client_type_chart"/>
<field name="bandwidth_chart"/>
<field name="active_firewall_rules"/>
<field name="blocked_connections"/>
<field name="vpn_status"/>
<field name="threat_count"/>
<templates>
<t t-name="kanban-box">
<div class="o_unifi_dashboard_content"/>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_unifi_dashboard" model="ir.actions.act_window">
<field name="name">UniFi Dashboard</field>
<field name="res_model">unifi.ctrl</field>
<field name="view_mode">kanban</field>
<field name="view_id" ref="unifi_dashboard_view"/>
<field name="target">main</field>
</record>
<!-- Menu item -->
<menuitem
id="menu_unifi_dashboard"
name="Dashboard"
parent="unifi_menu_root"
action="action_unifi_dashboard"
sequence="1"/>
</odoo>

View file

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Device List View -->
<record id="view_unifi_device_tree" model="ir.ui.view">
<field name="name">unifi.device.tree</field>
<field name="model">unifi.device</field>
<field name="arch" type="xml">
<tree string="UniFi Devices">
<field name="name"/>
<field name="model"/>
<field name="ip_address"/>
<field name="status"/>
<field name="mac" optional="show"/>
<field name="type" optional="show"/>
<field name="version" optional="hide"/>
<field name="last_seen" optional="hide"/>
<field name="controller_id" optional="hide"/>
<field name="site_id" optional="hide"/>
</tree>
</field>
</record>
<!-- Device Form View -->
<record id="view_unifi_device_form" model="ir.ui.view">
<field name="name">unifi.device.form</field>
<field name="model">unifi.device</field>
<field name="arch" type="xml">
<form string="UniFi Device">
<sheet>
<group>
<group>
<field name="name"/>
<field name="mac"/>
<field name="model"/>
<field name="type"/>
</group>
<group>
<field name="ip_address"/>
<field name="version"/>
<field name="status"/>
<field name="last_seen"/>
<field name="controller_id"/>
<field name="site_id"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Device Search View -->
<record id="view_unifi_device_search" model="ir.ui.view">
<field name="name">unifi.device.search</field>
<field name="model">unifi.device</field>
<field name="arch" type="xml">
<search string="Search UniFi Devices">
<field name="name"/>
<field name="mac"/>
<field name="ip_address"/>
<field name="model"/>
<separator/>
<filter string="Online" name="online" domain="[('status', '=', 'online')]"/>
<filter string="Offline" name="offline" domain="[('status', '=', 'offline')]"/>
<group expand="0" string="Group By">
<filter string="Status" name="group_status" context="{'group_by': 'status'}"/>
<filter string="Type" name="group_type" context="{'group_by': 'type'}"/>
<filter string="Model" name="group_model" context="{'group_by': 'model'}"/>
<filter string="Controller" name="group_controller" context="{'group_by': 'controller_id'}"/>
<filter string="Site" name="group_site" context="{'group_by': 'site_id'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_unifi_firewall_group_tree" model="ir.ui.view">
<field name="name">unifi.firewall.group.tree</field>
<field name="model">unifi.firewall.group</field>
<field name="arch" type="xml">
<tree string="Firewall Groups">
<field name="name"/>
<field name="group_type"/>
<field name="ctrl_id"/>
<field name="description"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_unifi_firewall_group_form" model="ir.ui.view">
<field name="name">unifi.firewall.group.form</field>
<field name="model">unifi.firewall.group</field>
<field name="arch" type="xml">
<form string="Firewall Group">
<header>
<button name="copy_to_controller"
string="Apply to Controller"
type="object"
class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Group Name"/>
</h1>
</div>
<group>
<group>
<field name="group_type"/>
<field name="ctrl_id"/>
</group>
<group>
<field name="description"/>
</group>
</group>
<notebook>
<page string="Members" name="members">
<field name="members" placeholder="One entry per line..."/>
</page>
<page string="Associated Rules" name="rules">
<field name="rule_ids"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_unifi_firewall_group_search" model="ir.ui.view">
<field name="name">unifi.firewall.group.search</field>
<field name="model">unifi.firewall.group</field>
<field name="arch" type="xml">
<search string="Search Firewall Groups">
<field name="name"/>
<field name="ctrl_id"/>
<field name="group_type"/>
<field name="description"/>
<group expand="0" string="Group By">
<filter string="Controller" name="group_by_controller" context="{'group_by': 'ctrl_id'}"/>
<filter string="Group Type" name="group_by_type" context="{'group_by': 'group_type'}"/>
</group>
</search>
</field>
</record>
<!-- Action Window -->
<record id="action_unifi_firewall_group" model="ir.actions.act_window">
<field name="name">Firewall Groups</field>
<field name="res_model">unifi.firewall.group</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_unifi_firewall_group_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first firewall group!
</p>
<p>
Firewall groups allow you to manage collections of IP addresses or ports that can be referenced in firewall rules.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_unifi_firewall_group"
name="Firewall Groups"
parent="menu_unifi_security"
action="action_unifi_firewall_group"
sequence="10"/>
</odoo>

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Vue Liste pour les Templates de Règles de Pare-feu -->
<record id="view_firewall_rule_template_tree" model="ir.ui.view">
<field name="name">firewall.rule.template.tree</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<tree string="Firewall Rule Templates">
<field name="rule_index" widget="handle"/>
<field name="name"/>
<field name="enabled"/>
<field name="ruleset"/>
<field name="protocol"/>
<field name="port"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="action"/>
<field name="ctrl_id"/>
</tree>
</field>
</record>
<!-- Vue Formulaire pour les Templates de Règles de Pare-feu -->
<record id="view_firewall_rule_template_form" model="ir.ui.view">
<field name="name">firewall.rule.template.form</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<form string="Firewall Rule Template">
<header>
<button name="copy_to_controller"
string="Apply to Controller"
type="object"
class="oe_highlight"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" placeholder="Rule Name"/>
</h1>
</div>
<group>
<group>
<field name="enabled"/>
<field name="ruleset"/>
<field name="rule_index"/>
<field name="protocol"/>
<field name="port" invisible="protocol == 'icmp'"/>
<field name="action"/>
</group>
<group>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="ctrl_id"/>
<field name="firewall_group_id"/>
</group>
</group>
<notebook>
<page string="DPI Categories" name="dpi_categories">
<field name="categories" widget="many2many_tags"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Vue Recherche -->
<record id="view_firewall_rule_template_search" model="ir.ui.view">
<field name="name">firewall.rule.template.search</field>
<field name="model">unifi.firewall.rule.template</field>
<field name="arch" type="xml">
<search string="Search Firewall Rules">
<field name="name"/>
<field name="ctrl_id"/>
<field name="ruleset"/>
<field name="protocol"/>
<field name="port"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<separator/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Disabled" name="disabled" domain="[('enabled', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Controller" name="group_by_controller" context="{'group_by': 'ctrl_id'}"/>
<filter string="Rule Set" name="group_by_ruleset" context="{'group_by': 'ruleset'}"/>
<filter string="Protocol" name="group_by_protocol" context="{'group_by': 'protocol'}"/>
<filter string="Action" name="group_by_action" context="{'group_by': 'action'}"/>
</group>
</search>
</field>
</record>
<!-- Action Window -->
<record id="action_firewall_rule_template" model="ir.actions.act_window">
<field name="name">Firewall Rule Templates</field>
<field name="res_model">unifi.firewall.rule.template</field>
<field name="view_mode">tree,form</field>
<field name="search_view_id" ref="view_firewall_rule_template_search"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create your first firewall rule template!
</p>
<p>
Firewall rule templates allow you to define and manage rules that can be applied to your UniFi controllers.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_firewall_rule_template"
name="Rule Templates"
parent="menu_unifi_security"
action="action_unifi_firewall_rule_template"
sequence="30"/>
</odoo>

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_firewall_rule_tree" model="ir.ui.view">
<field name="name">firewall.rule.tree</field>
<field name="model">unifi.firewall.rule</field>
<field name="arch" type="xml">
<tree string="Firewall Rules">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="ruleset"/>
<field name="direction"/>
<field name="protocol"/>
<field name="src_network_id"/>
<field name="src_address"/>
<field name="dst_network_id"/>
<field name="dst_address"/>
<field name="action"/>
<field name="enabled"/>
<field name="controller_id"/>
</tree>
</field>
</record>
<record id="view_firewall_rule_form" model="ir.ui.view">
<field name="name">firewall.rule.form</field>
<field name="model">unifi.firewall.rule</field>
<field name="arch" type="xml">
<form string="Firewall Rule">
<sheet>
<group>
<group>
<field name="name"/>
<field name="enabled"/>
<field name="sequence"/>
<field name="protocol"/>
<field name="action"/>
<field name="direction"/>
<field name="ruleset"/>
<field name="rule_index"/>
</group>
<group>
<field name="src_network_id"/>
<field name="src_address"/>
<field name="src_port" invisible="protocol not in ['tcp', 'udp']"/>
<field name="dst_network_id"/>
<field name="dst_address"/>
<field name="dst_port" invisible="protocol not in ['tcp', 'udp']"/>
<field name="icmp_type" invisible="protocol != 'icmp'"/>
</group>
</group>
<group string="Connection States">
<group>
<field name="state_new"/>
<field name="state_established"/>
</group>
<group>
<field name="state_invalid"/>
<field name="state_related"/>
</group>
</group>
<group>
<field name="description"/>
<field name="controller_id"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_backend" name="odoo_unifi_manager assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/odoo_unifi_manager/static/src/js/unifi_firewall_rule_wizard.js"/>
</xpath>
</template>
<record id="view_firewall_rule_wizard_form" model="ir.ui.view">
<field name="name">unifi.firewall.rule.wizard.form</field>
<field name="model">unifi.firewall.rule.wizard</field>
<field name="arch" type="xml">
<form js_class="firewall_rule_wizard_form">
<group>
<field name="template_id"/>
<field name="name"/>
<field name="action"/>
<field name="src_ip"/>
<field name="dst_ip"/>
<field name="protocol"/>
<field name="port"/>
</group>
<footer>
<button string="Create Rule" name="create_firewall_rule" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_firewall_rule_wizard" model="ir.actions.act_window">
<field name="name">Create Firewall Rule</field>
<field name="res_model">unifi.firewall.rule.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Menu Principal -->
<menuitem id="unifi_menu_root"
name="Unifi"
sequence="90"
web_icon="odoo_unifi_manager,static/description/icon.png"
groups="base.group_system"/>
<!-- Dashboard -->
<menuitem
id="menu_unifi_dashboard"
name="Dashboard"
parent="unifi_menu_root"
action="action_unifi_dashboard"
sequence="1"/>
<!-- Overview Section -->
<menuitem
id="menu_unifi_overview"
name="Overview"
parent="unifi_menu_root"
sequence="5"/>
<!-- Administration Section -->
<menuitem
id="menu_unifi_administration"
name="Administration"
parent="unifi_menu_root"
sequence="10">
<menuitem id="menu_unifi_controller"
name="Controllers"
action="action_unifi_controller"
sequence="5"/>
<menuitem id="menu_unifi_clients"
name="Clients"
action="action_unifi_clients_list"
sequence="10"/>
<menuitem id="menu_unifi_device"
name="Devices"
action="action_unifi_device"
sequence="15"/>
<menuitem id="menu_unifi_site"
name="Sites"
action="action_unifi_site"
sequence="18"/>
</menuitem>
<!-- Network Configuration -->
<menuitem
id="menu_unifi_network_config"
name="Network"
parent="unifi_menu_root"
sequence="20">
<menuitem id="menu_unifi_networks"
name="Networks"
action="action_unifi_networks_list"
sequence="10"/>
<menuitem id="menu_unifi_wifi"
name="WiFi Networks"
action="action_unifi_wifi_list"
sequence="20"/>
<menuitem id="menu_unifi_port_forward"
name="Port Forward"
action="action_unifi_port_forward"
sequence="30"/>
</menuitem>
<!-- Security Configuration -->
<menuitem
id="menu_unifi_security"
name="Security"
parent="unifi_menu_root"
sequence="30">
<menuitem id="menu_unifi_firewall_group"
name="Firewall Groups"
action="action_unifi_firewall_group"
sequence="10"/>
<menuitem id="menu_unifi_firewall_rules"
name="Firewall Rules"
action="action_unifi_firewall_rules_list"
sequence="20"/>
<menuitem id="menu_unifi_firewall_templates"
name="Rule Templates"
action="action_unifi_firewall_rule_template"
sequence="30"/>
</menuitem>
</odoo>

View file

@ -9,6 +9,8 @@
<field name="cidr"/>
<field name="gateway"/>
<field name="vlan_id"/>
<field name="ctrl_id"/>
<field name="last_sync"/>
</tree>
</field>
</record>
@ -21,26 +23,20 @@
<form string="Network">
<sheet>
<group>
<field name="name"/>
<field name="cidr"/>
<field name="gateway"/>
<field name="vlan_id"/>
<field name="description"/>
<group>
<field name="name"/>
<field name="cidr"/>
<field name="gateway"/>
</group>
<group>
<field name="vlan_id"/>
<field name="ctrl_id"/>
<field name="description"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_network_list" model="ir.actions.act_window">
<field name="name">Networks</field>
<field name="res_model">unifi.network</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create and manage your networks here.
</p>
</field>
</record>
</odoo>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_unifi_port_forward_tree" model="ir.ui.view">
<field name="name">unifi.port.forward.tree</field>
<field name="model">unifi.port.forward</field>
<field name="arch" type="xml">
<tree string="Port Forward Rules">
<field name="name"/>
<field name="src"/>
<field name="dst"/>
<field name="dst_port"/>
<field name="protocol"/>
<field name="fwd_ip"/>
<field name="fwd_port"/>
<field name="log"/>
<field name="enabled"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_unifi_port_forward_form" model="ir.ui.view">
<field name="name">unifi.port.forward.form</field>
<field name="model">unifi.port.forward</field>
<field name="arch" type="xml">
<form string="Port Forward Rule">
<sheet>
<group>
<group>
<field name="name"/>
<field name="enabled"/>
<field name="fwd_ip"/>
<field name="fwd_port"/>
<field name="log"/>
</group>
<group>
<field name="protocol"/>
<field name="src"/>
<field name="dst"/>
<field name="dst_port"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_unifi_port_forward_search" model="ir.ui.view">
<field name="name">unifi.port.forward.search</field>
<field name="model">unifi.port.forward</field>
<field name="arch" type="xml">
<search string="Port Forward Rules">
<field name="name"/>
<field name="dst_port"/>
<field name="fwd_ip"/>
<field name="protocol"/>
<separator/>
<filter string="Enabled" name="enabled" domain="[('enabled', '=', True)]"/>
<filter string="Disabled" name="disabled" domain="[('enabled', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Protocol" name="group_protocol" context="{'group_by': 'protocol'}"/>
<filter string="Status" name="group_enabled" context="{'group_by': 'enabled'}"/>
</group>
</search>
</field>
</record>
</odoo>

View file

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="view_unifi_site_tree" model="ir.ui.view">
<field name="name">unifi.site.tree</field>
<field name="model">unifi.site</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="site_id"/>
<field name="controller_id"/>
<field name="active"/>
</tree>
</field>
</record>
<!-- Form View -->
<record id="view_unifi_site_form" model="ir.ui.view">
<field name="name">unifi.site.form</field>
<field name="model">unifi.site</field>
<field name="arch" type="xml">
<form>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object" class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"/>
</button>
</div>
<group>
<group>
<field name="name"/>
<field name="site_id"/>
<field name="controller_id"/>
</group>
<group>
<field name="description"/>
</group>
</group>
<notebook>
<page string="Devices" name="devices">
<field name="device_ids" readonly="1">
<tree>
<field name="name"/>
<field name="model"/>
<field name="type"/>
<field name="status"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="view_unifi_site_search" model="ir.ui.view">
<field name="name">unifi.site.search</field>
<field name="model">unifi.site</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="site_id"/>
<field name="controller_id"/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
<filter string="Controller" name="group_by_controller" domain="[]" context="{'group_by': 'controller_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_unifi_site" model="ir.actions.act_window">
<field name="name">UniFi Sites</field>
<field name="res_model">unifi.site</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No UniFi sites found.
</p>
<p>
Sites will be automatically synchronized from your UniFi controllers.
</p>
</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_unifi_site"
name="Sites"
parent="menu_unifi_administration"
action="action_unifi_site"
sequence="20"/>
</odoo>

View file

@ -8,6 +8,8 @@
<field name="name"/>
<field name="security_mode"/>
<field name="vlan_id"/>
<field name="ctrl_id"/>
<field name="last_sync"/>
</tree>
</field>
</record>
@ -20,26 +22,20 @@
<form string="WiFi Network">
<sheet>
<group>
<field name="name"/>
<field name="security_mode"/>
<field name="password"/>
<field name="vlan_id"/>
<field name="description"/>
<group>
<field name="name"/>
<field name="security_mode"/>
<field name="password" password="True"/>
</group>
<group>
<field name="vlan_id"/>
<field name="ctrl_id"/>
<field name="description"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_wifi_list" model="ir.actions.act_window">
<field name="name">WiFi Networks</field>
<field name="res_model">unifi.wifi</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create and manage your WiFi networks here.
</p>
</field>
</record>
</odoo>

View file

@ -1,61 +0,0 @@
<odoo>
<!-- Menu Principal -->
<menuitem id="unifi_menu_root"
name="Unifi"
sequence="90"
web_icon="odoo_unifi_manager,static/description/icon.png"
groups="base.group_system"
>
<menuitem
id="menu_unifi_overvier"
name="Overview"
sequence="5"
/>
<menuitem
id="menu_unifi_administration"
name="Administration"
sequence="10"
>
<menuitem id="menu_unifi_firewall_rules"
name="Firewall Rules"
action="action_firewall_rules_list"
sequence="20"
/>
<menuitem id="menu_unifi_networks"
name="Networks"
action="action_network_list"
sequence="30"
/>
<menuitem id="menu_unifi_wifi"
name="Wifi Networks"
action="action_wifi_list"
sequence="40"
/>
</menuitem>
<menuitem
id="menu_unifi_configuration"
name="Configuration"
sequence="20"
>
<menuitem id="menu_unifi_firewall_templates"
name="Templates"
action="action_firewall_templates_list"
sequence="10"
/>
<menuitem id="menu_unifi_controller"
name="Controllers"
action="action_unifi_ctrls_list"
sequence="20"
/>
</menuitem>
</menuitem>
</odoo>

View file

@ -1 +1 @@
from . import firewall_rule_wizard
from . import unifi_firewall_rule_wizard

View file

@ -55,31 +55,15 @@ class FirewallRuleWizard(models.TransientModel):
template = self.template_id
self.env['unifi.firewall.rule'].create({
'name': self.name,
'action': template.action,
'src_ip': template.src_ip,
'dst_ip': template.dst_ip,
'protocol': template.protocol,
'port': template.port,
'action': self.action,
'src_ip': self.src_ip,
'dst_ip': self.dst_ip,
'protocol': self.protocol,
'port': self.port,
})
@api.onchange('template_id')
def _onchange_template_id(self):
"""Apply template values only if they are not empty."""
if self.template_id:
# Appliquer les valeurs du modèle uniquement si elles ne sont pas vides
if self.template_id.action:
self.action = self.template_id.action
if self.template_id.src_ip:
self.src_ip = self.template_id.src_ip
if self.template_id.dst_ip:
self.dst_ip = self.template_id.dst_ip
if self.template_id.protocol:
self.protocol = self.template_id.protocol
if self.template_id.port:
self.port = self.template_id.port
@api.constrains('port')
def _check_port(self):
if self.port:
if not self.port.isdigit() and '-' not in self.port:
raise ValidationError("Port must be a number or a valid range (e.g., 80 or 8000-9000).")
raise ValidationError("Port must be a number or a valid range (e.g., 80 or 8000-9000).")