From 946ad00253b063bd88ab9740aeabc09614720a90 Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sat, 7 Feb 2026 13:44:50 +1100 Subject: [PATCH] FIX: lowercase MAC normalization across project v0.1 Signed-off-by: jokob-sk --- front/css/dark-patch.css | 92 +++--------------- front/css/system-dark-patch.css | 93 ------------------- front/deviceDetailsEdit.php | 4 +- front/network.php | 40 ++++---- front/plugins/ddns_update/script.py | 4 +- front/plugins/internet_ip/script.py | 8 +- front/plugins/omada_sdn_imp/omada_sdn.py | 6 +- front/plugins/omada_sdn_openapi/script.py | 4 +- front/plugins/plugin_helper.py | 26 +++--- .../unifi_api_import/unifi_api_import.py | 6 +- front/plugins/unifi_import/script.py | 2 +- scripts/generate-device-inventory.py | 12 +-- server/api_server/openapi/schemas.py | 6 +- server/database.py | 4 + server/db/db_upgrade.py | 44 +++++++++ server/initialise.py | 2 +- server/models/device_instance.py | 2 +- server/scan/device_handling.py | 6 +- server/scan/device_heuristics.py | 2 +- test/api_endpoints/test_device_endpoints.py | 4 +- .../test_device_update_normalization.py | 48 ++++++---- test/test_plugin_helper.py | 6 +- 22 files changed, 164 insertions(+), 257 deletions(-) diff --git a/front/css/dark-patch.css b/front/css/dark-patch.css index 93d43262..dbda3801 100755 --- a/front/css/dark-patch.css +++ b/front/css/dark-patch.css @@ -42,14 +42,7 @@ h4 { .content-header > .breadcrumb > li > a { color: #bec5cb; } -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - border-top: 0; -} + .table > thead > tr.odd, .table > tbody > tr.odd, .table > tfoot > tr.odd { @@ -73,7 +66,6 @@ h4 { border: 1px solid #353c42; } .dataTables_wrapper input[type="search"] { - border-radius: 4px; background-color: #353c42; border: 0; color: #bec5cb; @@ -126,7 +118,6 @@ h4 { border-color: #3c8dbc; } .sidebar-menu > li > .treeview-menu { - margin: 0 1px; background-color: #32393e; } .sidebar a { @@ -144,16 +135,13 @@ h4 { color: #fff; } .sidebar-form { - border-radius: 3px; border: 1px solid #3e464c; - margin: 10px; } .sidebar-form input[type="text"], .sidebar-form .btn { box-shadow: none; background-color: #3e464c; border: 1px solid transparent; - height: 35px; } .sidebar-form input[type="text"] { color: #666; @@ -207,20 +195,13 @@ h4 { .box > .box-header .btn { color: #bec5cb; } -.box.box-info, -.box.box-primary, -.box.box-success, -.box.box-warning, -.box.box-danger { - border-top-width: 3px; -} + .main-header .navbar { background-color: #272c30; } .main-header .navbar .nav > li > a, .main-header .navbar .nav > li > .navbar-text { color: #bec5cb; - max-height: 50px; } .main-header .navbar .nav > li > a:hover, .main-header .navbar .nav > li > a:active, @@ -277,7 +258,6 @@ h4 { background: rgba(64, 72, 80, 0.666); } .nav-tabs-custom > .nav-tabs > li { - margin-right: 1px; color: #bec5cb; } .nav-tabs-custom > .nav-tabs > li.active > a, @@ -386,11 +366,8 @@ h4 { code, pre { - padding: 2px 4px; - font-size: 90%; color: #bec5cb; background-color: #353c42; - border-radius: 4px; } /* Used in the Query Log table */ @@ -456,7 +433,7 @@ td.highlight { /* Used by the long-term pages */ .daterangepicker { background-color: #3e464c; - border-radius: 4px; + border: 1px solid #353c42; } .daterangepicker .ranges li:hover { @@ -467,7 +444,7 @@ td.highlight { } .daterangepicker .calendar-table { background-color: #3e464c; - border-radius: 4px; + border: 1px solid #353c42; } .daterangepicker td.off, @@ -535,7 +512,7 @@ textarea[readonly], .panel-body, .panel-default > .panel-heading { background-color: #3e464c; - border-radius: 4px; + border: 1px solid #353c42; color: #bec5cb; } @@ -568,23 +545,10 @@ input[type="password"]::-webkit-caps-lock-indicator { background-image: linear-gradient(to right, #114100 0%, #525200 100%); } -.icheckbox_polaris, -.icheckbox_futurico, -.icheckbox_minimal-blue { - margin-right: 10px; -} -.iradio_polaris, -.iradio_futurico, -.iradio_minimal-blue { - margin-right: 8px; -} - /* Overlay box with spinners as shown during data collection for graphs */ .box .overlay, .overlay-wrapper .overlay { - z-index: 50; background-color: rgba(53, 60, 66, 0.733); - border-radius: 3px; } .box .overlay > .fa, .overlay-wrapper .overlay > .fa, @@ -594,7 +558,6 @@ input[type="password"]::-webkit-caps-lock-indicator { .navbar-nav > .user-menu > .dropdown-menu > .user-footer { background-color: #353c42bb; - padding: 10px; } .modal-content { @@ -626,29 +589,29 @@ input[type="password"]::-webkit-caps-lock-indicator { /*** Additional fixes For UI ***/ .pa-small-box-aqua .inner { background-color: rgb(45,108,133); - border-top-left-radius: 10px; - border-top-right-radius: 10px; + + } .pa-small-box-green .inner { background-color: rgb(31,76,46); - border-top-left-radius: 10px; - border-top-right-radius: 10px; + + } .pa-small-box-yellow .inner { background-color: rgb(151,104,37); - border-top-left-radius: 10px; - border-top-right-radius: 10px; + + } .pa-small-box-red .inner { background-color: rgb(120,50,38); - border-top-left-radius: 10px; - border-top-right-radius: 10px; + + } .pa-small-box-gray .inner { background-color: #777; /* color: rgba(20,20,20,30%); */ - border-top-left-radius: 10px; - border-top-right-radius: 10px; + + } .pa-small-box-gray .inner h3 { color: #bbb; @@ -687,15 +650,6 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected .db_tools_table_cell_b:nth-child(1) {background: #272c30} .db_tools_table_cell_b:nth-child(2) {background: #272c30} -.db_info_table { - display: table; - border-spacing: 0em; - font-weight: 400; - font-size: 15px; - width: 100%; - margin: auto; -} - .nav-tabs-custom > .nav-tabs > li:hover > a, .nav-tabs-custom > .nav-tabs > li.active:hover > a { background-color: #272c30; color: #bec5cb; @@ -738,23 +692,7 @@ table.dataTable tbody tr.selected, table.dataTable tbody tr .selected color: #bec5cb; background-color: #272c30; } -/* Add border radius to bottom of the status boxes*/ -.pa-small-box-footer { - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; -} -.small-box > .inner h3, .small-box > .inner p { - margin-bottom: 0px; - margin-left: 0px; -} -.small-box:hover .icon { - font-size: 3em; -} -.small-box .icon { - top: 0.01em; - font-size: 3.25em; -} .nax_semitransparent-panel{ background-color: #000 !important; } diff --git a/front/css/system-dark-patch.css b/front/css/system-dark-patch.css index f55ce1c2..353c1679 100755 --- a/front/css/system-dark-patch.css +++ b/front/css/system-dark-patch.css @@ -76,9 +76,7 @@ border: 1px solid #353c42; } .dataTables_wrapper input[type="search"] { - border-radius: 4px; background-color: #353c42; - border: 0; color: #bec5cb; } .dataTables_paginate .pagination li > a { @@ -129,7 +127,6 @@ border-color: #3c8dbc; } .sidebar-menu > li > .treeview-menu { - margin: 0 1px; background-color: #32393e; } .sidebar a { @@ -147,23 +144,16 @@ color: #fff; } .sidebar-form { - border-radius: 3px; border: 1px solid #3e464c; - margin: 10px; } .sidebar-form input[type="text"], .sidebar-form .btn { box-shadow: none; background-color: #3e464c; border: 1px solid transparent; - height: 35px; } .sidebar-form input[type="text"] { color: #666; - border-top-left-radius: 2px; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-bottom-left-radius: 2px; } .sidebar-form input[type="text"]:focus, .sidebar-form input[type="text"]:focus + .input-group-btn .btn { @@ -175,10 +165,6 @@ } .sidebar-form .btn { color: #999; - border-top-left-radius: 0; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - border-bottom-left-radius: 0; } .box, .box-footer, @@ -210,20 +196,12 @@ .box > .box-header .btn { color: #bec5cb; } - .box.box-info, - .box.box-primary, - .box.box-success, - .box.box-warning, - .box.box-danger { - border-top-width: 3px; - } .main-header .navbar { background-color: #272c30; } .main-header .navbar .nav > li > a, .main-header .navbar .nav > li > .navbar-text { color: #bec5cb; - max-height: 50px; } .main-header .navbar .nav > li > a:hover, .main-header .navbar .nav > li > a:active, @@ -280,7 +258,6 @@ background: rgba(64, 72, 80, 0.666); } .nav-tabs-custom > .nav-tabs > li { - margin-right: 1px; color: #bec5cb; } .nav-tabs-custom > .nav-tabs > li.active > a, @@ -389,11 +366,8 @@ code, pre { - padding: 2px 4px; - font-size: 90%; color: #bec5cb; background-color: #353c42; - border-radius: 4px; } /* Used in the Query Log table */ @@ -459,7 +433,6 @@ /* Used by the long-term pages */ .daterangepicker { background-color: #3e464c; - border-radius: 4px; border: 1px solid #353c42; } .daterangepicker .ranges li:hover { @@ -470,7 +443,6 @@ } .daterangepicker .calendar-table { background-color: #3e464c; - border-radius: 4px; border: 1px solid #353c42; } .daterangepicker td.off, @@ -537,7 +509,6 @@ .panel-body, .panel-default > .panel-heading { background-color: #3e464c; - border-radius: 4px; border: 1px solid #353c42; color: #bec5cb; } @@ -570,23 +541,10 @@ background-image: linear-gradient(to right, #114100 0%, #525200 100%); } - .icheckbox_polaris, - .icheckbox_futurico, - .icheckbox_minimal-blue { - margin-right: 10px; - } - .iradio_polaris, - .iradio_futurico, - .iradio_minimal-blue { - margin-right: 8px; - } - /* Overlay box with spinners as shown during data collection for graphs */ .box .overlay, .overlay-wrapper .overlay { - z-index: 50; background-color: rgba(53, 60, 66, 0.733); - border-radius: 3px; } .box .overlay > .fa, .overlay-wrapper .overlay > .fa, @@ -596,7 +554,6 @@ .navbar-nav > .user-menu > .dropdown-menu > .user-footer { background-color: #353c42bb; - padding: 10px; } .modal-content { @@ -625,36 +582,21 @@ border-color: rgb(120, 127, 133); } - /*** Additional fixes For Pi.Alert UI ***/ - .small-box { - border-radius: 10px; - border-top: 0px; - } .pa-small-box-aqua .inner { background-color: rgb(45,108,133); - border-top-left-radius: 10px; - border-top-right-radius: 10px; } .pa-small-box-green .inner { background-color: rgb(31,76,46); - border-top-left-radius: 10px; - border-top-right-radius: 10px; } .pa-small-box-yellow .inner { background-color: rgb(151,104,37); - border-top-left-radius: 10px; - border-top-right-radius: 10px; } .pa-small-box-red .inner { background-color: rgb(120,50,38); - border-top-left-radius: 10px; - border-top-right-radius: 10px; } .pa-small-box-gray .inner { background-color: #777; /* color: rgba(20,20,20,30%); */ - border-top-left-radius: 10px; - border-top-right-radius: 10px; } .pa-small-box-gray .inner h3 { color: #bbb; @@ -693,15 +635,6 @@ .db_tools_table_cell_b:nth-child(1) {background: #272c30} .db_tools_table_cell_b:nth-child(2) {background: #272c30} - .db_info_table { - display: table; - border-spacing: 0em; - font-weight: 400; - font-size: 15px; - width: 100%; - margin: auto; - } - .nav-tabs-custom > .nav-tabs > li:hover > a, .nav-tabs-custom > .nav-tabs > li.active:hover > a { background-color: #272c30; color: #bec5cb; @@ -723,15 +656,6 @@ color: white !important; } - /* remove white border that appears on mobile screen sizes */ - .box-body { - border: 0px; - } - /* remove white border that appears on mobile screen sizes */ - .table-responsive { - border: 0px; - } - .login-page { background-color: transparent; } @@ -744,23 +668,6 @@ color: #bec5cb; background-color: #272c30; } - /* Add border radius to bottom of the status boxes*/ - .pa-small-box-footer { - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - } - - .small-box > .inner h3, .small-box > .inner p { - margin-bottom: 0px; - margin-left: 0px; - } - .small-box:hover .icon { - font-size: 3em; - } - .small-box .icon { - top: 0.01em; - font-size: 3.25em; - } .nax_semitransparent-panel{ background-color: #000 !important; } diff --git a/front/deviceDetailsEdit.php b/front/deviceDetailsEdit.php index ef447800..c9acb3a5 100755 --- a/front/deviceDetailsEdit.php +++ b/front/deviceDetailsEdit.php @@ -423,7 +423,7 @@ function setDeviceData(direction = '', refreshCallback = '') { // Build payload const payload = { - devName: $('#NEWDEV_devName').val().replace(/'/g, "’"), + devName: $('#NEWDEV_devName').val(), devOwner: $('#NEWDEV_devOwner').val().replace(/'/g, "’"), devType: $('#NEWDEV_devType').val().replace(/'/g, ""), devVendor: $('#NEWDEV_devVendor').val().replace(/'/g, "’"), @@ -432,7 +432,7 @@ function setDeviceData(direction = '', refreshCallback = '') { devFavorite: ($('#NEWDEV_devFavorite')[0].checked * 1), devGroup: $('#NEWDEV_devGroup').val().replace(/'/g, "’"), devLocation: $('#NEWDEV_devLocation').val().replace(/'/g, "’"), - devComments: encodeSpecialChars($('#NEWDEV_devComments').val()), + devComments: ($('#NEWDEV_devComments').val()), devParentMAC: $('#NEWDEV_devParentMAC').val(), devParentPort: $('#NEWDEV_devParentPort').val(), diff --git a/front/network.php b/front/network.php index 13830eed..c634c703 100755 --- a/front/network.php +++ b/front/network.php @@ -85,20 +85,24 @@ // \ // PC (leaf) <------- leafs are not included in this SQL query const rawSql = ` - SELECT node_name, node_mac, online, node_type, node_ports_count, parent_mac, node_icon, node_alert - FROM ( - SELECT a.devName as node_name, a.devMac as node_mac, a.devPresentLastScan as online, - a.devType as node_type, a.devParentMAC as parent_mac, a.devIcon as node_icon, a.devAlertDown as node_alert - FROM Devices a - WHERE a.devType IN (${networkDeviceTypes}) and a.devIsArchived = 0 - ) t1 - LEFT JOIN ( - SELECT b.devParentMAC as node_mac_2, count() as node_ports_count - FROM Devices b - WHERE b.devParentMAC NOT NULL - GROUP BY b.devParentMAC - ) t2 - ON (t1.node_mac = t2.node_mac_2) + SELECT + parent.devName AS node_name, + parent.devMac AS node_mac, + parent.devPresentLastScan AS online, + parent.devType AS node_type, + parent.devParentMAC AS parent_mac, + parent.devIcon AS node_icon, + parent.devAlertDown AS node_alert, + COUNT(child.devMac) AS node_ports_count + FROM Devices AS parent + LEFT JOIN Devices AS child + ON child.devParentMAC = parent.devMac + WHERE parent.devType IN ( + ${networkDeviceTypes}) + AND parent.devIsArchived = 0 + GROUP BY parent.devMac, parent.devName, parent.devPresentLastScan, + parent.devType, parent.devParentMAC, parent.devIcon, parent.devAlertDown + ORDER BY parent.devName; `; const apiBase = getApiBase(); @@ -378,6 +382,10 @@ // ---------------------------------------------------- function loadConnectedDevices(node_mac) { + + // 1. Force to lowercase to match the new DB standard + const normalized_mac = node_mac.toLowerCase(); + const sql = ` SELECT devName, devMac, devLastIP, devVendor, devPresentLastScan, devAlertDown, devParentPort, CASE @@ -389,12 +397,12 @@ ELSE 'Unknown status' END AS devStatus FROM Devices - WHERE devParentMac = '${node_mac}'`; + WHERE devParentMac = '${normalized_mac}'`; const id = node_mac.replace(/:/g, '_'); const wrapperHtml = ` - +
`; diff --git a/front/plugins/ddns_update/script.py b/front/plugins/ddns_update/script.py index 39bdade4..c25526a1 100755 --- a/front/plugins/ddns_update/script.py +++ b/front/plugins/ddns_update/script.py @@ -81,14 +81,14 @@ def ddns_update(DDNS_UPDATE_URL, DDNS_USER, DDNS_PASSWORD, DDNS_DOMAIN, PREV_IP) # plugin_objects = Plugin_Objects(RESULT_FILE) # plugin_objects.add_object( - # primaryId = 'Internet', # MAC (Device Name) + # primaryId = 'internet', # MAC (Device Name) # secondaryId = new_internet_IP, # IP Address # watched1 = f'Previous IP: {PREV_IP}', # watched2 = '', # watched3 = '', # watched4 = '', # extra = f'Previous IP: {PREV_IP}', - # foreignKey = 'Internet') + # foreignKey = 'internet') # plugin_objects.write_result_file() diff --git a/front/plugins/internet_ip/script.py b/front/plugins/internet_ip/script.py index ff5d3cea..328341a6 100755 --- a/front/plugins/internet_ip/script.py +++ b/front/plugins/internet_ip/script.py @@ -79,14 +79,14 @@ def main(): plugin_objects = Plugin_Objects(RESULT_FILE) plugin_objects.add_object( - primaryId = 'Internet', # MAC (Device Name) + primaryId = 'internet', # MAC (Device Name) secondaryId = new_internet_IP, # IP Address watched1 = f'Previous IP: {PREV_IP}', watched2 = cmd_output.replace('\n', ''), watched3 = retries_needed, watched4 = 'Gateway', extra = f'Previous IP: {PREV_IP}', - foreignKey = 'Internet' + foreignKey = 'internet' ) plugin_objects.write_result_file() @@ -101,8 +101,8 @@ def main(): # =============================================================================== def check_internet_IP(PREV_IP, DIG_GET_IP_ARG): - # Get Internet IP - mylog('verbose', [f'[{pluginName}] - Retrieving Internet IP']) + # Get internet IP + mylog('verbose', [f'[{pluginName}] - Retrieving internet IP']) internet_IP, cmd_output = get_internet_IP(DIG_GET_IP_ARG) mylog('verbose', [f'[{pluginName}] Current internet_IP : {internet_IP}']) diff --git a/front/plugins/omada_sdn_imp/omada_sdn.py b/front/plugins/omada_sdn_imp/omada_sdn.py index b00ce3f9..9447df69 100755 --- a/front/plugins/omada_sdn_imp/omada_sdn.py +++ b/front/plugins/omada_sdn_imp/omada_sdn.py @@ -330,8 +330,8 @@ def main(): myssid = device[PORT_SSID] if not device[PORT_SSID].isdigit() else "" ParentNetworkNode = ( ieee2ietf_mac_formater(device[SWITCH_AP]) - if device[SWITCH_AP] != "Internet" - else "Internet" + if device[SWITCH_AP].lower() != "internet" + else "internet" ) mymac = ieee2ietf_mac_formater(device[MAC]) plugin_objects.add_object( @@ -665,7 +665,7 @@ def get_device_data(omada_clients_output, switches_and_aps, device_handler): device_data_bymac[default_router_mac][TYPE] = "Firewall" # step2 let's find the first switch and set the default router parent to internet first_switch = device_data_bymac[default_router_mac][SWITCH_AP] - device_data_bymac[default_router_mac][SWITCH_AP] = "Internet" + device_data_bymac[default_router_mac][SWITCH_AP] = "internet" # step3 let's set the switch connected to the default gateway uplink to the default gateway and hardcode port to 1 for now: # device_data_bymac[first_switch][SWITCH_AP]=default_router_mac # device_data_bymac[first_switch][SWITCH_AP][PORT_SSID] = '1' diff --git a/front/plugins/omada_sdn_openapi/script.py b/front/plugins/omada_sdn_openapi/script.py index 7d341126..11b17b60 100755 --- a/front/plugins/omada_sdn_openapi/script.py +++ b/front/plugins/omada_sdn_openapi/script.py @@ -413,11 +413,11 @@ class OmadaData: OmadaHelper.verbose(f"Making entry for: {entry['mac_address']}") - # If the device_type is gateway, set the parent_node to Internet + # If the device_type is gateway, set the parent_node to internet device_type = entry["device_type"].lower() parent_node = entry["parent_node_mac_address"] if len(parent_node) == 0 and entry["device_type"] == "gateway" and is_typical_router_ip(entry["ip_address"]): - parent_node = "Internet" + parent_node = "internet" # Some device type naming exceptions if device_type == "iphone": diff --git a/front/plugins/plugin_helper.py b/front/plugins/plugin_helper.py index 6e1f99a0..b1c0399a 100755 --- a/front/plugins/plugin_helper.py +++ b/front/plugins/plugin_helper.py @@ -177,27 +177,25 @@ def decode_settings_base64(encoded_str, convert_types=True): # ------------------------------------------------------------------- def normalize_mac(mac): """ - Normalize a MAC address to the standard format with colon separators. - For example, "aa-bb-cc-dd-ee-ff" will be normalized to "AA:BB:CC:DD:EE:FF". - Wildcard MAC addresses like "AA:BB:CC:*" will be normalized to "AA:BB:CC:*". + normalize a mac address to the standard format with colon separators. + for example, "AA-BB-CC-DD-EE-FF" will be normalized to "aa:bb:cc:dd:ee:ff". + wildcard mac addresses like "AA:BB:CC:*" will be normalized to "aa:bb:cc:*". - :param mac: The MAC address to normalize. - :return: The normalized MAC address. + :param mac: the mac address to normalize. + :return: the normalized mac address (lowercase). """ - s = str(mac).strip() + s = str(mac).strip().lower() - if s.lower() == "internet": - return "Internet" + if s == "internet": + return "internet" - s = s.upper() - - # Determine separator if present, prefer colon, then hyphen + # determine separator if present, prefer colon, then hyphen if ':' in s: parts = s.split(':') elif '-' in s: parts = s.split('-') else: - # No explicit separator; attempt to split every two chars + # no explicit separator; attempt to split every two chars parts = [s[i:i + 2] for i in range(0, len(s), 2)] normalized_parts = [] @@ -206,10 +204,10 @@ def normalize_mac(mac): if part == '*': normalized_parts.append('*') else: - # Ensure two hex digits (zfill is fine for alphanumeric input) + # ensure two hex digits normalized_parts.append(part.zfill(2)) - # Use colon as canonical separator + # use colon as canonical separator return ':'.join(normalized_parts) diff --git a/front/plugins/unifi_api_import/unifi_api_import.py b/front/plugins/unifi_api_import/unifi_api_import.py index 4c51f7b4..678e8a46 100755 --- a/front/plugins/unifi_api_import/unifi_api_import.py +++ b/front/plugins/unifi_api_import/unifi_api_import.py @@ -74,7 +74,7 @@ def main(): watched1 = device['dev_name'], # name watched2 = device['dev_type'], # device_type (AP/Switch etc) watched3 = device['dev_connected'], # connectedAt or empty - watched4 = device['dev_parent_mac'], # parent_mac or "Internet" + watched4 = device['dev_parent_mac'], # parent_mac or "internet" extra = '', foreignKey = device['dev_mac'] ) @@ -115,10 +115,10 @@ def get_device_data(site, api): continue device_id_to_mac[dev["id"]] = dev.get("macAddress", "") - # Helper to resolve uplinkDeviceId to parent MAC, or "Internet" if no uplink + # Helper to resolve uplinkDeviceId to parent MAC, or "internet" if no uplink def resolve_parent_mac(uplink_id): if not uplink_id: - return "Internet" + return "internet" return device_id_to_mac.get(uplink_id, "Unknown") # Process Unifi devices diff --git a/front/plugins/unifi_import/script.py b/front/plugins/unifi_import/script.py index d62154b7..d2a3886b 100755 --- a/front/plugins/unifi_import/script.py +++ b/front/plugins/unifi_import/script.py @@ -173,7 +173,7 @@ def collect_details(device_type, devices, online_macs, processed_macs, plugin_ob # override parent MAC if this is a router if parentMac == 'null' and is_typical_router_ip(ipTmp): - parentMac = 'Internet' + parentMac = 'internet' # Add object only if not processed if macTmp not in processed_macs and (status == 1 or force_import is True): diff --git a/scripts/generate-device-inventory.py b/scripts/generate-device-inventory.py index 3ca76a4b..1ad959f3 100644 --- a/scripts/generate-device-inventory.py +++ b/scripts/generate-device-inventory.py @@ -216,7 +216,7 @@ def generate_rows(args: argparse.Namespace, header: list[str]) -> list[dict[str, rows: list[dict[str, str]] = [] - # Include one Internet root device that anchors the tree; it does not consume an IP. + # Include one internet root device that anchors the tree; it does not consume an IP. required_devices = 1 + args.switches + args.aps + args.devices if required_devices > len(ip_pool): raise ValueError( @@ -229,12 +229,12 @@ def generate_rows(args: argparse.Namespace, header: list[str]) -> list[dict[str, ip_pool.remove(choice) return choice - # Root "Internet" device (no parent, no IP) so the topology has a defined root. + # Root "internet" device (no parent, no IP) so the topology has a defined root. root_row = build_row( - name="Internet", + name="internet", dev_type="Gateway", vendor="NetAlertX", - mac="Internet", + mac="internet", parent_mac="", ip="", header=header, @@ -243,7 +243,7 @@ def generate_rows(args: argparse.Namespace, header: list[str]) -> list[dict[str, ssid=args.ssid, now=now, ) - root_row["devComments"] = "Synthetic root device representing the Internet." + root_row["devComments"] = "Synthetic root device representing the internet." root_row["devParentRelType"] = "Root" root_row["devStaticIP"] = "0" root_row["devScan"] = "0" @@ -261,7 +261,7 @@ def generate_rows(args: argparse.Namespace, header: list[str]) -> list[dict[str, dev_type="Firewall", vendor=random.choice(VENDORS), mac=router_mac, - parent_mac="Internet", + parent_mac="internet", ip=router_ip, header=header, owner=args.owner, diff --git a/server/api_server/openapi/schemas.py b/server/api_server/openapi/schemas.py index fe8f9618..409ca176 100644 --- a/server/api_server/openapi/schemas.py +++ b/server/api_server/openapi/schemas.py @@ -64,9 +64,9 @@ ALLOWED_EVENT_TYPES = Literal[ def validate_mac(value: str) -> str: """Validate and normalize MAC address format.""" - # Allow "Internet" as a special case for the gateway/WAN device + # Allow "internet" as a special case for the gateway/WAN device if value.lower() == "internet": - return "Internet" + return "internet" if not is_mac(value): raise ValueError(f"Invalid MAC address format: {value}") @@ -439,7 +439,7 @@ class DeviceUpdateRequest(BaseModel): def sanitize_text_fields(cls, v: Optional[str]) -> Optional[str]: if v is None: return v - return sanitize_string(v) + return v class DeleteDevicesRequest(BaseModel): diff --git a/server/database.py b/server/database.py index 07678c07..b76ff076 100755 --- a/server/database.py +++ b/server/database.py @@ -16,6 +16,7 @@ from db.db_upgrade import ( ensure_Parameters, ensure_Settings, ensure_Indexes, + ensure_mac_lowercase_triggers, ) @@ -198,6 +199,9 @@ class DB: # Indexes ensure_Indexes(self.sql) + # Normalization triggers + ensure_mac_lowercase_triggers(self.sql) + # commit changes self.commitDB() except Exception as e: diff --git a/server/db/db_upgrade.py b/server/db/db_upgrade.py index b79ddf88..71e7fe5e 100755 --- a/server/db/db_upgrade.py +++ b/server/db/db_upgrade.py @@ -105,6 +105,50 @@ def ensure_column(sql, table: str, column_name: str, column_type: str) -> bool: return False +def ensure_mac_lowercase_triggers(sql): + """ + Ensures the triggers for lowercasing MAC addresses exist on the Devices table. + """ + try: + # 1. Handle INSERT Trigger + sql.execute("SELECT name FROM sqlite_master WHERE type='trigger' AND name='trg_lowercase_mac_insert'") + if not sql.fetchone(): + mylog("verbose", ["[db_upgrade] Creating trigger 'trg_lowercase_mac_insert'"]) + sql.execute(""" + CREATE TRIGGER trg_lowercase_mac_insert + AFTER INSERT ON Devices + BEGIN + UPDATE Devices + SET devMac = LOWER(NEW.devMac), + devParentMAC = LOWER(NEW.devParentMAC) + WHERE rowid = NEW.rowid; + END; + """) + + # 2. Handle UPDATE Trigger + sql.execute("SELECT name FROM sqlite_master WHERE type='trigger' AND name='trg_lowercase_mac_update'") + if not sql.fetchone(): + mylog("verbose", ["[db_upgrade] Creating trigger 'trg_lowercase_mac_update'"]) + # Note: Using 'WHEN' to prevent unnecessary updates and recursion + sql.execute(""" + CREATE TRIGGER trg_lowercase_mac_update + AFTER UPDATE OF devMac, devParentMAC ON Devices + WHEN (NEW.devMac GLOB '*[A-Z]*') OR (NEW.devParentMAC GLOB '*[A-Z]*') + BEGIN + UPDATE Devices + SET devMac = LOWER(NEW.devMac), + devParentMAC = LOWER(NEW.devParentMAC) + WHERE rowid = NEW.rowid; + END; + """) + + return True + + except Exception as e: + mylog("none", [f"[db_upgrade] ERROR while ensuring MAC triggers: {e}"]) + return False + + def ensure_views(sql) -> bool: """ Ensures required views exist. diff --git a/server/initialise.py b/server/initialise.py index e6bb2242..d3b13c22 100755 --- a/server/initialise.py +++ b/server/initialise.py @@ -358,7 +358,7 @@ def importConfigs(pm, db, all_plugins): "Router", "USB LAN Adapter", "USB WIFI Adapter", - "Internet", + "internet", ], c_d, "Network device types", diff --git a/server/models/device_instance.py b/server/models/device_instance.py index 0712ad55..e522f4a9 100755 --- a/server/models/device_instance.py +++ b/server/models/device_instance.py @@ -495,7 +495,7 @@ class DeviceInstance: # Fetch children cur.execute( - "SELECT * FROM Devices WHERE devParentMAC = ? ORDER BY devPresentLastScan DESC", + "SELECT * FROM Devices WHERE LOWER(devParentMAC) = LOWER(?) ORDER BY devPresentLastScan DESC", (device_data["devMac"],), ) children_rows = cur.fetchall() diff --git a/server/scan/device_handling.py b/server/scan/device_handling.py index dcb24476..886f550f 100755 --- a/server/scan/device_handling.py +++ b/server/scan/device_handling.py @@ -728,10 +728,10 @@ def create_new_devices(db): scanParentMAC = raw_parent_mac scanParentMAC = ( scanParentMAC - if scanParentMAC and scanMac != "Internet" + if scanParentMAC and scanMac.lower() != "internet" else ( get_setting_value("NEWDEV_devParentMAC") - if scanMac != "Internet" + if scanMac.lower() != "internet" else "null" ) ) @@ -1243,7 +1243,7 @@ def update_devPresentLastScan_based_on_force_status(db): # ------------------------------------------------------------------------------- -# Check if the variable contains a valid MAC address or "Internet" +# Check if the variable contains a valid MAC address or "internet" def check_mac_or_internet(input_str): # Regular expression pattern for matching a MAC address mac_pattern = r"([0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2}[:-][0-9A-Fa-f]{2})" diff --git a/server/scan/device_heuristics.py b/server/scan/device_heuristics.py index b9c14520..71bd183a 100755 --- a/server/scan/device_heuristics.py +++ b/server/scan/device_heuristics.py @@ -179,7 +179,7 @@ def guess_device_attributes( # # Internet shortcut # if mac == "INTERNET": - # return ICONS.get("globe", default_icon), DEVICE_TYPES.get("Internet", default_type) + # return ICONS.get("globe", default_icon), DEVICE_TYPES.get("internet", default_type) type_ = None icon = None diff --git a/test/api_endpoints/test_device_endpoints.py b/test/api_endpoints/test_device_endpoints.py index 7a1ffa96..85284f73 100644 --- a/test/api_endpoints/test_device_endpoints.py +++ b/test/api_endpoints/test_device_endpoints.py @@ -132,7 +132,7 @@ def test_update_device_column(client, api_token, test_mac): # Update its parent MAC resp = client.post( f"/device/{test_mac}/update-column", - json={"columnName": "devParentMAC", "columnValue": "Internet"}, + json={"columnName": "devParentMAC", "columnValue": "internet"}, headers=auth_headers(api_token), ) @@ -142,7 +142,7 @@ def test_update_device_column(client, api_token, test_mac): # Try updating a non-existent device resp_missing = client.post( "/device/11:22:33:44:55:66/update-column", - json={"columnName": "devParentMAC", "columnValue": "Internet"}, + json={"columnName": "devParentMAC", "columnValue": "internet"}, headers=auth_headers(api_token), ) diff --git a/test/api_endpoints/test_device_update_normalization.py b/test/api_endpoints/test_device_update_normalization.py index 70176d5e..832111a1 100644 --- a/test/api_endpoints/test_device_update_normalization.py +++ b/test/api_endpoints/test_device_update_normalization.py @@ -1,70 +1,78 @@ - import pytest -import random from helper import get_setting_value from api_server.api_server_start import app from models.device_instance import DeviceInstance + @pytest.fixture(scope="session") def api_token(): return get_setting_value("API_TOKEN") + @pytest.fixture def client(): with app.test_client() as client: yield client + @pytest.fixture def test_mac_norm(): - # Normalized MAC - return "AA:BB:CC:DD:EE:FF" + # Now normalized to lowercase + return "aa:bb:cc:dd:ee:ff" + @pytest.fixture def test_parent_mac_input(): - # Lowercase input MAC - return "aa:bb:cc:dd:ee:00" + # Input with mixed/upper case to test the trigger/normalization + return "AA:BB:CC:DD:EE:00" + @pytest.fixture def test_parent_mac_norm(): - # Normalized expected MAC - return "AA:BB:CC:DD:EE:00" + # Expected result in DB (lowercase) + return "aa:bb:cc:dd:ee:00" + def auth_headers(token): return {"Authorization": f"Bearer {token}"} + def test_update_normalization(client, api_token, test_mac_norm, test_parent_mac_input, test_parent_mac_norm): - # 1. Create a device (using normalized MAC) + # 1. Create a device create_payload = { "createNew": True, "devName": "Normalization Test Device", "devOwner": "Unit Test", } + # Pass the lowercase mac resp = client.post(f"/device/{test_mac_norm}", json=create_payload, headers=auth_headers(api_token)) assert resp.status_code == 200 assert resp.json.get("success") is True - # 2. Update the device using LOWERCASE MAC in URL - # And set devParentMAC to LOWERCASE + # 2. Update the device sending UPPERCASE parent MAC + # To verify the triggers/logic flip it to lowercase update_payload = { "devParentMAC": test_parent_mac_input, "devName": "Updated Device" } - # Using lowercase MAC in URL: aa:bb:cc:dd:ee:ff - lowercase_mac = test_mac_norm.lower() - - resp = client.post(f"/device/{lowercase_mac}", json=update_payload, headers=auth_headers(api_token)) + + resp = client.post(f"/device/{test_mac_norm}", json=update_payload, headers=auth_headers(api_token)) assert resp.status_code == 200 assert resp.json.get("success") is True - # 3. Verify in DB that devParentMAC is NORMALIZED + # 3. Verify in DB that devParentMAC is LOWERCASE device_handler = DeviceInstance() + # Query using lowercase mac device = device_handler.getDeviceData(test_mac_norm) - + assert device is not None assert device["devName"] == "Updated Device" - # This is the critical check: + + # CRITICAL CHECKS: + # It must be lowercase now assert device["devParentMAC"] == test_parent_mac_norm - assert device["devParentMAC"] != test_parent_mac_input # Should verify it changed from input if input was different case + # It should NOT be the uppercase input we sent + assert device["devParentMAC"] != test_parent_mac_input # Cleanup - device_handler.deleteDeviceByMAC(test_mac_norm) + device_handler.deleteDeviceByMAC(test_mac_norm) \ No newline at end of file diff --git a/test/test_plugin_helper.py b/test/test_plugin_helper.py index 9a88f39b..59e11597 100644 --- a/test/test_plugin_helper.py +++ b/test/test_plugin_helper.py @@ -19,6 +19,6 @@ def test_normalize_mac_preserves_wildcard(): def test_normalize_mac_preserves_internet_root(): - assert normalize_mac("internet") == "Internet" - assert normalize_mac("Internet") == "Internet" - assert normalize_mac("INTERNET") == "Internet" + assert normalize_mac("internet") == "internet" + assert normalize_mac("Internet") == "internet" + assert normalize_mac("INTERNET") == "internet"