added gunicorn and updated to the last version of server monitorizare
This commit is contained in:
@@ -129,10 +129,20 @@ def execute():
|
||||
seen.add(h['hostname'])
|
||||
|
||||
settings = ansible_service.load_settings()
|
||||
|
||||
# Discover custom playbooks (exclude built-ins that have dedicated buttons)
|
||||
_builtin_names = {'update_devices', 'restart_service', 'distribute_ssh_keys', 'system_health'}
|
||||
custom_playbooks = []
|
||||
if ansible_service.playbook_dir.exists():
|
||||
for _f in sorted(ansible_service.playbook_dir.glob('*.yml')):
|
||||
if _f.stem.lower() not in _builtin_names:
|
||||
custom_playbooks.append({'name': _f.stem, 'filename': _f.name})
|
||||
|
||||
return render_template('ansible/execute.html',
|
||||
inventory=inventory_data,
|
||||
all_inv_hosts=all_inv_hosts,
|
||||
preselect_playbook=preselect,
|
||||
custom_playbooks=custom_playbooks,
|
||||
use_password_auth=settings.get('use_password_auth', False))
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading execute form: {e}")
|
||||
@@ -141,6 +151,7 @@ def execute():
|
||||
inventory={'groups': {}},
|
||||
all_inv_hosts=[],
|
||||
preselect_playbook='',
|
||||
custom_playbooks=[],
|
||||
use_password_auth=False)
|
||||
|
||||
elif request.method == 'POST':
|
||||
@@ -462,7 +473,7 @@ def playbook_content():
|
||||
from pathlib import Path
|
||||
requested_path = Path(playbook_path)
|
||||
if not requested_path.is_absolute():
|
||||
requested_path = ansible_service.playbook_dir / requested_path
|
||||
requested_path = Path.cwd() / requested_path
|
||||
|
||||
# Ensure path is within playbook directory
|
||||
try:
|
||||
|
||||
@@ -34,7 +34,7 @@ def devices():
|
||||
with get_db().get_session() as session:
|
||||
devices = session.query(Device).order_by(Device.last_seen.desc()).all()
|
||||
|
||||
# Get log count per device
|
||||
# Log count per device
|
||||
device_log_counts = {}
|
||||
for device in devices:
|
||||
log_count = session.query(LogEntry).filter_by(device_id=device.id).count()
|
||||
@@ -42,14 +42,22 @@ def devices():
|
||||
|
||||
pending_count = session.query(WMTUpdateRequest).filter_by(status='pending').count()
|
||||
|
||||
# Pending approval requests mapped by mac_address for per-device badge
|
||||
pending_requests = session.query(WMTUpdateRequest).filter_by(status='pending').all()
|
||||
pending_by_mac = {}
|
||||
for req in pending_requests:
|
||||
pending_by_mac[req.mac_address] = pending_by_mac.get(req.mac_address, 0) + 1
|
||||
|
||||
return render_template('device_management.html',
|
||||
devices=devices,
|
||||
device_log_counts=device_log_counts,
|
||||
pending_count=pending_count)
|
||||
pending_count=pending_count,
|
||||
pending_by_mac=pending_by_mac)
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading devices: {e}")
|
||||
flash(f'Error loading devices: {e}', 'error')
|
||||
return render_template('device_management.html', devices=[], device_log_counts={}, pending_count=0)
|
||||
return render_template('device_management.html', devices=[], device_log_counts={},
|
||||
pending_count=0, pending_by_mac={})
|
||||
|
||||
@main_bp.route('/device/<int:device_id>')
|
||||
def device_detail(device_id):
|
||||
@@ -516,7 +524,18 @@ def admin():
|
||||
logging.error(f'Admin inventory stats error: {e}')
|
||||
stats['inventory_hosts'] = '?'
|
||||
stats['inventory_groups_yaml'] = '?'
|
||||
return render_template('admin.html', stats=stats)
|
||||
# Pass registered devices for per-device delete UI
|
||||
devices = []
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
devices = [
|
||||
{'id': d.id, 'hostname': d.hostname, 'nume_masa': d.nume_masa,
|
||||
'mac_address': d.mac_address, 'device_ip': d.device_ip}
|
||||
for d in session.query(Device).order_by(Device.nume_masa).all()
|
||||
]
|
||||
except Exception as e:
|
||||
logging.error(f'Admin device list error: {e}')
|
||||
return render_template('admin.html', stats=stats, devices=devices)
|
||||
|
||||
|
||||
@main_bp.route('/admin/clear/logs', methods=['POST'])
|
||||
@@ -532,9 +551,43 @@ def admin_clear_logs():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@main_bp.route('/admin/clear/device-logs', methods=['POST'])
|
||||
def admin_clear_device_logs():
|
||||
"""Delete all log entries from the database (devices stay intact)."""
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
count = session.query(LogEntry).delete()
|
||||
session.commit()
|
||||
return jsonify({'success': True, 'deleted': count})
|
||||
except Exception as e:
|
||||
logging.error(f'Admin clear device logs error: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@main_bp.route('/admin/delete/device/<int:device_id>', methods=['POST'])
|
||||
def admin_delete_device(device_id):
|
||||
"""Delete a single registered device and its log entries."""
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
device = session.query(Device).filter_by(id=device_id).first()
|
||||
if not device:
|
||||
return jsonify({'success': False, 'error': 'Device not found'}), 404
|
||||
name = device.nume_masa or device.hostname
|
||||
session.execute(text('DELETE FROM device_inventory_groups WHERE device_id = :id'), {'id': device_id})
|
||||
session.query(LogEntry).filter_by(device_id=device_id).delete()
|
||||
session.query(WMTUpdateRequest).filter_by(device_id=device_id).delete()
|
||||
session.delete(device)
|
||||
session.commit()
|
||||
logging.info(f'Admin deleted device {device_id} ({name})')
|
||||
return jsonify({'success': True, 'name': name})
|
||||
except Exception as e:
|
||||
logging.error(f'Admin delete device {device_id} error: {e}')
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@main_bp.route('/admin/clear/devices', methods=['POST'])
|
||||
def admin_clear_devices():
|
||||
"""Delete all devices (and their log entries) from the database."""
|
||||
"""Delete ALL devices (and their log entries) from the database."""
|
||||
try:
|
||||
with get_db().get_session() as session:
|
||||
session.execute(text('DELETE FROM device_inventory_groups'))
|
||||
|
||||
@@ -3,8 +3,12 @@ WMT management web routes – global settings, device registry, update requests.
|
||||
"""
|
||||
import csv
|
||||
import io
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, Response
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, Response, jsonify
|
||||
from datetime import datetime, timezone
|
||||
from app.models import WMTGlobalConfig, Device, WMTUpdateRequest
|
||||
from config.database_config import get_db
|
||||
import logging
|
||||
@@ -295,6 +299,8 @@ def device_edit(device_id):
|
||||
device.location = request.form.get('location', '').strip() or None
|
||||
device.card_presence = request.form.get('card_presence', 'enable')
|
||||
device.description = request.form.get('notes', '').strip() or None
|
||||
custom_url = request.form.get('custom_chrome_url', '').strip()
|
||||
device.custom_chrome_url = custom_url if custom_url else None
|
||||
device.config_updated_at = datetime.utcnow()
|
||||
device.info_reviewed_at = datetime.utcnow()
|
||||
flash('Device updated.', 'success')
|
||||
@@ -363,3 +369,196 @@ def device_delete(device_id):
|
||||
logger.error(f'WMT device_delete error: {e}')
|
||||
flash(f'Error: {e}', 'error')
|
||||
return redirect(url_for('wmt_web.devices'))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# WMT Client Release Management
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
WMT_RELEASES_DIR = Path('data/wmt_releases')
|
||||
|
||||
|
||||
def _read_release_meta():
|
||||
meta_path = WMT_RELEASES_DIR / 'latest.json'
|
||||
if meta_path.exists():
|
||||
try:
|
||||
return json.loads(meta_path.read_text())
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
@wmt_web_bp.route('/releases')
|
||||
def releases():
|
||||
"""WMT client release management page."""
|
||||
meta = _read_release_meta()
|
||||
zip_size = None
|
||||
if meta:
|
||||
zip_path = WMT_RELEASES_DIR / meta.get('filename', '')
|
||||
if zip_path.exists():
|
||||
zip_size = zip_path.stat().st_size
|
||||
return render_template('wmt/releases.html', meta=meta, zip_size=zip_size)
|
||||
|
||||
|
||||
@wmt_web_bp.route('/releases/upload', methods=['POST'])
|
||||
def releases_upload():
|
||||
"""Upload a new WMT client release zip and set it as latest."""
|
||||
f = request.files.get('release_zip')
|
||||
version_str = request.form.get('version', '').strip()
|
||||
notes = request.form.get('notes', '').strip()
|
||||
|
||||
if not f or not f.filename.endswith('.zip'):
|
||||
flash('Please upload a .zip file.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
if not version_str:
|
||||
flash('Version is required.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
# Validate version format (digits and dots)
|
||||
if not re.match(r'^\d+(\.\d+)*$', version_str):
|
||||
flash('Version must be in numeric format (e.g. 3.0 or 3.1.2).', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
try:
|
||||
WMT_RELEASES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
filename = f'wmt_v{version_str}.zip'
|
||||
zip_path = WMT_RELEASES_DIR / filename
|
||||
|
||||
# Validate the zip contains app.py
|
||||
data = f.read()
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
||||
names = zf.namelist()
|
||||
if 'app.py' not in names:
|
||||
flash('Invalid release: zip must contain app.py at the root.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
except zipfile.BadZipFile:
|
||||
flash('Uploaded file is not a valid zip archive.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
zip_path.write_bytes(data)
|
||||
|
||||
meta = {
|
||||
'version': version_str,
|
||||
'notes': notes,
|
||||
'uploaded_at': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
'filename': filename,
|
||||
}
|
||||
(WMT_RELEASES_DIR / 'latest.json').write_text(json.dumps(meta, indent=2))
|
||||
|
||||
logger.info(f'New WMT release uploaded: v{version_str} ({filename})')
|
||||
flash(f'Release v{version_str} uploaded and set as latest.', 'success')
|
||||
except Exception as e:
|
||||
logger.error(f'WMT release upload error: {e}')
|
||||
flash(f'Upload error: {e}', 'error')
|
||||
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
|
||||
@wmt_web_bp.route('/releases/delete', methods=['POST'])
|
||||
def releases_delete():
|
||||
"""Remove the current release zip and metadata."""
|
||||
try:
|
||||
meta = _read_release_meta()
|
||||
if meta:
|
||||
zip_path = WMT_RELEASES_DIR / meta.get('filename', '')
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
meta_path = WMT_RELEASES_DIR / 'latest.json'
|
||||
if meta_path.exists():
|
||||
meta_path.unlink()
|
||||
flash('Release deleted.', 'success')
|
||||
except Exception as e:
|
||||
logger.error(f'WMT release delete error: {e}')
|
||||
flash(f'Error: {e}', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
|
||||
# Patterns excluded when building a release from a local folder
|
||||
_BUILD_EXCLUDE_DIRS = {'.git', '__pycache__', 'data', 'venv', '.venv', 'node_modules'}
|
||||
_BUILD_EXCLUDE_EXTS = {'.pyc', '.pyo', '.bak', '.log', '.swp'}
|
||||
|
||||
|
||||
def _should_exclude(rel_parts):
|
||||
"""Return True if this path should be left out of the release zip."""
|
||||
# Any hidden segment (starts with '.')
|
||||
for part in rel_parts:
|
||||
if part.startswith('.'):
|
||||
return True
|
||||
# Top-level or nested dir exclusion
|
||||
if rel_parts[0] in _BUILD_EXCLUDE_DIRS:
|
||||
return True
|
||||
# Extension exclusion
|
||||
if Path(rel_parts[-1]).suffix.lower() in _BUILD_EXCLUDE_EXTS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@wmt_web_bp.route('/releases/build', methods=['POST'])
|
||||
def releases_build():
|
||||
"""
|
||||
Build a release zip directly from a folder on this server,
|
||||
excluding hidden files, __pycache__, data/, venv/, etc.
|
||||
"""
|
||||
folder_path = request.form.get('folder_path', '/home/pi/Desktop/WMT').strip()
|
||||
version_str = request.form.get('version', '').strip()
|
||||
notes = request.form.get('notes', '').strip()
|
||||
|
||||
if not version_str:
|
||||
flash('Version is required.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
if not re.match(r'^\d+(\.\d+)*$', version_str):
|
||||
flash('Version must be in numeric format (e.g. 3.0).', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
src = Path(folder_path)
|
||||
if not src.is_dir():
|
||||
flash(f'Folder not found: {folder_path}', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
try:
|
||||
buf = io.BytesIO()
|
||||
file_count = 0
|
||||
has_app_py = False
|
||||
|
||||
with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
for abs_path in sorted(src.rglob('*')):
|
||||
if not abs_path.is_file():
|
||||
continue
|
||||
rel = abs_path.relative_to(src)
|
||||
parts = rel.parts
|
||||
if _should_exclude(parts):
|
||||
continue
|
||||
arcname = str(rel)
|
||||
zf.write(abs_path, arcname)
|
||||
file_count += 1
|
||||
if arcname == 'app.py':
|
||||
has_app_py = True
|
||||
|
||||
if not has_app_py:
|
||||
flash(f'No app.py found in {folder_path} — cannot create release.', 'error')
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
WMT_RELEASES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
filename = f'wmt_v{version_str}.zip'
|
||||
zip_path = WMT_RELEASES_DIR / filename
|
||||
zip_path.write_bytes(buf.getvalue())
|
||||
|
||||
meta = {
|
||||
'version': version_str,
|
||||
'notes': notes,
|
||||
'uploaded_at': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
'filename': filename,
|
||||
}
|
||||
(WMT_RELEASES_DIR / 'latest.json').write_text(json.dumps(meta, indent=2))
|
||||
|
||||
size_kb = len(buf.getvalue()) // 1024
|
||||
logger.info(f'WMT release built from {folder_path}: v{version_str}, {file_count} files, {size_kb} KB')
|
||||
flash(f'Release v{version_str} built from {folder_path} ({file_count} files, {size_kb} KB).', 'success')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'WMT release build error: {e}')
|
||||
flash(f'Build error: {e}', 'error')
|
||||
|
||||
return redirect(url_for('wmt_web.releases'))
|
||||
|
||||
Reference in New Issue
Block a user