From ead8a7bb9dd12616048476d885867ec88cb22c43 Mon Sep 17 00:00:00 2001 From: ske087 Date: Fri, 26 Jun 2026 11:13:20 +0300 Subject: [PATCH] updated host gropups and playbook --- app/api/ansible.py | 4 ++- app/services/ansible_service.py | 51 ++++++++++++++++++++++++++++++-- data/wmt_releases/latest.json | 8 ++--- data/wmt_releases/wmt_v3.0.zip | Bin 0 -> 14527 bytes 4 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 data/wmt_releases/wmt_v3.0.zip diff --git a/app/api/ansible.py b/app/api/ansible.py index 0963237..342e1d9 100644 --- a/app/api/ansible.py +++ b/app/api/ansible.py @@ -37,9 +37,10 @@ def get_inventory_raw(): @ansible_bp.route('/inventory/sync', methods=['POST']) def sync_inventory(): - """Sync all active app devices into monitoring_devices inventory group""" + """Sync all active app devices into monitoring_devices and wmt_clients inventory groups""" try: result = ansible_service.sync_devices_to_inventory() + ansible_service.sync_wmt_devices_to_inventory() status = 200 if result.get('success') else 400 return jsonify(result), status except Exception as e: @@ -116,6 +117,7 @@ def refresh_inventory(): """Refresh Ansible inventory from database (legacy alias for /sync)""" try: result = ansible_service.sync_devices_to_inventory() + ansible_service.sync_wmt_devices_to_inventory() return jsonify(result), 200 if result.get('success') else 400 except Exception as e: logging.error(f"Error refreshing inventory: {e}") diff --git a/app/services/ansible_service.py b/app/services/ansible_service.py index 4e6f908..e39ad36 100644 --- a/app/services/ansible_service.py +++ b/app/services/ansible_service.py @@ -174,6 +174,52 @@ class AnsibleService: logging.error(f"Error syncing devices to inventory: {e}") return {'success': False, 'error': str(e)} + def sync_wmt_devices_to_inventory(self) -> Dict: + """Sync all active WMT-enabled devices into the wmt_clients inventory group. + Only devices that have checked in via the WMT client API (wmt_last_seen IS NOT NULL) + are included. Preserves all other inventory groups.""" + try: + data = self._read_inventory() + children = data['all'].setdefault('children', {}) + children['wmt_clients'] = {'hosts': {}} + synced = 0 + with self.db.get_session() as session: + devices = ( + session.query(Device) + .filter(Device.status == 'active', + Device.wmt_last_seen.isnot(None)) + .all() + ) + settings = self.load_settings() + use_password = settings.get('use_password_auth', False) + ssh_password = settings.get('ssh_fallback_password', '') + for device in devices: + if device.device_ip == '127.0.0.1' or device.hostname == 'localhost': + hvars = {'ansible_connection': 'local', 'ansible_host': '127.0.0.1'} + elif use_password and ssh_password: + hvars = { + 'ansible_host': device.device_ip, + 'ansible_user': 'pi', + 'ansible_password': ssh_password, + 'ansible_become_password': ssh_password, + 'ansible_ssh_common_args': '-o PubkeyAuthentication=no -o PreferredAuthentications=password -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + } + else: + hvars = { + 'ansible_host': device.device_ip, + 'ansible_user': 'pi', + 'ansible_ssh_private_key_file': str(self.ssh_key_path.resolve()), + 'ansible_ssh_common_args': '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null' + } + children['wmt_clients']['hosts'][device.hostname] = hvars + synced += 1 + self._write_inventory(data) + return {'success': True, 'synced': synced, + 'message': f'Synced {synced} WMT device(s) to wmt_clients group'} + except Exception as e: + logging.error(f'Error syncing WMT devices to inventory: {e}') + return {'success': False, 'error': str(e)} + def add_group_to_inventory(self, group_name: str) -> Dict: """Add a new empty group to the inventory.""" import re as _re @@ -193,9 +239,9 @@ class AnsibleService: def remove_group_from_inventory(self, group_name: str) -> Dict: """Remove a custom group from the inventory.""" - if group_name == 'monitoring_devices': + if group_name in ('monitoring_devices', 'wmt_clients'): return {'success': False, - 'error': 'Cannot remove the default monitoring_devices group'} + 'error': f'Cannot remove the built-in "{group_name}" group'} try: data = self._read_inventory() children = data['all'].get('children', {}) or {} @@ -264,6 +310,7 @@ class AnsibleService: def generate_dynamic_inventory(self) -> Dict: """Sync DB devices into inventory and return the full inventory dict.""" self.sync_devices_to_inventory() + self.sync_wmt_devices_to_inventory() return self._read_inventory() def create_update_playbook(self) -> str: diff --git a/data/wmt_releases/latest.json b/data/wmt_releases/latest.json index 0e31deb..6a0f7d9 100644 --- a/data/wmt_releases/latest.json +++ b/data/wmt_releases/latest.json @@ -1,6 +1,6 @@ { - "version": "2.9", - "notes": "", - "uploaded_at": "2026-05-13T13:17:18", - "filename": "wmt_v2.9.zip" + "version": "3.0", + "notes": "Auto-update from server; TestTemplate silent mode; notconfig devices surface in To Be Configured section", + "uploaded_at": "2026-06-26T07:56:55", + "filename": "wmt_v3.0.zip" } \ No newline at end of file diff --git a/data/wmt_releases/wmt_v3.0.zip b/data/wmt_releases/wmt_v3.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..7db3c7b34b989e5ed205dc69f3d32716dc812e57 GIT binary patch literal 14527 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