mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
feat: Enhance plugin configurations and improve MAC normalization
This commit is contained in:
@@ -333,6 +333,68 @@
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -307,6 +307,68 @@
|
||||
"string": "Some devices don't have a MAC assigned. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -305,7 +305,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -166,7 +166,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -119,7 +119,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -689,7 +689,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -720,7 +720,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -121,7 +121,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -303,6 +303,68 @@
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -296,6 +296,68 @@
|
||||
"string": "Maximum time in seconds to wait for the script to finish. If this time is exceeded the script is aborted."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -410,6 +410,68 @@
|
||||
"string": "Benachrichtige nur bei diesen Status. <code>new</code> bedeutet ein neues eindeutiges (einzigartige Kombination aus PrimaryId und SecondaryId) Objekt wurde erkennt. <code>watched-changed</code> bedeutet eine ausgewählte <code>Watched_ValueN</code>-Spalte hat sich geändert."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -169,7 +169,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -422,7 +422,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -267,6 +267,68 @@
|
||||
"string": "Password for Mikrotik Router"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -119,7 +119,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -451,6 +451,68 @@
|
||||
"string": "When scanning remote networks, NMAP can only retrieve the IP address, not the MAC address. Enabling the FAKE_MAC setting generates a fake MAC address from the IP address to track devices, but it may cause inconsistencies if IPs change or devices are re-discovered with a different MAC. Static IPs are recommended. Device type and icon might not be detected correctly and some plugins might fail if they depend on a valid MAC address. When unchecked, devices with empty MAC addresses are skipped."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -120,7 +120,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -467,6 +467,68 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -440,6 +440,68 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"database_column_definitions": [
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -154,7 +154,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -253,7 +253,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true"}],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true"}],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -184,7 +184,12 @@ def normalize_mac(mac):
|
||||
:param mac: The MAC address to normalize.
|
||||
:return: The normalized MAC address.
|
||||
"""
|
||||
s = str(mac).upper().strip()
|
||||
s = str(mac).strip()
|
||||
|
||||
if s.lower() == "internet":
|
||||
return "Internet"
|
||||
|
||||
s = s.upper()
|
||||
|
||||
# Determine separator if present, prefer colon, then hyphen
|
||||
if ':' in s:
|
||||
|
||||
@@ -593,7 +593,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -624,7 +624,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -859,6 +859,68 @@
|
||||
"string": "Status"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_ALWAYS",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": ["devMac", "devLastIP"],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set always columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are always overwritten by this plugin, unless the user locks or overwrites the value."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"function": "SET_EMPTY",
|
||||
"type": {
|
||||
"dataType": "array",
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"default_value": [],
|
||||
"options": [
|
||||
"devMac",
|
||||
"devLastIP"
|
||||
],
|
||||
"localized": ["name", "description"],
|
||||
"name": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "Set empty columns"
|
||||
}
|
||||
],
|
||||
"description": [
|
||||
{
|
||||
"language_code": "en_us",
|
||||
"string": "These columns are only overwritten if they are empty or set to NEWDEV."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -505,7 +505,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -538,7 +538,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -923,7 +923,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -959,7 +959,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -233,7 +233,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
@@ -265,7 +265,7 @@
|
||||
"elements": [
|
||||
{
|
||||
"elementType": "select",
|
||||
"elementOptions": [{ "multiple": "true", "orderable": "true" }],
|
||||
"elementOptions": [{ "multiple": "true", "ordeable": "true" }],
|
||||
"transformers": []
|
||||
}
|
||||
]
|
||||
|
||||
@@ -44,7 +44,7 @@ from models.user_events_queue_instance import UserEventsQueueInstance # noqa: E
|
||||
|
||||
from models.event_instance import EventInstance # noqa: E402 [flake8 lint suppression]
|
||||
# Import tool logic from the MCP/tools module to reuse behavior (no blueprints)
|
||||
from plugin_helper import is_mac # noqa: E402 [flake8 lint suppression]
|
||||
from plugin_helper import is_mac, normalize_mac # noqa: E402 [flake8 lint suppression]
|
||||
# is_mac is provided in mcp_endpoint and used by those handlers
|
||||
# mcp_endpoint contains helper functions; routes moved into this module to keep a single place for routes
|
||||
from messaging.in_app import ( # noqa: E402 [flake8 lint suppression]
|
||||
@@ -469,33 +469,28 @@ def api_device_field_lock(mac, payload=None):
|
||||
if not field_name:
|
||||
return jsonify({"success": False, "error": "fieldName is required"}), 400
|
||||
|
||||
# Validate that the field can be locked
|
||||
source_field = field_name + "Source"
|
||||
allowed_tracked_fields = {
|
||||
"devMac", "devName", "devLastIP", "devVendor", "devFQDN",
|
||||
"devSSID", "devParentMAC", "devParentPort", "devParentRelType", "devVlan"
|
||||
}
|
||||
if field_name not in allowed_tracked_fields:
|
||||
return jsonify({"success": False, "error": f"Field '{field_name}' cannot be locked"}), 400
|
||||
|
||||
device_handler = DeviceInstance()
|
||||
normalized_mac = normalize_mac(mac)
|
||||
|
||||
try:
|
||||
# When locking: set source to LOCKED
|
||||
# When unlocking: check current value and let plugins take over
|
||||
new_source = "LOCKED" if should_lock else "NEWDEV"
|
||||
result = device_handler.updateDeviceColumn(mac, source_field, new_source)
|
||||
|
||||
if result.get("success"):
|
||||
action = "locked" if should_lock else "unlocked"
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"message": f"Field {field_name} {action}",
|
||||
"fieldName": field_name,
|
||||
"locked": should_lock
|
||||
})
|
||||
if should_lock:
|
||||
result = device_handler.lockDeviceField(normalized_mac, field_name)
|
||||
action = "locked"
|
||||
else:
|
||||
return jsonify(result), 400
|
||||
result = device_handler.unlockDeviceField(normalized_mac, field_name)
|
||||
action = "unlocked"
|
||||
|
||||
response = dict(result)
|
||||
response["fieldName"] = field_name
|
||||
response["locked"] = should_lock
|
||||
|
||||
if response.get("success"):
|
||||
response.setdefault("message", f"Field {field_name} {action}")
|
||||
return jsonify(response)
|
||||
|
||||
if "does not support" in response.get("error", ""):
|
||||
response["error"] = f"Field '{field_name}' cannot be {action}"
|
||||
return jsonify(response), 400
|
||||
except Exception as e:
|
||||
mylog("none", f"Error locking field {field_name} for {mac}: {str(e)}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@@ -152,12 +152,20 @@ def enforce_source_on_user_update(devMac, updates_dict, conn):
|
||||
cur = conn.cursor()
|
||||
|
||||
# Check if field has a corresponding source and should be updated
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("PRAGMA table_info(Devices)")
|
||||
device_columns = {row["name"] for row in cur.fetchall()}
|
||||
except Exception:
|
||||
device_columns = set()
|
||||
|
||||
updates_to_apply = {}
|
||||
for field_name, new_value in updates_dict.items():
|
||||
if field_name in FIELD_SOURCE_MAP:
|
||||
source_field = FIELD_SOURCE_MAP[field_name]
|
||||
# User is updating this field, so mark it as USER
|
||||
updates_to_apply[source_field] = "USER"
|
||||
if not device_columns or source_field in device_columns:
|
||||
updates_to_apply[source_field] = "USER"
|
||||
|
||||
if not updates_to_apply:
|
||||
return
|
||||
@@ -198,6 +206,15 @@ def lock_field(devMac, field_name, conn):
|
||||
|
||||
source_field = FIELD_SOURCE_MAP[field_name]
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("PRAGMA table_info(Devices)")
|
||||
device_columns = {row["name"] for row in cur.fetchall()}
|
||||
except Exception:
|
||||
device_columns = set()
|
||||
|
||||
if device_columns and source_field not in device_columns:
|
||||
mylog("debug", [f"[lock_field] Source column {source_field} missing for {field_name}"])
|
||||
return
|
||||
|
||||
sql = f"UPDATE Devices SET {source_field}='LOCKED' WHERE devMac = ?"
|
||||
|
||||
@@ -227,6 +244,15 @@ def unlock_field(devMac, field_name, conn):
|
||||
|
||||
source_field = FIELD_SOURCE_MAP[field_name]
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("PRAGMA table_info(Devices)")
|
||||
device_columns = {row["name"] for row in cur.fetchall()}
|
||||
except Exception:
|
||||
device_columns = set()
|
||||
|
||||
if device_columns and source_field not in device_columns:
|
||||
mylog("debug", [f"[unlock_field] Source column {source_field} missing for {field_name}"])
|
||||
return
|
||||
|
||||
# Unlock by resetting to empty (allows overwrite)
|
||||
sql = f"UPDATE Devices SET {source_field}='' WHERE devMac = ?"
|
||||
|
||||
@@ -593,7 +593,6 @@ class DeviceInstance:
|
||||
conn = get_temp_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute(sql, values)
|
||||
conn.commit()
|
||||
|
||||
if data.get("createNew", False):
|
||||
# Initialize source-tracking fields on device creation.
|
||||
@@ -617,7 +616,6 @@ class DeviceInstance:
|
||||
source_values.append(normalized_mac)
|
||||
source_sql = f"UPDATE Devices SET {set_clause} WHERE devMac = ?"
|
||||
cur.execute(source_sql, source_values)
|
||||
conn.commit()
|
||||
|
||||
# Enforce source tracking on user updates
|
||||
# User-updated fields should have their *Source set to "USER"
|
||||
@@ -631,6 +629,8 @@ class DeviceInstance:
|
||||
conn.close()
|
||||
return {"success": False, "error": f"Source tracking failed: {e}"}
|
||||
|
||||
# Commit all changes atomically after all operations succeed
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
mylog("debug", f"[DeviceInstance] setDeviceData SQL: {sql.strip()}")
|
||||
|
||||
@@ -545,12 +545,13 @@ def create_new_devices(db):
|
||||
# Derive primary IP family values
|
||||
cur_IP = str(cur_IP).strip() if cur_IP else ""
|
||||
cur_IP_normalized = check_IP_format(cur_IP) if ":" not in cur_IP else cur_IP
|
||||
|
||||
# Validate IPv6 addresses using format_ip_long for consistency
|
||||
if ":" in cur_IP_normalized:
|
||||
|
||||
# Validate IPv6 addresses using format_ip_long for consistency (do not store integer result)
|
||||
if cur_IP_normalized and ":" in cur_IP_normalized:
|
||||
validated_ipv6 = format_ip_long(cur_IP_normalized)
|
||||
cur_IP_normalized = validated_ipv6 if validated_ipv6 else ""
|
||||
|
||||
if validated_ipv6 is None or validated_ipv6 < 0:
|
||||
cur_IP_normalized = ""
|
||||
|
||||
primary_ipv4 = cur_IP_normalized if cur_IP_normalized and ":" not in cur_IP_normalized else ""
|
||||
primary_ipv6 = cur_IP_normalized if cur_IP_normalized and ":" in cur_IP_normalized else ""
|
||||
|
||||
|
||||
@@ -115,6 +115,30 @@ class TestDeviceFieldLock:
|
||||
assert resp.status_code == 400
|
||||
assert "cannot be locked" in resp.json.get("error", "")
|
||||
|
||||
def test_lock_field_normalizes_mac(self, client, test_mac, auth_headers):
|
||||
"""Lock endpoint should normalize MACs before applying locks."""
|
||||
# Create device with normalized MAC
|
||||
self.test_create_test_device(client, test_mac, auth_headers)
|
||||
|
||||
mac_variant = "aa-bb-cc-dd-ee-ff"
|
||||
payload = {
|
||||
"fieldName": "devName",
|
||||
"lock": True
|
||||
}
|
||||
resp = client.post(
|
||||
f"/device/{mac_variant}/field/lock",
|
||||
json=payload,
|
||||
headers=auth_headers
|
||||
)
|
||||
assert resp.status_code == 200, f"Failed to lock via normalized MAC: {resp.json}"
|
||||
assert resp.json.get("locked") is True
|
||||
|
||||
# Verify source is LOCKED on normalized MAC
|
||||
resp = client.get(f"/device/{test_mac}", headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
device_data = resp.json
|
||||
assert device_data.get("devNameSource") == "LOCKED"
|
||||
|
||||
def test_lock_all_tracked_fields(self, client, test_mac, auth_headers):
|
||||
"""Lock each tracked field individually."""
|
||||
# First create device
|
||||
|
||||
@@ -72,6 +72,103 @@ def ip_test_db():
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_device_db():
|
||||
"""Create an in-memory SQLite database for create_new_devices tests."""
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY,
|
||||
devName TEXT,
|
||||
devVendor TEXT,
|
||||
devLastIP TEXT,
|
||||
devPrimaryIPv4 TEXT,
|
||||
devPrimaryIPv6 TEXT,
|
||||
devFirstConnection TEXT,
|
||||
devLastConnection TEXT,
|
||||
devSyncHubNode TEXT,
|
||||
devGUID TEXT,
|
||||
devParentMAC TEXT,
|
||||
devParentPort TEXT,
|
||||
devSite TEXT,
|
||||
devSSID TEXT,
|
||||
devType TEXT,
|
||||
devSourcePlugin TEXT,
|
||||
devAlertEvents INTEGER,
|
||||
devAlertDown INTEGER,
|
||||
devPresentLastScan INTEGER,
|
||||
devIsArchived INTEGER,
|
||||
devIsNew INTEGER,
|
||||
devSkipRepeated INTEGER,
|
||||
devScan INTEGER,
|
||||
devOwner TEXT,
|
||||
devFavorite INTEGER,
|
||||
devGroup TEXT,
|
||||
devComments TEXT,
|
||||
devLogEvents INTEGER,
|
||||
devLocation TEXT,
|
||||
devCustomProps TEXT,
|
||||
devParentRelType TEXT,
|
||||
devReqNicsOnline INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE CurrentScan (
|
||||
cur_MAC TEXT,
|
||||
cur_Name TEXT,
|
||||
cur_Vendor TEXT,
|
||||
cur_ScanMethod TEXT,
|
||||
cur_IP TEXT,
|
||||
cur_SyncHubNodeName TEXT,
|
||||
cur_NetworkNodeMAC TEXT,
|
||||
cur_PORT TEXT,
|
||||
cur_NetworkSite TEXT,
|
||||
cur_SSID TEXT,
|
||||
cur_Type TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Events (
|
||||
eve_MAC TEXT,
|
||||
eve_IP TEXT,
|
||||
eve_DateTime TEXT,
|
||||
eve_EventType TEXT,
|
||||
eve_AdditionalInfo TEXT,
|
||||
eve_PendingAlertEmail INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE Sessions (
|
||||
ses_MAC TEXT,
|
||||
ses_IP TEXT,
|
||||
ses_EventTypeConnection TEXT,
|
||||
ses_DateTimeConnection TEXT,
|
||||
ses_EventTypeDisconnection TEXT,
|
||||
ses_DateTimeDisconnection TEXT,
|
||||
ses_StillConnected INTEGER,
|
||||
ses_AdditionalInfo TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
yield conn
|
||||
conn.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ip_handlers():
|
||||
"""Mock device_handling helper functions."""
|
||||
@@ -311,6 +408,55 @@ def test_invalid_ip_values_rejected(ip_test_db, mock_ip_handlers):
|
||||
), f"Invalid IP '{invalid_ip}' should not overwrite valid IPv4"
|
||||
|
||||
|
||||
def test_invalid_ipv6_rejected_on_create_new_devices(new_device_db):
|
||||
"""Invalid IPv6 values should not be persisted when creating new devices."""
|
||||
cur = new_device_db.cursor()
|
||||
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO CurrentScan (
|
||||
cur_MAC, cur_Name, cur_Vendor, cur_ScanMethod, cur_IP,
|
||||
cur_SyncHubNodeName, cur_NetworkNodeMAC, cur_PORT,
|
||||
cur_NetworkSite, cur_SSID, cur_Type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
"AA:BB:CC:DD:EE:10",
|
||||
"",
|
||||
"Vendor",
|
||||
"ARPSCAN",
|
||||
"fe80::zz",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
),
|
||||
)
|
||||
new_device_db.commit()
|
||||
|
||||
db = Mock()
|
||||
db.sql_connection = new_device_db
|
||||
db.sql = cur
|
||||
db.commitDB = Mock(side_effect=new_device_db.commit)
|
||||
|
||||
with patch("helper.get_setting_value", return_value=""), patch.object(
|
||||
device_handling, "get_setting_value", return_value=""
|
||||
):
|
||||
device_handling.create_new_devices(db)
|
||||
|
||||
row = cur.execute(
|
||||
"SELECT devLastIP, devPrimaryIPv4, devPrimaryIPv6 FROM Devices WHERE devMac = ?",
|
||||
("AA:BB:CC:DD:EE:10",),
|
||||
).fetchone()
|
||||
|
||||
assert row is not None, "Device should be created"
|
||||
assert row["devLastIP"] == "", "Invalid IPv6 should not set devLastIP"
|
||||
assert row["devPrimaryIPv4"] == "", "Invalid IPv6 should not set devPrimaryIPv4"
|
||||
assert row["devPrimaryIPv6"] == "", "Invalid IPv6 should not set devPrimaryIPv6"
|
||||
|
||||
|
||||
def test_ipv4_ipv6_mixed_in_multiple_scans(ip_test_db, mock_ip_handlers):
|
||||
"""Multiple scans with different IP types should set both primary fields correctly."""
|
||||
cur = ip_test_db.cursor()
|
||||
|
||||
231
test/test_device_atomicity.py
Normal file
231
test/test_device_atomicity.py
Normal file
@@ -0,0 +1,231 @@
|
||||
"""
|
||||
Test for atomicity of device updates with source-tracking.
|
||||
|
||||
Verifies that:
|
||||
1. If source-tracking fails, the device row is rolled back.
|
||||
2. If source-tracking succeeds, device row and sources are both committed.
|
||||
3. Database remains consistent in both scenarios.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
# Add server and plugins to path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'server'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'front', 'plugins'))
|
||||
|
||||
from models.device_instance import DeviceInstance # noqa: E402 [flake8 lint suppression]
|
||||
from plugin_helper import normalize_mac # noqa: E402 [flake8 lint suppression]
|
||||
|
||||
|
||||
class TestDeviceAtomicity(unittest.TestCase):
|
||||
"""Test atomic transactions for device updates with source-tracking."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create an in-memory SQLite DB for testing."""
|
||||
self.test_db = tempfile.NamedTemporaryFile(delete=False, suffix='.db')
|
||||
self.test_db_path = self.test_db.name
|
||||
self.test_db.close()
|
||||
|
||||
# Create minimal schema
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
# Create Devices table with source-tracking columns
|
||||
cur.execute("""
|
||||
CREATE TABLE Devices (
|
||||
devMac TEXT PRIMARY KEY,
|
||||
devName TEXT,
|
||||
devOwner TEXT,
|
||||
devType TEXT,
|
||||
devVendor TEXT,
|
||||
devIcon TEXT,
|
||||
devFavorite INTEGER DEFAULT 0,
|
||||
devGroup TEXT,
|
||||
devLocation TEXT,
|
||||
devComments TEXT,
|
||||
devParentMAC TEXT,
|
||||
devParentPort TEXT,
|
||||
devSSID TEXT,
|
||||
devSite TEXT,
|
||||
devStaticIP INTEGER DEFAULT 0,
|
||||
devScan INTEGER DEFAULT 0,
|
||||
devAlertEvents INTEGER DEFAULT 0,
|
||||
devAlertDown INTEGER DEFAULT 0,
|
||||
devParentRelType TEXT DEFAULT 'default',
|
||||
devReqNicsOnline INTEGER DEFAULT 0,
|
||||
devSkipRepeated INTEGER DEFAULT 0,
|
||||
devIsNew INTEGER DEFAULT 0,
|
||||
devIsArchived INTEGER DEFAULT 0,
|
||||
devLastConnection TEXT,
|
||||
devFirstConnection TEXT,
|
||||
devLastIP TEXT,
|
||||
devGUID TEXT,
|
||||
devCustomProps TEXT,
|
||||
devSourcePlugin TEXT,
|
||||
devNameSource TEXT,
|
||||
devTypeSource TEXT,
|
||||
devVendorSource TEXT,
|
||||
devIconSource TEXT,
|
||||
devGroupSource TEXT,
|
||||
devLocationSource TEXT,
|
||||
devCommentsSource TEXT,
|
||||
devMacSource TEXT
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test database."""
|
||||
if os.path.exists(self.test_db_path):
|
||||
os.unlink(self.test_db_path)
|
||||
|
||||
def _get_test_db_connection(self):
|
||||
"""Override database connection for testing."""
|
||||
conn = sqlite3.connect(self.test_db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def test_create_new_device_atomicity(self):
|
||||
"""
|
||||
Test that device creation and source-tracking are atomic.
|
||||
If source tracking fails, the device should not be created.
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Patch at module level where it's used
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
# Create a new device
|
||||
data = {
|
||||
"createNew": True,
|
||||
"devMac": test_mac,
|
||||
"devName": "Test Device",
|
||||
"devOwner": "John Doe",
|
||||
"devType": "Laptop",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify success
|
||||
self.assertTrue(result["success"], f"Device creation failed: {result}")
|
||||
|
||||
# Verify device exists
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertIsNotNone(device, "Device was not created")
|
||||
self.assertEqual(device["devName"], "Test Device")
|
||||
|
||||
# Verify source tracking was set
|
||||
self.assertEqual(device["devMacSource"], "NEWDEV")
|
||||
self.assertEqual(device["devNameSource"], "NEWDEV")
|
||||
|
||||
def test_update_device_with_source_tracking_atomicity(self):
|
||||
"""
|
||||
Test that device update and source-tracking are atomic.
|
||||
If source tracking fails, the device update should be rolled back.
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Create initial device
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO Devices (
|
||||
devMac, devName, devOwner, devType,
|
||||
devNameSource, devTypeSource
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (test_mac, "Old Name", "Old Owner", "Desktop", "PLUGIN", "PLUGIN"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Patch database connection
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
|
||||
mock_enforce.return_value = None
|
||||
data = {
|
||||
"createNew": False,
|
||||
"devMac": test_mac,
|
||||
"devName": "New Name",
|
||||
"devOwner": "New Owner",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify success
|
||||
self.assertTrue(result["success"], f"Device update failed: {result}")
|
||||
|
||||
# Verify device was updated
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(device["devName"], "New Name")
|
||||
self.assertEqual(device["devOwner"], "New Owner")
|
||||
|
||||
def test_source_tracking_failure_rolls_back_device(self):
|
||||
"""
|
||||
Test that if enforce_source_on_user_update fails, the entire
|
||||
transaction is rolled back (device and sources).
|
||||
"""
|
||||
device_instance = DeviceInstance()
|
||||
test_mac = normalize_mac("aa:bb:cc:dd:ee:ff")
|
||||
|
||||
# Create initial device
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
INSERT INTO Devices (
|
||||
devMac, devName, devOwner, devType,
|
||||
devNameSource, devTypeSource
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
""", (test_mac, "Original Name", "Original Owner", "Desktop", "PLUGIN", "PLUGIN"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# Patch database connection and mock source enforcement failure
|
||||
with patch('models.device_instance.get_temp_db_connection', self._get_test_db_connection):
|
||||
with patch('models.device_instance.enforce_source_on_user_update') as mock_enforce:
|
||||
# Simulate source tracking failure
|
||||
mock_enforce.side_effect = Exception("Source tracking error")
|
||||
|
||||
data = {
|
||||
"createNew": False,
|
||||
"devMac": test_mac,
|
||||
"devName": "Failed Update",
|
||||
"devOwner": "Failed Owner",
|
||||
}
|
||||
|
||||
result = device_instance.setDeviceData(test_mac, data)
|
||||
|
||||
# Verify error response
|
||||
self.assertFalse(result["success"])
|
||||
self.assertIn("Source tracking failed", result["error"])
|
||||
|
||||
# Verify device was NOT updated (rollback successful)
|
||||
conn = self._get_test_db_connection()
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT * FROM Devices WHERE devMac = ?", (test_mac,))
|
||||
device = cur.fetchone()
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(device["devName"], "Original Name", "Device should not have been updated on source tracking failure")
|
||||
self.assertEqual(device["devOwner"], "Original Owner", "Device should not have been updated on source tracking failure")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -16,3 +16,9 @@ def test_normalize_mac_preserves_wildcard():
|
||||
result = normalize_mac("aabbcc*")
|
||||
assert result == "AA:BB:CC:*", f"Expected 'AA:BB:CC:*' but got '{result}'"
|
||||
assert normalize_mac("aa:bb:cc:dd:ee:ff") == "AA:BB:CC:DD:EE:FF"
|
||||
|
||||
|
||||
def test_normalize_mac_preserves_internet_root():
|
||||
assert normalize_mac("internet") == "Internet"
|
||||
assert normalize_mac("Internet") == "Internet"
|
||||
assert normalize_mac("INTERNET") == "Internet"
|
||||
|
||||
Reference in New Issue
Block a user