Files
enterprise_digital-platform/Server_Monitorizare_v2/app/api/wmt.py
T

303 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
WMT (Workstation Management Terminal) configuration API
Handles config distribution and device update requests from WMT clients.
"""
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
logger = logging.getLogger(__name__)
wmt_api_bp = Blueprint('wmt_api', __name__, url_prefix='/api/wmt')
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _get_or_create_global_config(session):
"""Return the single WMTGlobalConfig row, creating it with defaults if absent."""
cfg = session.query(WMTGlobalConfig).first()
if cfg is None:
cfg = WMTGlobalConfig()
session.add(cfg)
session.flush()
return cfg
def _latest_config_ts(session, mac_address):
"""Return timestamps for global config and this device's admin-reviewed info."""
global_cfg = session.query(WMTGlobalConfig).first()
global_ts = global_cfg.updated_at if global_cfg and global_cfg.updated_at else datetime(1970, 1, 1)
device = session.query(Device).filter_by(mac_address=mac_address).first()
# Use info_reviewed_at as the authoritative device-level timestamp
device_ts = device.info_reviewed_at if device and device.info_reviewed_at else datetime(1970, 1, 1)
latest = max(global_ts, device_ts)
return global_ts, device_ts, latest
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@wmt_api_bp.route('/config/timestamp', methods=['GET'])
def get_config_timestamp():
"""
Returns the last-modified timestamps for global config and this device's config.
Query param: mac=<mac_address>
Response:
{
"global_updated_at": "2026-04-22T10:00:00",
"device_updated_at": "2026-04-22T09:00:00", // null if device unknown
"latest_updated_at": "2026-04-22T10:00:00"
}
"""
mac = request.args.get('mac', '').strip().lower()
if not mac:
return jsonify({'error': 'mac query parameter is required'}), 400
try:
with get_db().get_session() as session:
global_ts, device_info_reviewed_ts, latest = _latest_config_ts(session, mac)
return jsonify({
'global_updated_at': global_ts.isoformat() if global_ts != datetime(1970, 1, 1) else None,
'device_info_reviewed_at': device_info_reviewed_ts.isoformat() if device_info_reviewed_ts != datetime(1970, 1, 1) else None,
'latest_updated_at': latest.isoformat(),
}), 200
except Exception as e:
logger.error(f'Error getting WMT config timestamp: {e}')
return jsonify({'error': str(e)}), 500
@wmt_api_bp.route('/config/<mac_address>', methods=['GET'])
def get_device_config(mac_address):
"""
Returns merged config (global settings + device-specific) for a given MAC.
Used by WMT client to pull updated config at startup.
Response: merged dict consumable by the WMT config.txt writer.
"""
mac = mac_address.strip().lower()
try:
with get_db().get_session() as session:
global_cfg = _get_or_create_global_config(session)
device = session.query(Device).filter_by(mac_address=mac).first()
# Update last_seen if device is known
if device:
device.last_seen = datetime.utcnow()
_, device_ts, latest_ts = _latest_config_ts(session, mac)
payload = {
# Global settings (device custom_chrome_url overrides global chrome_url if set)
'chrome_url': (device.custom_chrome_url if device and device.custom_chrome_url else global_cfg.chrome_url),
'chrome_local_url': global_cfg.chrome_local_url or '',
'chrome_insecure_origin': global_cfg.chrome_insecure_origin,
'card_api_base_url': global_cfg.card_api_base_url,
'server_log_url': global_cfg.server_log_url,
'internet_check_host': global_cfg.internet_check_host,
'update_host': global_cfg.update_host,
'update_user': global_cfg.update_user,
# Device-specific settings (empty string if unknown)
'device_name': device.device_name if device else '',
'hostname': device.hostname if device else '',
'device_ip': device.device_ip if device else '',
'location': device.location if device else '',
'card_presence': device.card_presence if device else 'enable',
# Admin-review timestamp for device info (client stores in [device] section)
'info_reviewed_at': device.info_reviewed_at.isoformat() if (device and device.info_reviewed_at) else '1970-01-01T00:00:00',
# Sync metadata
'config_updated_at': latest_ts.isoformat(),
}
return jsonify(payload), 200
except Exception as e:
logger.error(f'Error fetching WMT config for {mac}: {e}')
return jsonify({'error': str(e)}), 500
@wmt_api_bp.route('/config/update_request', methods=['POST'])
def submit_update_request():
"""
WMT client sends its current running config so the server can validate it.
Three outcomes:
1. Device known AND config matches server record
→ record config_synced_at = now, update last_seen
→ respond {"status": "ok", "in_sync": true}
Client does nothing.
2. Device known BUT config differs from server record
→ the server is authoritative; client must pull fresh config
→ respond {"status": "sync_required", "in_sync": false}
Client should call GET /api/wmt/config/<mac> to obtain correct values.
3. Device unknown (new client not registered by MAC or hostname)
→ create WMTUpdateRequest so admin can approve and assign settings
→ respond {"status": "pending_approval"}
Client waits; it will get real config once admin approves.
card_presence is always applied directly no approval required.
Expected JSON:
{
"mac_address": "b8:27:eb:aa:bb:cc",
"device_name": "Masa-01",
"hostname": "rpi-masa01",
"device_ip": "192.168.1.100",
"card_presence": "enable",
"client_config_mtime": "2026-04-22T09:30:00"
}
"""
if not request.is_json:
return jsonify({'error': 'Content-Type must be application/json'}), 400
data = request.get_json()
mac = (data.get('mac_address') or '').strip().lower()
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):
return (a or '') == (b or '')
try:
with get_db().get_session() as session:
device = session.query(Device).filter_by(mac_address=mac).first()
# ── Outcome 3: unknown device ─────────────────────────────
if not device:
# Check if a pending request with the same data already exists
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)
):
existing.submitted_at = datetime.utcnow()
logger.debug(f'WMT unknown device {mac} refreshed existing pending request')
else:
req = WMTUpdateRequest(
mac_address=mac,
device_id=None,
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)
logger.info(f'WMT pending approval request created for unknown device {mac}')
return jsonify({
'status': 'pending_approval',
'message': 'Device not registered. Awaiting admin approval.',
}), 202
# Device is known from here on ─────────────────────────────
device.last_seen = datetime.utcnow()
if card_presence in ('enable', 'disable'):
device.card_presence = card_presence
# ── Outcome 1: config matches server record ────────────────
config_in_sync = (
_eq(proposed_name, device.nume_masa) and
_eq(proposed_hostname, device.hostname) and
_eq(proposed_ip, device.device_ip)
)
if config_in_sync:
device.config_synced_at = datetime.utcnow()
logger.debug(f'WMT config OK for {mac} synced_at updated')
return jsonify({
'status': 'ok',
'in_sync': True,
'message': 'Config matches server record.',
}), 200
# ── Outcome 2: config differs client must pull from server ─
logger.info(
f'WMT config mismatch for {mac}: '
f'client has name={proposed_name!r} host={proposed_hostname!r} ip={proposed_ip!r} '
f'but server has name={device.nume_masa!r} host={device.hostname!r} ip={device.device_ip!r}'
)
return jsonify({
'status': 'sync_required',
'in_sync': False,
'message': 'Config differs from server record. Pull updated config.',
}), 200
except Exception as e:
logger.error(f'Error processing WMT config check 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