feat: WMT client versioning, release management and force-update playbook
- api/wmt.py: add GET /api/wmt/client/version and GET /api/wmt/client/download endpoints; rewrite submit_update_request with dedup logic - web/wmt.py: add releases, releases_upload, releases_delete, releases_build routes; build-from-folder excludes hidden/data/venv/pyc files - web/main.py: admin per-device delete route; clear-device-logs route; pass devices list to admin template - templates/wmt/releases.html: new release management page (current release info, upload form, build-from-folder card) - templates/admin.html: replace nuclear clear-devices with clear-logs + per-device delete table - templates/base.html: add Client Releases nav link in WMT sidebar section - templates/ansible/execute.html: add Update WMT Code playbook card - ansible/playbooks/update_wmt_code.yml: rsync WMT_project to clients excluding data/; backs up app.py; restarts wmt service - ansible_service.py: register update_wmt_code description - .gitignore: whitelist update_wmt_code.yml
This commit is contained in:
+115
-13
@@ -2,8 +2,10 @@
|
||||
WMT (Workstation Management Terminal) configuration API
|
||||
Handles config distribution and device update requests from WMT clients.
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask import Blueprint, request, jsonify, send_file
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
from app.models import WMTGlobalConfig, Device, WMTUpdateRequest
|
||||
from config.database_config import get_db
|
||||
import logging
|
||||
@@ -125,7 +127,11 @@ def get_device_config(mac_address):
|
||||
@wmt_api_bp.route('/config/update_request', methods=['POST'])
|
||||
def submit_update_request():
|
||||
"""
|
||||
WMT client sends current device info as an update request for admin approval.
|
||||
WMT client sends current device info. Three outcomes:
|
||||
|
||||
1. Device not registered → create pending WMTUpdateRequest for admin approval.
|
||||
2. Device registered, info UNCHANGED → update last_seen only; no request created.
|
||||
3. Device registered, info CHANGED → create/refresh a pending WMTUpdateRequest.
|
||||
|
||||
Expected JSON:
|
||||
{
|
||||
@@ -133,6 +139,7 @@ def submit_update_request():
|
||||
"device_name": "Masa-01",
|
||||
"hostname": "rpi-masa01",
|
||||
"device_ip": "192.168.1.100",
|
||||
"card_presence": "enable", // optional – applied directly, no approval
|
||||
"client_config_mtime": "2026-04-22T09:30:00" // optional
|
||||
}
|
||||
"""
|
||||
@@ -144,31 +151,126 @@ def submit_update_request():
|
||||
if not mac:
|
||||
return jsonify({'error': 'mac_address is required'}), 400
|
||||
|
||||
proposed_name = (data.get('device_name') or '').strip()
|
||||
proposed_hostname = (data.get('hostname') or '').strip()
|
||||
proposed_ip = (data.get('device_ip') or '').strip()
|
||||
card_presence = data.get('card_presence')
|
||||
|
||||
def _eq(a, b):
|
||||
"""Compare two values treating None and '' as equivalent."""
|
||||
return (a or '') == (b or '')
|
||||
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
device = session.query(Device).filter_by(mac_address=mac).first()
|
||||
|
||||
# Always update last_seen and card_presence (no approval needed)
|
||||
if device:
|
||||
device.last_seen = datetime.utcnow()
|
||||
if card_presence in ('enable', 'disable'):
|
||||
device.card_presence = card_presence
|
||||
|
||||
# --- Determine whether the proposed info differs from the server record ---
|
||||
if device:
|
||||
info_changed = not (
|
||||
_eq(proposed_name, device.nume_masa) and
|
||||
_eq(proposed_hostname, device.hostname) and
|
||||
_eq(proposed_ip, device.device_ip)
|
||||
)
|
||||
else:
|
||||
# Unknown device – always needs admin attention
|
||||
info_changed = True
|
||||
|
||||
if not info_changed:
|
||||
# Heartbeat only – nothing for admin to review
|
||||
logger.debug(f'WMT heartbeat from {mac} – info unchanged, last_seen updated')
|
||||
return jsonify({'status': 'no_change', 'message': 'Device info matches server record'}), 200
|
||||
|
||||
# --- Info changed (or device unknown): avoid duplicate pending requests ---
|
||||
existing = (
|
||||
session.query(WMTUpdateRequest)
|
||||
.filter_by(mac_address=mac, status='pending')
|
||||
.order_by(WMTUpdateRequest.submitted_at.desc())
|
||||
.first()
|
||||
)
|
||||
if existing and (
|
||||
_eq(existing.proposed_device_name, proposed_name) and
|
||||
_eq(existing.proposed_hostname, proposed_hostname) and
|
||||
_eq(existing.proposed_device_ip, proposed_ip)
|
||||
):
|
||||
# Identical pending request already exists – just refresh its timestamp
|
||||
existing.submitted_at = datetime.utcnow()
|
||||
logger.debug(f'WMT duplicate pending request from {mac} – timestamp refreshed')
|
||||
return jsonify({'status': 'pending', 'message': 'Update request already pending admin review'}), 200
|
||||
|
||||
# Create a new update request
|
||||
req = WMTUpdateRequest(
|
||||
mac_address=mac,
|
||||
device_id=device.id if device else None,
|
||||
proposed_device_name=data.get('device_name'),
|
||||
proposed_hostname=data.get('hostname'),
|
||||
proposed_device_ip=data.get('device_ip'),
|
||||
proposed_device_name=proposed_name or None,
|
||||
proposed_hostname=proposed_hostname or None,
|
||||
proposed_device_ip=proposed_ip or None,
|
||||
client_config_mtime=data.get('client_config_mtime'),
|
||||
submitted_at=datetime.utcnow(),
|
||||
status='pending',
|
||||
)
|
||||
session.add(req)
|
||||
|
||||
# Update device last_seen
|
||||
if device:
|
||||
device.last_seen = datetime.utcnow()
|
||||
# card_presence is a device capability flag – update directly (no approval needed)
|
||||
if data.get('card_presence') in ('enable', 'disable'):
|
||||
device.card_presence = data['card_presence']
|
||||
|
||||
logger.info(f'WMT update request received from {mac}')
|
||||
reason = 'new device' if not device else 'info changed'
|
||||
logger.info(f'WMT update request created for {mac} ({reason})')
|
||||
return jsonify({'status': 'received', 'message': 'Update request queued for admin review'}), 201
|
||||
except Exception as e:
|
||||
logger.error(f'Error saving WMT update request from {mac}: {e}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WMT client auto-update endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WMT_RELEASES_DIR = Path('data/wmt_releases')
|
||||
|
||||
|
||||
@wmt_api_bp.route('/client/version', methods=['GET'])
|
||||
def get_client_version():
|
||||
"""
|
||||
Returns the latest available WMT client version metadata.
|
||||
Client calls this to decide if it needs to update.
|
||||
|
||||
Response: { "version": "3.0", "notes": "...", "uploaded_at": "...", "filename": "wmt_v3.0.zip" }
|
||||
"""
|
||||
meta_path = WMT_RELEASES_DIR / 'latest.json'
|
||||
if not meta_path.exists():
|
||||
return jsonify({'error': 'No release available'}), 404
|
||||
try:
|
||||
meta = json.loads(meta_path.read_text())
|
||||
return jsonify(meta), 200
|
||||
except Exception as e:
|
||||
logger.error(f'Error reading WMT release metadata: {e}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@wmt_api_bp.route('/client/download', methods=['GET'])
|
||||
def download_client_release():
|
||||
"""
|
||||
Streams the latest WMT client release zip to the requesting device.
|
||||
Client downloads this when its version is older than the server version.
|
||||
"""
|
||||
meta_path = WMT_RELEASES_DIR / 'latest.json'
|
||||
if not meta_path.exists():
|
||||
return jsonify({'error': 'No release available'}), 404
|
||||
try:
|
||||
meta = json.loads(meta_path.read_text())
|
||||
zip_path = WMT_RELEASES_DIR / meta['filename']
|
||||
if not zip_path.exists():
|
||||
return jsonify({'error': f'Release file not found: {meta["filename"]}'}), 404
|
||||
logger.info(f'WMT client downloading release {meta["version"]} from {request.remote_addr}')
|
||||
return send_file(
|
||||
str(zip_path.resolve()),
|
||||
mimetype='application/zip',
|
||||
as_attachment=True,
|
||||
download_name=meta['filename'],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f'Error serving WMT release: {e}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
Reference in New Issue
Block a user