Unifi to test
This commit is contained in:
parent
010752b01f
commit
5050839056
56 changed files with 5086 additions and 643 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
@ -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)"
|
||||
)
|
||||
|
|
@ -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)"
|
||||
)
|
||||
|
|
@ -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."
|
||||
)
|
||||
619
odoo_unifi_manager/models/unifi_api_client.py
Normal file
619
odoo_unifi_manager/models/unifi_api_client.py
Normal 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')
|
||||
|
|
@ -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}")
|
||||
313
odoo_unifi_manager/models/unifi_client_model.py
Normal file
313
odoo_unifi_manager/models/unifi_client_model.py
Normal 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)}")
|
||||
966
odoo_unifi_manager/models/unifi_controller.py
Normal file
966
odoo_unifi_manager/models/unifi_controller.py
Normal 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
|
||||
69
odoo_unifi_manager/models/unifi_device.py
Normal file
69
odoo_unifi_manager/models/unifi_device.py
Normal 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!')
|
||||
]
|
||||
47
odoo_unifi_manager/models/unifi_dpi_category.py
Normal file
47
odoo_unifi_manager/models/unifi_dpi_category.py
Normal 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
|
||||
})
|
||||
93
odoo_unifi_manager/models/unifi_firewall_group.py
Normal file
93
odoo_unifi_manager/models/unifi_firewall_group.py
Normal 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)
|
||||
245
odoo_unifi_manager/models/unifi_firewall_rule.py
Normal file
245
odoo_unifi_manager/models/unifi_firewall_rule.py
Normal 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()
|
||||
})
|
||||
157
odoo_unifi_manager/models/unifi_firewall_rule_template.py
Normal file
157
odoo_unifi_manager/models/unifi_firewall_rule_template.py
Normal 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)
|
||||
72
odoo_unifi_manager/models/unifi_models.py
Normal file
72
odoo_unifi_manager/models/unifi_models.py
Normal 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
|
||||
234
odoo_unifi_manager/models/unifi_network.py
Normal file
234
odoo_unifi_manager/models/unifi_network.py
Normal 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()
|
||||
})
|
||||
210
odoo_unifi_manager/models/unifi_port_forward.py
Normal file
210
odoo_unifi_manager/models/unifi_port_forward.py
Normal 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
|
||||
}
|
||||
16
odoo_unifi_manager/models/unifi_site.py
Normal file
16
odoo_unifi_manager/models/unifi_site.py
Normal 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!')
|
||||
]
|
||||
54
odoo_unifi_manager/models/unifi_system_health.py
Normal file
54
odoo_unifi_manager/models/unifi_system_health.py
Normal 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)
|
||||
117
odoo_unifi_manager/models/unifi_wifi.py
Normal file
117
odoo_unifi_manager/models/unifi_wifi.py
Normal 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
|
||||
|
|
@ -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."
|
||||
)
|
||||
1
odoo_unifi_manager/requirements.txt
Normal file
1
odoo_unifi_manager/requirements.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
pyunifi==2.21.1
|
||||
|
|
@ -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
|
||||
|
26
odoo_unifi_manager/security/security.xml
Normal file
26
odoo_unifi_manager/security/security.xml
Normal 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>
|
||||
13
odoo_unifi_manager/static/lib/chart.js/Chart.bundle.min.js
vendored
Normal file
13
odoo_unifi_manager/static/lib/chart.js/Chart.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
90
odoo_unifi_manager/static/src/css/unifi_dashboard.css
Normal file
90
odoo_unifi_manager/static/src/css/unifi_dashboard.css
Normal 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;
|
||||
}
|
||||
}
|
||||
123
odoo_unifi_manager/static/src/js/dashboard_view.js
Normal file
123
odoo_unifi_manager/static/src/js/dashboard_view.js
Normal 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);
|
||||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
86
odoo_unifi_manager/static/src/scss/dashboard.scss
Normal file
86
odoo_unifi_manager/static/src/scss/dashboard.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
odoo_unifi_manager/static/src/xml/dashboard.xml
Normal file
96
odoo_unifi_manager/static/src/xml/dashboard.xml
Normal 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>
|
||||
155
odoo_unifi_manager/static/src/xml/dashboard_templates.xml
Normal file
155
odoo_unifi_manager/static/src/xml/dashboard_templates.xml
Normal 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>
|
||||
1
odoo_unifi_manager/tests/__init__.py
Normal file
1
odoo_unifi_manager/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_unifi_client
|
||||
142
odoo_unifi_manager/tests/test_unifi_client.py
Normal file
142
odoo_unifi_manager/tests/test_unifi_client.py
Normal 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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
81
odoo_unifi_manager/views/unifi_action.xml
Normal file
81
odoo_unifi_manager/views/unifi_action.xml
Normal 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>
|
||||
151
odoo_unifi_manager/views/unifi_client_views.xml
Normal file
151
odoo_unifi_manager/views/unifi_client_views.xml
Normal 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', '<', -70)]"/>
|
||||
<filter string="Poor Satisfaction" name="poor_satisfaction"
|
||||
domain="[('satisfaction', '<', 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>
|
||||
53
odoo_unifi_manager/views/unifi_controller_views.xml
Normal file
53
odoo_unifi_manager/views/unifi_controller_views.xml
Normal 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>
|
||||
47
odoo_unifi_manager/views/unifi_dashboard.xml
Normal file
47
odoo_unifi_manager/views/unifi_dashboard.xml
Normal 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>
|
||||
75
odoo_unifi_manager/views/unifi_device_views.xml
Normal file
75
odoo_unifi_manager/views/unifi_device_views.xml
Normal 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>
|
||||
98
odoo_unifi_manager/views/unifi_firewall_group_views.xml
Normal file
98
odoo_unifi_manager/views/unifi_firewall_group_views.xml
Normal 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>
|
||||
116
odoo_unifi_manager/views/unifi_firewall_rule_template_views.xml
Normal file
116
odoo_unifi_manager/views/unifi_firewall_rule_template_views.xml
Normal 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>
|
||||
70
odoo_unifi_manager/views/unifi_firewall_rule_views.xml
Normal file
70
odoo_unifi_manager/views/unifi_firewall_rule_views.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
99
odoo_unifi_manager/views/unifi_menu.xml
Normal file
99
odoo_unifi_manager/views/unifi_menu.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
69
odoo_unifi_manager/views/unifi_port_forward_views.xml
Normal file
69
odoo_unifi_manager/views/unifi_port_forward_views.xml
Normal 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>
|
||||
94
odoo_unifi_manager/views/unifi_site_views.xml
Normal file
94
odoo_unifi_manager/views/unifi_site_views.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1 +1 @@
|
|||
from . import firewall_rule_wizard
|
||||
from . import unifi_firewall_rule_wizard
|
||||
|
|
@ -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).")
|
||||
Loading…
Reference in a new issue