From c591ab9ce3eff25f7a1ea8b19c1d7fce1ba0658d Mon Sep 17 00:00:00 2001 From: jokob-sk Date: Sat, 8 Jun 2024 12:29:03 +1000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=83=20Sync=20Hub=20v0.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/img/NetAlertX_logo.png | Bin 6098 -> 5725 bytes front/img/NetAlertX_logo_notification.png | Bin 5782 -> 6098 bytes front/img/NetAlertX_logo_red.png | Bin 0 -> 6098 bytes ...ion_white.png => NetAlertX_logo_white.png} | Bin ...n_yellow.png => NetAlertX_logo_yellow.png} | Bin front/php/templates/header.php | 2 +- front/plugins/sync/README.md | 48 +++++- front/plugins/sync/config.json | 152 +++++++++--------- front/plugins/sync/hub.php | 6 +- front/plugins/sync/sync.py | 31 ++-- server/plugin.py | 14 +- 11 files changed, 157 insertions(+), 96 deletions(-) create mode 100755 front/img/NetAlertX_logo_red.png rename front/img/{NetAlertX_logo_notification_white.png => NetAlertX_logo_white.png} (100%) rename front/img/{NetAlertX_logo_notification_yellow.png => NetAlertX_logo_yellow.png} (100%) diff --git a/front/img/NetAlertX_logo.png b/front/img/NetAlertX_logo.png index 5a18b9fe9399d07504859e27ab06513a3290023d..a3bfaec79d3731e14ebdee2f0e8e914c3c645fe6 100755 GIT binary patch literal 5725 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
  • - 0 + 0
  • diff --git a/front/plugins/sync/README.md b/front/plugins/sync/README.md index 4f03eff6..ebf02d22 100755 --- a/front/plugins/sync/README.md +++ b/front/plugins/sync/README.md @@ -1,6 +1,52 @@ ## Overview -Synchronization plugin to synchronize multiple app instances. The plugin sends encrypted `last_result.log` files for individual plugins. +Synchronization plugin to synchronize multiple app instances. The Plugin can sychronize 2 types of data: + +1. 💻 Devices: The plugin sends an encrypted `table_devices.json` file to synchronize the whole Devices DB table. +1. 🔌 Plugin data: The plugin sends encrypted `last_result.log` files for individual plugins. + +### Synchronizing 💻 Devices data + +This is probably what most of the setups will use. Required settings follow. + +#### Node (Source) Settings + +- When to run `SYNC_RUN` +- Schedule `SYNC_RUN_SCHD` +- API token `SYNC_api_token` +- Encryption Key `SYNC_encryption_key` +- Node name `SYNC_node_name` +- Hub URL `SYNC_hub_url` +- Send Devices `SYNC_devices` 👈 + +#### Hub (Target) Settings + +- When to run `SYNC_RUN` +- Schedule `SYNC_RUN_SCHD` +- API token `SYNC_api_token` +- Encryption Key `SYNC_encryption_key` + +### Synchronizing 🔌 Plugins data + +This mechanism will be probably used in special use cases. Required settings follow. + +#### Node (Source) Settings + +- When to run `SYNC_RUN` +- Schedule `SYNC_RUN_SCHD` +- API token `SYNC_api_token` +- Encryption Key `SYNC_encryption_key` +- Node name `SYNC_node_name` +- Hub URL `SYNC_hub_url` +- Send Plugins `SYNC_plugins` 👈 + +#### Hub (Target) Settings + +- When to run `SYNC_RUN` +- Schedule `SYNC_RUN_SCHD` +- API token `SYNC_api_token` +- Encryption Key `SYNC_encryption_key` + ### Usage diff --git a/front/plugins/sync/config.json b/front/plugins/sync/config.json index 4c92ba24..dda81414 100755 --- a/front/plugins/sync/config.json +++ b/front/plugins/sync/config.json @@ -81,6 +81,82 @@ "string" : "When the node sync should run. Data might be lost if you run the sync less frequently. Good options are always_after_scan, on_new_device, on_notification" }] }, + { + "function": "RUN_SCHD", + "type": "text", + "display_condition": { + "type" : "setting", + "name" : "SYNC_instance_type", + "value": "hub" + }, + "default_value":"*/5 * * * *", + "options": [], + "localized": ["name", "description"], + "name" : [{ + "language_code":"en_us", + "string" : "Schedule" + }, + { + "language_code":"es_es", + "string" : "Schedule" + }, + { + "language_code":"de_de", + "string" : "Schedule" + }], + "description": [{ + "language_code":"en_us", + "string" : "Only enabled if you select schedule in the SYNC_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes." + }, + { + "language_code":"es_es", + "string" : "Solo está habilitado si selecciona schedule en la configuración SYNC_RUN. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en crontab.guru). Por ejemplo, ingresar 0 4 * * * ejecutará el escaneo después de las 4 a.m. en el TIMEZONE que configuró arriba. Se ejecutará la PRÓXIMA vez que pase el tiempo." + }, + { + "language_code":"de_de", + "string" : "Nur aktiviert, wenn Sie schedule in der SYNC_RUN-Einstellung auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter crontab.guru). Wenn Sie beispielsweise 0 4 * * * eingeben, wird der Scan nach 4 Uhr morgens in der TIMEZONE den Sie oben festgelegt haben. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht." + }] + }, + { + "function": "api_token", + "type": "text", + "maxLength": 50, + "default_value": "", + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "API token" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "API token to secure communication. The API token needs to be the same on the hub and on the nodes." + } + ] + }, + { + "function": "encryption_key", + "type": "text", + "maxLength": 50, + "default_value": "", + "options": [], + "localized": ["name", "description"], + "name": [ + { + "language_code": "en_us", + "string": "Encryption Key" + } + ], + "description": [ + { + "language_code": "en_us", + "string": "Encryption key used to encrypt the sent data. The key needs to be the same on the hub and on the nodes." + } + ] + }, { "function": "hub_url", "type": "text", @@ -183,46 +259,6 @@ } ] }, - { - "function": "api_token", - "type": "text", - "maxLength": 50, - "default_value": "", - "options": [], - "localized": ["name", "description"], - "name": [ - { - "language_code": "en_us", - "string": "API token" - } - ], - "description": [ - { - "language_code": "en_us", - "string": "API token to secure communication. The API token needs to be the same on the hub and on the nodes." - } - ] - }, - { - "function": "encryption_key", - "type": "text", - "maxLength": 50, - "default_value": "", - "options": [], - "localized": ["name", "description"], - "name": [ - { - "language_code": "en_us", - "string": "Encryption Key" - } - ], - "description": [ - { - "language_code": "en_us", - "string": "Encryption key used to encrypt the sent data. The key needs to be the same on the hub and on the nodes." - } - ] - }, { "function": "CMD", "type": "readonly", @@ -258,42 +294,6 @@ } ] }, - { - "function": "RUN_SCHD", - "type": "text", - "display_condition": { - "type" : "setting", - "name" : "SYNC_instance_type", - "value": "hub" - }, - "default_value":"*/5 * * * *", - "options": [], - "localized": ["name", "description"], - "name" : [{ - "language_code":"en_us", - "string" : "Schedule" - }, - { - "language_code":"es_es", - "string" : "Schedule" - }, - { - "language_code":"de_de", - "string" : "Schedule" - }], - "description": [{ - "language_code":"en_us", - "string" : "Only enabled if you select schedule in the SYNC_RUN setting. Make sure you enter the schedule in the correct cron-like format (e.g. validate at crontab.guru). For example entering 0 4 * * * will run the scan after 4 am in the TIMEZONE you set above. Will be run NEXT time the time passes." - }, - { - "language_code":"es_es", - "string" : "Solo está habilitado si selecciona schedule en la configuración SYNC_RUN. Asegúrese de ingresar la programación en el formato similar a cron correcto (por ejemplo, valide en crontab.guru). Por ejemplo, ingresar 0 4 * * * ejecutará el escaneo después de las 4 a.m. en el TIMEZONE que configuró arriba. Se ejecutará la PRÓXIMA vez que pase el tiempo." - }, - { - "language_code":"de_de", - "string" : "Nur aktiviert, wenn Sie schedule in der SYNC_RUN-Einstellung auswählen. Stellen Sie sicher, dass Sie den Zeitplan im richtigen Cron-ähnlichen Format eingeben (z. B. validieren unter crontab.guru). Wenn Sie beispielsweise 0 4 * * * eingeben, wird der Scan nach 4 Uhr morgens in der TIMEZONE den Sie oben festgelegt haben. Wird das NÄCHSTE Mal ausgeführt, wenn die Zeit vergeht." - }] - }, { "function": "RUN_TIMEOUT", "type": "integer", diff --git a/front/plugins/sync/hub.php b/front/plugins/sync/hub.php index 941f6ec7..3fc560ba 100755 --- a/front/plugins/sync/hub.php +++ b/front/plugins/sync/hub.php @@ -35,8 +35,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } // Generate a unique file path to avoid overwriting existing files - $files = glob("{$storage_path}/last_result.{encoded,decoded}.{$node_name}.*.log", GLOB_BRACE); + $encoded_files = glob("{$storage_path}/last_result.encoded.{$node_name}.*.log"); + $decoded_files = glob("{$storage_path}/last_result.decoded.{$node_name}.*.log"); + + $files = array_merge($encoded_files, $decoded_files); $file_count = count($files) + 1; + $file_path = "{$storage_path}/last_result.encoded.{$node_name}.{$file_count}.log"; diff --git a/front/plugins/sync/sync.py b/front/plugins/sync/sync.py index 9abe3498..dae53408 100755 --- a/front/plugins/sync/sync.py +++ b/front/plugins/sync/sync.py @@ -128,24 +128,18 @@ def main(): unique_mac_addresses.add(device['dev_MAC']) device_data.append(device) + + if len(device_data) > 0: # Retrieve existing dev_MAC values from the Devices table placeholders = ', '.join('?' for _ in unique_mac_addresses) cursor.execute(f'SELECT dev_MAC FROM Devices WHERE dev_MAC IN ({placeholders})', tuple(unique_mac_addresses)) existing_mac_addresses = set(row[0] for row in cursor.fetchall()) + - # Filter out existing devices - new_devices = [device for device in device_data if device['dev_MAC'] not in existing_mac_addresses] - - # Remove 'rowid' key if it exists - for device in new_devices: - device.pop('rowid', None) - - - # Prepare the insert statement - if new_devices: - # insert devices into the lats_result.log to manage state - for device in new_devices: + # insert devices into the lats_result.log to manage state + for device in device_data: + if device['dev_PresentLastScan'] == 1: plugin_objects.add_object( primaryId = device['dev_MAC'], secondaryId = device['dev_LastIP'], @@ -156,6 +150,19 @@ def main(): extra = '', foreignKey = device['dev_GUID']) + # Filter out existing devices + new_devices = [device for device in device_data if device['dev_MAC'] not in existing_mac_addresses] + + # Remove 'rowid' key if it exists + for device in new_devices: + device.pop('rowid', None) + + mylog('verbose', [f'[{pluginName}] All devices: "{len(device_data)}"']) + mylog('verbose', [f'[{pluginName}] New devices: "{len(new_devices)}"']) + + # Prepare the insert statement + if new_devices: + columns = ', '.join(k for k in new_devices[0].keys() if k != 'rowid') placeholders = ', '.join('?' for k in new_devices[0] if k != 'rowid') sql = f'INSERT INTO Devices ({columns}) VALUES ({placeholders})' diff --git a/server/plugin.py b/server/plugin.py index af63a469..b6b2bf58 100755 --- a/server/plugin.py +++ b/server/plugin.py @@ -225,13 +225,15 @@ def execute_plugin(db, all_plugins, plugin, pluginsState = plugins_state() ): file_dir = os.path.join(pluginsPath, plugin["code_name"]) file_prefix = 'last_result' - # Decode files, rename them, and get the list of files files_to_process = decode_and_rename_files(file_dir, file_prefix) for filename in files_to_process: + + full_path = os.path.join(file_dir, filename) + # Open the decrypted file and process its contents - with open(os.path.join(file_dir, filename), 'r') as f: + with open(full_path, 'r') as f: newLines = f.read().split('\n') # if the script produced some output, clean it up to ensure it's the correct format @@ -271,9 +273,11 @@ def execute_plugin(db, all_plugins, plugin, pluginsState = plugins_state() ): ) else: mylog('none', ['[Plugins] Skipped invalid line in the output: ', line]) - - # TODO: delete processed files - # os.rename(file_path, os.path.join(file_dir, new_filename)) + + # keep current instance log file, delete all from other nodes + if filename != 'last_result.log' and os.path.exists(full_path): + os.remove(full_path) + mylog('verbose', [f'[Plugins] Processed and deleted file: {full_path} ']) # app-db-query