Add server update alerting and fleet client auto-update system (synchronized release lockstep)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Failing after 1m14s
This commit is contained in:
+209
-6
@@ -14,6 +14,10 @@ from typing import Optional, Dict, Any, List
|
||||
from src.auth_manager import SessionManager
|
||||
from src.license_manager import LicenseManager
|
||||
from src.storage_engine import StorageEngine, StorageCryptoManager
|
||||
try:
|
||||
from src.version import LOGAR_VERSION, is_newer_version
|
||||
except ImportError:
|
||||
from version import LOGAR_VERSION, is_newer_version
|
||||
|
||||
|
||||
def format_relative_time(dt_str: Optional[str]) -> str:
|
||||
@@ -183,11 +187,20 @@ class WebUIRenderer:
|
||||
)
|
||||
pg_config = config.get("postgres", {})
|
||||
|
||||
# Fetch clients
|
||||
update_mgr = state.get("update_manager")
|
||||
update_status = update_mgr.get_status() if update_mgr else {}
|
||||
hosted_client_ver = update_status.get("hosted_client_version") or LOGAR_VERSION
|
||||
hosted_packages = update_status.get("hosted_packages", {})
|
||||
|
||||
# Fetch clients safely
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
c.execute("SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen FROM clients ORDER BY last_seen DESC")
|
||||
clients = c.fetchall()
|
||||
try:
|
||||
c.execute("SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen, COALESCE(client_version, ''), COALESCE(update_status, 'up_to_date') FROM clients ORDER BY last_seen DESC")
|
||||
clients = c.fetchall()
|
||||
except sqlite3.OperationalError:
|
||||
c.execute("SELECT client_id, hostname, os_type, cert_fingerprint, status, first_seen, last_seen FROM clients ORDER BY last_seen DESC")
|
||||
clients = [r + ("", "up_to_date") for r in c.fetchall()]
|
||||
|
||||
# Fetch verified anomalies
|
||||
c.execute("SELECT fingerprint, server, severity, message, run_count, last_seen FROM active_issues WHERE status = 'VERIFIED' ORDER BY last_seen DESC LIMIT 50")
|
||||
@@ -203,10 +216,10 @@ class WebUIRenderer:
|
||||
hermes_tokens = c.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Build client rows with exact + relative activity time and seat management actions
|
||||
# Build client rows with exact + relative activity time, version badges, and seat management actions
|
||||
client_rows = []
|
||||
for cl in clients:
|
||||
cid, host, ost, fp, st, first, last = cl
|
||||
cid, host, ost, fp, st, first, last, cver, up_st = cl[:9]
|
||||
esc_cid = html.escape(str(cid))
|
||||
esc_host = html.escape(str(host))
|
||||
esc_ost = html.escape(str(ost))
|
||||
@@ -216,6 +229,14 @@ class WebUIRenderer:
|
||||
rel_time = format_relative_time(last)
|
||||
last_display = f"<div><strong>{html.escape(str(last[:19]))}</strong></div><div style='font-size:11px; color:#94a3b8;'>{html.escape(rel_time)}</div>"
|
||||
|
||||
# Client Version Display
|
||||
if not cver:
|
||||
ver_display = f'<span class="badge" style="background:#1e293b; color:#94a3b8; font-size:11px;">v{LOGAR_VERSION}</span>'
|
||||
elif is_newer_version(hosted_client_ver, cver):
|
||||
ver_display = f'<span class="badge badge-warning" title="Update available to v{hosted_client_ver}" style="font-size:11px;">v{html.escape(cver)} (Outdated)</span>'
|
||||
else:
|
||||
ver_display = f'<span class="badge badge-active" style="font-size:11px;">v{html.escape(cver)}</span>'
|
||||
|
||||
if fp == "LOCAL-HOST-BUILTIN":
|
||||
esc_st = "LOCAL HUB HOST"
|
||||
badge_cls = "badge-active"
|
||||
@@ -237,6 +258,7 @@ class WebUIRenderer:
|
||||
<td><strong>{esc_host}</strong></td>
|
||||
<td><code>{esc_cid}</code></td>
|
||||
<td><span class="badge badge-os">{esc_ost.upper()}</span></td>
|
||||
<td>{ver_display}</td>
|
||||
<td>{fp_display}</td>
|
||||
<td>{last_display}</td>
|
||||
<td>
|
||||
@@ -247,7 +269,7 @@ class WebUIRenderer:
|
||||
</td>
|
||||
</tr>
|
||||
""")
|
||||
clients_html = "".join(client_rows) if client_rows else '<tr><td colspan="7" class="empty-state">No edge clients registered yet.</td></tr>'
|
||||
clients_html = "".join(client_rows) if client_rows else '<tr><td colspan="8" class="empty-state">No edge clients registered yet.</td></tr>'
|
||||
|
||||
# Build anomaly rows
|
||||
anomaly_rows = []
|
||||
@@ -322,6 +344,26 @@ class WebUIRenderer:
|
||||
</div>
|
||||
"""
|
||||
|
||||
server_update_banner_html = ""
|
||||
update_tab_badge = ""
|
||||
if update_status.get("server_update_available"):
|
||||
up_ver = html.escape(str(update_status.get("latest_upstream_version", "")))
|
||||
update_tab_badge = ' <span class="badge badge-warning" style="font-size:10px; margin-left:4px; padding:2px 6px;">NEW</span>'
|
||||
server_update_banner_html = f"""
|
||||
<div style="background: rgba(59, 130, 246, 0.12); border: 1px solid rgba(59, 130, 246, 0.35); border-radius: 10px; padding: 14px 20px; margin-bottom: 24px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 14px;">
|
||||
<div style="display: flex; align-items: center; gap: 14px;">
|
||||
<div style="font-size: 26px;">🔔</div>
|
||||
<div>
|
||||
<div style="font-weight: 700; color: #60a5fa; font-size: 14px; margin-bottom: 2px;">LOGAR Server Update Available: v{up_ver} (Current: v{LOGAR_VERSION})</div>
|
||||
<div style="font-size: 13px; color: #cbd5e1;">A newer release of LOGAR is available. Deployed server packages automatically upgrade edge clients during their next forwarder check.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<button onclick="switchTab('updates')" class="btn btn-primary" style="font-size: 12px; padding: 7px 14px;">📦 View Updates</button>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
detected_host = (request.headers.get("host", "").split(":")[0] if request and request.headers else "") or config.get("tcp_host", "127.0.0.1")
|
||||
if detected_host in ("0.0.0.0", "localhost", ""):
|
||||
detected_host = socket.gethostname()
|
||||
@@ -571,6 +613,108 @@ class WebUIRenderer:
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 6: Updates & Fleet Distribution -->
|
||||
<div class="tab-pane" id="tabPane-updates">
|
||||
<div class="card" style="margin-bottom: 24px;">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title" style="font-size: 15px;">🌐 Server Version & Upstream Release Status</div>
|
||||
<div class="subtitle" style="margin-top: 4px;">Automatic upstream release checking via Gitea production channels</div>
|
||||
</div>
|
||||
{('<span class="badge badge-warning">UPDATE AVAILABLE</span>' if update_status.get("server_update_available") else '<span class="badge badge-active">UP TO DATE</span>')}
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
||||
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Current Server Version</div>
|
||||
<div style="font-size: 18px; font-weight: 700; color: #38bdf8;">v{LOGAR_VERSION}</div>
|
||||
</div>
|
||||
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Latest Upstream Release</div>
|
||||
<div style="font-size: 18px; font-weight: 700; color: #34d399;">v{update_status.get('latest_upstream_version') or LOGAR_VERSION}</div>
|
||||
</div>
|
||||
<div style="background: #0f172a; padding: 14px 18px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">Last Checked</div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #e2e8f0; margin-top: 3px;">{update_status.get('last_checked_utc') or 'Never'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<button type="button" onclick="checkForUpdatesNow()" class="btn btn-primary">🔍 Check for Upstream Updates</button>
|
||||
{(f'<a href="{html.escape(update_status.get("release_url", ""))}" target="_blank" class="btn btn-secondary">🌐 View Release on Gitea</a>' if update_status.get("release_url") else '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title" style="font-size: 15px;">📦 Client Package Distribution Repository</div>
|
||||
<div class="subtitle" style="margin-top: 4px;">Hosted binaries for edge forwarders to automatically download and hot-upgrade</div>
|
||||
</div>
|
||||
<span class="badge badge-active">Target: v{hosted_client_ver}</span>
|
||||
</div>
|
||||
<div style="overflow-x: auto; margin-bottom: 24px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Target OS</th>
|
||||
<th>Package File</th>
|
||||
<th>Version</th>
|
||||
<th>Size</th>
|
||||
<th>SHA-256 Checksum</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><span class="badge badge-os">WINDOWS</span></td>
|
||||
<td><strong>{html.escape(hosted_packages.get('windows', {}).get('filename') or 'LOGAR_Client_Setup.exe')}</strong></td>
|
||||
<td><code>v{html.escape(str(hosted_packages.get('windows', {}).get('version') or hosted_client_ver))}</code></td>
|
||||
<td>{((hosted_packages.get('windows', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('windows', {}).get('exists') else 0)} KB</td>
|
||||
<td><code title="{html.escape(hosted_packages.get('windows', {}).get('sha256', ''))}">{(hosted_packages.get('windows', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('windows', {}).get('sha256') else 'N/A'}</code></td>
|
||||
<td>{('<span class="badge badge-active">Available</span>' if hosted_packages.get('windows', {}).get('exists') else '<span class="badge badge-warning">Not Uploaded</span>')}</td>
|
||||
<td>
|
||||
<a href="/api/client/update/download?os=windows" class="btn btn-outline" style="padding: 4px 10px; font-size: 12px;">📥 Download</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge badge-os">LINUX</span></td>
|
||||
<td><strong>{html.escape(hosted_packages.get('linux', {}).get('filename') or 'Linux_Client.bin')}</strong></td>
|
||||
<td><code>v{html.escape(str(hosted_packages.get('linux', {}).get('version') or hosted_client_ver))}</code></td>
|
||||
<td>{((hosted_packages.get('linux', {}).get('size_bytes', 0) // 1024) if hosted_packages.get('linux', {}).get('exists') else 0)} KB</td>
|
||||
<td><code title="{html.escape(hosted_packages.get('linux', {}).get('sha256', ''))}">{(hosted_packages.get('linux', {}).get('sha256', 'N/A')[:16] + '...') if hosted_packages.get('linux', {}).get('sha256') else 'N/A'}</code></td>
|
||||
<td>{('<span class="badge badge-active">Available</span>' if hosted_packages.get('linux', {}).get('exists') else '<span class="badge badge-warning">Not Uploaded</span>')}</td>
|
||||
<td>
|
||||
<a href="/api/client/update/download?os=linux" class="btn btn-outline" style="padding: 4px 10px; font-size: 12px;">📥 Download</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border);">
|
||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 12px;">📤 Upload Client Binary Package</div>
|
||||
<form onsubmit="submitClientPackageUpload(event)" style="display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end;">
|
||||
<div class="form-group" style="margin-bottom: 0; min-width: 140px;">
|
||||
<label class="form-label">OS Platform</label>
|
||||
<select id="uploadPkgOs" class="form-control" required>
|
||||
<option value="windows">Windows (.exe)</option>
|
||||
<option value="linux">Linux (.bin / .sh)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0; min-width: 140px;">
|
||||
<label class="form-label">Version Tag</label>
|
||||
<input type="text" id="uploadPkgVer" class="form-control" placeholder="0.3.5.2" value="{hosted_client_ver}">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 220px;">
|
||||
<label class="form-label">Select Binary File</label>
|
||||
<input type="file" id="uploadPkgFile" class="form-control" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent" style="padding: 10px 18px; font-size: 13px;">Upload & Deploy to Fleet</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html_content = f"""<!DOCTYPE html>
|
||||
@@ -797,6 +941,7 @@ class WebUIRenderer:
|
||||
<div id="statusAlert" class="alert"></div>
|
||||
|
||||
{unlicensed_banner_html}
|
||||
{server_update_banner_html}
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="tabs-nav">
|
||||
@@ -805,6 +950,7 @@ class WebUIRenderer:
|
||||
<button class="tab-btn" id="tabBtn-anomalies" onclick="switchTab('anomalies')">📊 Verified Telemetry</button>
|
||||
<button class="tab-btn" id="tabBtn-pki" onclick="switchTab('pki')">📜 Sub-CA & Licensing</button>
|
||||
<button class="tab-btn" id="tabBtn-db" onclick="switchTab('db')">💾 DB Engine</button>
|
||||
<button class="tab-btn" id="tabBtn-updates" onclick="switchTab('updates')">📦 Updates & Distribution{update_tab_badge}</button>
|
||||
</div>
|
||||
|
||||
<!-- KPI Summary Cards -->
|
||||
@@ -867,6 +1013,7 @@ class WebUIRenderer:
|
||||
<th>Host Node</th>
|
||||
<th>Client Identifier</th>
|
||||
<th>OS</th>
|
||||
<th>Version</th>
|
||||
<th>Cert Fingerprint</th>
|
||||
<th>Last Activity Time</th>
|
||||
<th>Seat Actions</th>
|
||||
@@ -1673,6 +1820,62 @@ class WebUIRenderer:
|
||||
img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodeURIComponent(text);
|
||||
el.appendChild(img);
|
||||
}}
|
||||
|
||||
async function checkForUpdatesNow() {{
|
||||
showAlert('Checking upstream repository for new releases...');
|
||||
try {{
|
||||
const res = await fetch('/api/server/update/check', {{ method: 'POST' }});
|
||||
const data = await res.json();
|
||||
if (data.server_update_available) {{
|
||||
showAlert('🔔 Update Available: v' + data.latest_upstream_version + ' is ready!');
|
||||
}} else {{
|
||||
showAlert('✅ LOGAR is up to date (Current: v' + data.current_server_version + ').');
|
||||
}}
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
}} catch (err) {{
|
||||
alert('Failed to check for updates: ' + err.message);
|
||||
}}
|
||||
}}
|
||||
|
||||
async function submitClientPackageUpload(e) {{
|
||||
e.preventDefault();
|
||||
const osType = document.getElementById('uploadPkgOs').value;
|
||||
const ver = document.getElementById('uploadPkgVer').value.trim();
|
||||
const fileInput = document.getElementById('uploadPkgFile');
|
||||
if (!fileInput.files.length) {{
|
||||
alert('Please choose a client binary package file.');
|
||||
return;
|
||||
}}
|
||||
const file = fileInput.files[0];
|
||||
const reader = new FileReader();
|
||||
showAlert('Reading package file (' + Math.round(file.size / 1024) + ' KB)...');
|
||||
reader.onload = async function() {{
|
||||
const b64 = reader.result.split(',')[1] || reader.result;
|
||||
showAlert('Uploading client package for ' + osType + '...');
|
||||
try {{
|
||||
const res = await fetch('/api/server/client-package/upload', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{
|
||||
os: osType,
|
||||
version: ver || undefined,
|
||||
filename: file.name,
|
||||
data_base64: b64
|
||||
}})
|
||||
}});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === 'success') {{
|
||||
showAlert('Client package uploaded successfully! (SHA-256: ' + (data.sha256 ? data.sha256.substring(0, 16) : '') + '...)');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}} else {{
|
||||
alert('Upload failed: ' + (data.detail || data.message || 'Error'));
|
||||
}}
|
||||
}} catch (err) {{
|
||||
alert('Upload request failed: ' + err.message);
|
||||
}}
|
||||
}};
|
||||
reader.readAsDataURL(file);
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
Reference in New Issue
Block a user