feat(hub): add auto-update switches for on/off & acquisition modes and remove manual download buttons from webui
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
This commit is contained in:
+97
-4
@@ -86,7 +86,8 @@ from src.server_models import (
|
||||
DbSwitchBackendRequest,
|
||||
DbFullConfigRequest,
|
||||
ClientUpdateRequest,
|
||||
ServerUpdateCheckRequest
|
||||
ServerUpdateCheckRequest,
|
||||
ServerUpdateConfigRequest
|
||||
)
|
||||
from src.auth_manager import (
|
||||
SessionManager,
|
||||
@@ -452,7 +453,19 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
||||
config["auth_token"] = secrets.token_hex(24)
|
||||
if "enrollment_secret" not in config:
|
||||
config["enrollment_secret"] = secrets.token_hex(24)
|
||||
if "server_fingerprint" not in config and "public_key" in config:
|
||||
if "auto_update_enabled" not in config:
|
||||
config["auto_update_enabled"] = True
|
||||
if "update_mode" not in config:
|
||||
config["update_mode"] = "auto"
|
||||
if "upstream_release_url" not in config:
|
||||
config["upstream_release_url"] = UPDATE_MANAGER.DEFAULT_GITEA_RELEASE_URL
|
||||
if "private_key" not in config or "public_key" not in config:
|
||||
sname = config.get("server_name", "LOGAR Enterprise Hub")
|
||||
pk, pubk, fp = generate_server_keypair(sname)
|
||||
config["private_key"] = pk
|
||||
config["public_key"] = pubk
|
||||
config["server_fingerprint"] = fp
|
||||
elif "server_fingerprint" not in config and "public_key" in config:
|
||||
try:
|
||||
pub, _ = pgpy.PGPKey.from_blob(config["public_key"])
|
||||
config["server_fingerprint"] = str(pub.fingerprint)
|
||||
@@ -463,6 +476,11 @@ def load_or_init_config(config_path: str = CONFIG_FILE_NAME) -> Dict[str, Any]:
|
||||
config["cert_dir"] = resolve_writable_path(config["cert_dir"], "LOGAR-Server", "certs")
|
||||
config["config_file_path"] = os.path.abspath(actual_config_path)
|
||||
|
||||
UPDATE_MANAGER.auto_update_enabled = config.get("auto_update_enabled", True)
|
||||
UPDATE_MANAGER.update_mode = config.get("update_mode", "auto")
|
||||
if config.get("upstream_release_url"):
|
||||
UPDATE_MANAGER.upstream_url = config["upstream_release_url"]
|
||||
|
||||
global SESSION_SECRET
|
||||
if not os.environ.get("LOGAR_SESSION_SECRET") and "session_secret" in config:
|
||||
SESSION_SECRET = config["session_secret"]
|
||||
@@ -1473,6 +1491,79 @@ def check_server_updates(user: str = Depends(get_current_user_optional)):
|
||||
return mgr.check_upstream_updates(force=True)
|
||||
|
||||
|
||||
@app.post("/api/update/sync-packages")
|
||||
@app.post("/api/server/update/sync-packages")
|
||||
def sync_upstream_packages_api(auth_user: str = Depends(require_admin_auth)):
|
||||
"""
|
||||
Downloads latest client binary packages from upstream release assets to local Hub storage.
|
||||
"""
|
||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||
if not is_feature_licensed("auto_update", db_path):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Fleet package synchronization requires an active license with the 'auto_update' capability."
|
||||
)
|
||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||
results = mgr.sync_upstream_packages(force=True)
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Upstream client packages synchronized and hosted successfully.",
|
||||
"results": results,
|
||||
"update_status": mgr.get_status()
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/update/config")
|
||||
@app.post("/api/server/update/config")
|
||||
def update_server_update_config(req: ServerUpdateConfigRequest, auth_user: str = Depends(require_admin_auth)):
|
||||
"""
|
||||
Updates fleet auto-update settings (on/off switch and automatic vs manual acquisition mode).
|
||||
"""
|
||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||
config = SERVER_STATE.get("config", {})
|
||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||
|
||||
if req.auto_update_enabled and not is_feature_licensed("auto_update", db_path):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Fleet auto-update requires an active license with the 'auto_update' capability."
|
||||
)
|
||||
|
||||
mode = (req.update_mode or "auto").lower()
|
||||
if mode not in ("auto", "manual"):
|
||||
raise HTTPException(status_code=400, detail="Invalid update_mode. Must be 'auto' (fetch from upstream) or 'manual' (provided only).")
|
||||
|
||||
mgr.auto_update_enabled = bool(req.auto_update_enabled)
|
||||
mgr.update_mode = mode
|
||||
if req.upstream_url:
|
||||
mgr.upstream_url = req.upstream_url.strip()
|
||||
|
||||
config["auto_update_enabled"] = mgr.auto_update_enabled
|
||||
config["update_mode"] = mgr.update_mode
|
||||
if req.upstream_url:
|
||||
config["upstream_release_url"] = mgr.upstream_url
|
||||
|
||||
cfg_file = config.get("config_file_path") or CONFIG_FILE_NAME
|
||||
try:
|
||||
with open(cfg_file, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"[!] Warning: Could not save config file: {e}")
|
||||
|
||||
# If enabled and switched to auto mode, optionally trigger sync in background
|
||||
if mgr.auto_update_enabled and mgr.update_mode == "auto":
|
||||
try:
|
||||
mgr.check_upstream_updates(force=False, auto_download_clients=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "Fleet update configuration saved successfully.",
|
||||
"update_status": mgr.get_status()
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/client/update/check")
|
||||
def check_client_update(
|
||||
client_id: str,
|
||||
@@ -2275,8 +2366,10 @@ async def update_checker_loop(interval_hours: int = 1):
|
||||
db_path = SERVER_STATE.get("config", {}).get("db_path", DEFAULT_DB_FILE)
|
||||
if is_feature_licensed("auto_update", db_path):
|
||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||
loop = asyncio.get_running_loop()
|
||||
await loop.run_in_executor(None, lambda: mgr.check_upstream_updates(force=False, auto_download_clients=True))
|
||||
if mgr.auto_update_enabled:
|
||||
loop = asyncio.get_running_loop()
|
||||
should_dl = (mgr.update_mode == "auto")
|
||||
await loop.run_in_executor(None, lambda: mgr.check_upstream_updates(force=False, auto_download_clients=should_dl))
|
||||
except Exception as e:
|
||||
print(f"[!] Warning in update checker background loop: {e}")
|
||||
await asyncio.sleep(interval_hours * 3600)
|
||||
|
||||
@@ -26,6 +26,12 @@ class ServerUpdateCheckRequest(BaseModel):
|
||||
force: bool = False
|
||||
|
||||
|
||||
class ServerUpdateConfigRequest(BaseModel):
|
||||
auto_update_enabled: bool = True
|
||||
update_mode: str = "auto" # "auto" | "manual"
|
||||
upstream_url: Optional[str] = None
|
||||
|
||||
|
||||
class ClientActionRequest(BaseModel):
|
||||
client_id: str
|
||||
|
||||
|
||||
@@ -26,12 +26,16 @@ class ServerUpdateManager:
|
||||
self,
|
||||
update_dir: Optional[str] = None,
|
||||
upstream_url: Optional[str] = None,
|
||||
check_enabled: bool = True
|
||||
check_enabled: bool = True,
|
||||
auto_update_enabled: bool = True,
|
||||
update_mode: str = "auto"
|
||||
):
|
||||
self.update_dir = update_dir or os.path.join(os.getcwd(), "updates")
|
||||
os.makedirs(self.update_dir, exist_ok=True)
|
||||
self.upstream_url = upstream_url or self.DEFAULT_GITEA_RELEASE_URL
|
||||
self.check_enabled = check_enabled
|
||||
self.auto_update_enabled = auto_update_enabled
|
||||
self.update_mode = update_mode.lower() if update_mode else "auto"
|
||||
|
||||
# Cached upstream release state
|
||||
self.last_checked_utc: Optional[str] = None
|
||||
@@ -275,7 +279,17 @@ class ServerUpdateManager:
|
||||
has_pkg = bool(pkg.get("exists"))
|
||||
client_ver = (current_version or "0.0.0.0").strip().lstrip("v")
|
||||
|
||||
# An update is available if the hosted version is strictly newer than the client version
|
||||
# An update is available if auto_update is enabled, package exists, and hosted version is strictly newer
|
||||
if not self.auto_update_enabled:
|
||||
return {
|
||||
"update_available": False,
|
||||
"client_id": client_id,
|
||||
"os": norm_os,
|
||||
"current_version": client_ver,
|
||||
"latest_version": target_version,
|
||||
"reason": "Fleet auto-update is disabled on this server."
|
||||
}
|
||||
|
||||
update_available = has_pkg and is_newer_version(target_version, client_ver)
|
||||
|
||||
return {
|
||||
@@ -377,10 +391,21 @@ class ServerUpdateManager:
|
||||
|
||||
return results
|
||||
|
||||
def sync_upstream_packages(self, force: bool = True, timeout: int = 60) -> Dict[str, Any]:
|
||||
"""
|
||||
Manually or programmatically triggers an immediate upstream release check and downloads
|
||||
the latest client binary packages to the Hub repository.
|
||||
"""
|
||||
self.check_upstream_updates(force=force, auto_download_clients=True, timeout=timeout)
|
||||
if self.release_assets:
|
||||
return self.download_and_cache_client_packages(assets=self.release_assets, timeout=timeout)
|
||||
return {"windows": False, "linux": False, "downloaded": [], "error": self.last_error or "No release assets found."}
|
||||
|
||||
def check_upstream_updates(self, force: bool = False, auto_download_clients: bool = True, timeout: int = 15) -> Dict[str, Any]:
|
||||
"""
|
||||
Queries the upstream repository release API (Gitea / GitHub / JSON manifest)
|
||||
to check if a newer server or client release is available, and automatically caches client packages.
|
||||
to check if a newer server or client release is available, and automatically caches client packages
|
||||
when in automatic acquisition mode.
|
||||
"""
|
||||
if not self.check_enabled and not force:
|
||||
return self.get_status()
|
||||
@@ -423,9 +448,9 @@ class ServerUpdateManager:
|
||||
if self.server_update_available:
|
||||
print(f"[!] 🔔 LOGAR Server Update Available: Current v{LOGAR_VERSION} -> Upstream v{upstream_ver}")
|
||||
|
||||
# Automatically download and cache client update packages on the Hub
|
||||
if auto_download_clients and assets:
|
||||
# Check if hosted packages need updating
|
||||
# Automatically download and cache client update packages on the Hub if in auto mode
|
||||
should_auto_download = auto_download_clients and (self.update_mode == "auto" or force)
|
||||
if should_auto_download and assets:
|
||||
hosted = self.get_hosted_packages()
|
||||
needs_win = not hosted.get("windows", {}).get("exists") or is_newer_version(upstream_ver, hosted.get("windows", {}).get("version", "0.0.0"))
|
||||
needs_linux = not hosted.get("linux", {}).get("exists") or is_newer_version(upstream_ver, hosted.get("linux", {}).get("version", "0.0.0"))
|
||||
@@ -452,5 +477,8 @@ class ServerUpdateManager:
|
||||
"last_checked_utc": self.last_checked_utc,
|
||||
"last_error": self.last_error,
|
||||
"check_enabled": self.check_enabled,
|
||||
"auto_update_enabled": self.auto_update_enabled,
|
||||
"update_mode": self.update_mode,
|
||||
"upstream_url": self.upstream_url,
|
||||
"hosted_packages": self.get_hosted_packages()
|
||||
}
|
||||
|
||||
+133
-17
@@ -192,6 +192,9 @@ class WebUIRenderer:
|
||||
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", {})
|
||||
auto_update_enabled = update_status.get("auto_update_enabled", True)
|
||||
update_mode = update_status.get("update_mode", "auto")
|
||||
upstream_url = update_status.get("upstream_url") or "https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest"
|
||||
|
||||
# Fetch clients safely
|
||||
conn = sqlite3.connect(db_path)
|
||||
@@ -649,11 +652,13 @@ class WebUIRenderer:
|
||||
<!-- Tab 6: Updates & Fleet Distribution -->
|
||||
<div class="tab-pane" id="tabPane-updates">
|
||||
{auto_update_lock_banner}
|
||||
|
||||
<!-- Server Version & Upstream Release Status -->
|
||||
<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 class="subtitle" style="margin-top: 4px;">Upstream release tracking 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>
|
||||
@@ -677,14 +682,78 @@ class WebUIRenderer:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<!-- Fleet Auto-Update & Acquisition Policy Form -->
|
||||
<div class="card" style="margin-bottom: 24px; {upload_container_style}">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="card-title" style="font-size: 15px;">⚙️ Fleet Distribution & Auto-Update Policy</div>
|
||||
<div class="subtitle" style="margin-top: 4px;">Configure hot-upgrade execution and choose how client binary images are acquired & hosted</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
{('<span class="badge badge-active" style="font-size: 11px;">🔄 FLEET AUTO-UPDATE LICENSED</span>' if is_auto_update_unlocked else '<span class="badge badge-danger" style="font-size: 11px;">🔒 UNLICENSED</span>')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onsubmit="saveUpdateConfig(event)">
|
||||
<!-- Switch 1: Fleet Auto-Update Service (On / Off) -->
|
||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px;">
|
||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||
<span>⚡</span> Fleet Auto-Update Service
|
||||
</div>
|
||||
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 6px;">
|
||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOn" value="1" {'checked' if (auto_update_enabled and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||
<span>🟢 <strong>Enabled (ON)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Edge forwarders query this Hub and auto hot-upgrade</span></span>
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer; padding: 10px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="autoUpdateEnabledRadio" id="autoUpdateOff" value="0" {'checked' if (not auto_update_enabled or not is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||
<span>🔴 <strong>Disabled (OFF)</strong> <span style="color: var(--text-muted); font-size: 12px;">— Suspend fleet hot-upgrades (clients stay on current version)</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Switch 2: Image Acquisition & Hosting Mode (Auto Fetch vs Manual Provided) -->
|
||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px;">
|
||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
||||
<span>📥</span> Image Acquisition & Hosting Mode
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
||||
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="updateModeRadio" id="modeAuto" value="auto" {'checked' if (update_mode == 'auto' and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||
<div>
|
||||
<div style="font-weight: 600; font-size: 13px; color: #34d399;">🌐 Automatic (Hub Downloads & Hosts Upstream Images)</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub connects to the upstream release repository, automatically downloads Windows & Linux client binaries (.exe, .bin), caches and hosts them locally for the fleet.</div>
|
||||
</div>
|
||||
</label>
|
||||
<label style="display: inline-flex; align-items: center; gap: 12px; cursor: pointer; padding: 12px 16px; background: #151c2e; border: 1px solid var(--border); border-radius: 8px;">
|
||||
<input type="radio" name="updateModeRadio" id="modeManual" value="manual" {'checked' if (update_mode == 'manual' or not is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||
<div>
|
||||
<div style="font-weight: 600; font-size: 13px; color: #f59e0b;">📁 Manually Provided (Upload Only)</div>
|
||||
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub will not download packages from the internet. Binary packages must be manually uploaded by an administrator below.</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0;">
|
||||
<label class="form-label" style="font-size: 12px;">Upstream Release API Endpoint</label>
|
||||
<input type="text" id="upstreamReleaseUrl" class="form-control" value="{html.escape(str(upstream_url))}" placeholder="https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest" {upload_disabled_attr}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<button type="submit" class="btn btn-primary" {upload_disabled_attr}>💾 Save Update Settings</button>
|
||||
<button type="button" onclick="syncUpstreamPackagesNow()" class="btn btn-accent" {upload_disabled_attr}>🔄 Sync & Download Upstream Images Now</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Client Package Distribution Repository (No Download Buttons) -->
|
||||
<div class="card" style="margin-bottom: 24px;">
|
||||
<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 class="subtitle" style="margin-top: 4px;">Hosted binaries served locally by this Hub to edge forwarders for hot-upgrades</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
{('<span class="badge badge-active" style="font-size: 11px;">🔄 FLEET AUTO-UPDATE LICENSED</span>' if is_auto_update_unlocked else '<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: #94a3b8; border: 1px solid rgba(148, 163, 184, 0.3); font-size: 11px;">STANDARD DISTRIBUTION</span>')}
|
||||
<span class="badge badge-active">Target: v{hosted_client_ver}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -697,8 +766,7 @@ class WebUIRenderer:
|
||||
<th>Version</th>
|
||||
<th>Size</th>
|
||||
<th>SHA-256 Checksum</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
<th>Repository Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -708,10 +776,7 @@ class WebUIRenderer:
|
||||
<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>
|
||||
<td>{('<span class="badge badge-active">✅ Hosted & Ready for Fleet</span>' if hosted_packages.get('windows', {}).get('exists') else '<span class="badge badge-warning">⚠️ Not Hosted</span>')}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge badge-os">LINUX</span></td>
|
||||
@@ -719,23 +784,21 @@ class WebUIRenderer:
|
||||
<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>
|
||||
<td>{('<span class="badge badge-active">✅ Hosted & Ready for Fleet</span>' if hosted_packages.get('linux', {}).get('exists') else '<span class="badge badge-warning">⚠️ Not Hosted</span>')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Manually Upload / Provide Binary Section -->
|
||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); {upload_container_style}">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;">
|
||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa;">📤 Upload Client Binary Package</div>
|
||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa;">📤 Manually Provide & Upload Client Binary Package</div>
|
||||
{('<span class="badge badge-danger">🔒 Auto-Update Locked</span>' if not is_auto_update_unlocked else '')}
|
||||
</div>
|
||||
<div style="margin-bottom: 14px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.3); border-radius: 6px; padding: 10px 14px; font-size: 12px; color: #fbbf24; display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 16px;">⚠️</span>
|
||||
<span><strong>Warning:</strong> Uploaded binary packages are immediately distributed to connected edge forwarders for automatic hot-upgrades. Ensure binaries are thoroughly tested and verified before uploading.</span>
|
||||
<span><strong>Warning:</strong> Uploaded binary packages are immediately hosted in this repository and distributed to connected edge forwarders for automatic hot-upgrades. Ensure binaries are verified before uploading.</span>
|
||||
</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;">
|
||||
@@ -753,7 +816,7 @@ class WebUIRenderer:
|
||||
<label class="form-label">Select Binary File</label>
|
||||
<input type="file" id="uploadPkgFile" class="form-control" required {upload_disabled_attr}>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-accent" style="padding: 10px 18px; font-size: 13px;" {upload_disabled_attr}>Upload & Deploy to Fleet</button>
|
||||
<button type="submit" class="btn btn-accent" style="padding: 10px 18px; font-size: 13px;" {upload_disabled_attr}>Upload & Host in Repository</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1890,6 +1953,59 @@ class WebUIRenderer:
|
||||
}}
|
||||
}}
|
||||
|
||||
async function saveUpdateConfig(e) {{
|
||||
if (e) e.preventDefault();
|
||||
const isEnabled = document.getElementById('autoUpdateOn') ? document.getElementById('autoUpdateOn').checked : true;
|
||||
const modeAuto = document.getElementById('modeAuto') ? document.getElementById('modeAuto').checked : true;
|
||||
const mode = modeAuto ? 'auto' : 'manual';
|
||||
const upstreamUrl = document.getElementById('upstreamReleaseUrl') ? document.getElementById('upstreamReleaseUrl').value.trim() : '';
|
||||
|
||||
showAlert('Saving fleet auto-update settings...');
|
||||
try {{
|
||||
const res = await fetch('/api/server/update/config', {{
|
||||
method: 'POST',
|
||||
headers: {{ 'Content-Type': 'application/json' }},
|
||||
body: JSON.stringify({{
|
||||
auto_update_enabled: isEnabled,
|
||||
update_mode: mode,
|
||||
upstream_url: upstreamUrl || undefined
|
||||
}})
|
||||
}});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === 'success') {{
|
||||
showAlert('✅ Fleet update configuration saved successfully!');
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
}} else {{
|
||||
let errMsg = 'Failed to save configuration';
|
||||
if (typeof data.detail === 'string') errMsg = data.detail;
|
||||
else if (data.message) errMsg = data.message;
|
||||
alert('Error: ' + errMsg);
|
||||
}}
|
||||
}} catch (err) {{
|
||||
alert('Request error: ' + err.message);
|
||||
}}
|
||||
}}
|
||||
|
||||
async function syncUpstreamPackagesNow() {{
|
||||
showAlert('Connecting to upstream repository and downloading latest client binary images...');
|
||||
try {{
|
||||
const res = await fetch('/api/server/update/sync-packages', {{ method: 'POST' }});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === 'success') {{
|
||||
const dl = (data.results && data.results.downloaded) ? data.results.downloaded.length : 0;
|
||||
showAlert('✅ Upstream synchronization complete (' + dl + ' package(s) downloaded/verified)!');
|
||||
setTimeout(() => location.reload(), 1500);
|
||||
}} else {{
|
||||
let errMsg = 'Synchronization failed';
|
||||
if (typeof data.detail === 'string') errMsg = data.detail;
|
||||
else if (data.message) errMsg = data.message;
|
||||
alert('Sync Error: ' + errMsg);
|
||||
}}
|
||||
}} catch (err) {{
|
||||
alert('Sync request failed: ' + err.message);
|
||||
}}
|
||||
}}
|
||||
|
||||
async function submitClientPackageUpload(e) {{
|
||||
e.preventDefault();
|
||||
const osType = document.getElementById('uploadPkgOs').value;
|
||||
|
||||
+49
-18
@@ -32,7 +32,10 @@ class TestUpdateSystem(unittest.TestCase):
|
||||
Server.init_db(self.test_db)
|
||||
self.update_mgr = ServerUpdateManager(update_dir=self.test_updates_dir, check_enabled=False)
|
||||
Server.SERVER_STATE["update_manager"] = self.update_mgr
|
||||
Server.SERVER_STATE["config"] = {"db_path": self.test_db}
|
||||
Server.SERVER_STATE["config"] = {
|
||||
"db_path": self.test_db,
|
||||
"config_file_path": os.path.join(self.test_dir, "test_server_config.json")
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
@@ -194,27 +197,55 @@ class TestUpdateSystem(unittest.TestCase):
|
||||
self.assertIn("tabPane-updates", html)
|
||||
self.assertIn("Client Package Distribution Repository", html)
|
||||
|
||||
def test_update_memoization_and_cache_invalidation(self):
|
||||
"""Tests that package info is served from memory cache and invalidates on update."""
|
||||
dummy_pkg = os.path.join(self.test_dir, "LOGAR_Client_Linux")
|
||||
def test_auto_update_switches_and_modes(self):
|
||||
"""Tests On/Off toggle, Auto vs Manual modes, and config API endpoint."""
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(self.test_db)
|
||||
conn.execute("UPDATE license_config SET license_status = 'active', features = '[\"auto_update\"]' WHERE id = 1")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
dummy_pkg = os.path.join(self.test_dir, "LOGAR_Client_Setup.exe")
|
||||
with open(dummy_pkg, "wb") as f:
|
||||
f.write(b"LINUX_CLIENT_PAYLOAD_V1")
|
||||
f.write(b"WIN_CLIENT_V0360")
|
||||
self.update_mgr.register_client_package("windows", dummy_pkg, version="0.3.6.0")
|
||||
|
||||
self.update_mgr.register_client_package("linux", dummy_pkg, version="0.3.6.0")
|
||||
pkgs_1 = self.update_mgr.get_hosted_packages()
|
||||
sha1 = pkgs_1["linux"]["sha256"]
|
||||
# 1. Active & auto mode -> update offered
|
||||
chk1 = Server.check_client_update(client_id="win-node", os="windows", current_version="0.3.5.2")
|
||||
self.assertTrue(chk1["update_available"])
|
||||
|
||||
# Repeated call should hit memory cache with identical hash
|
||||
pkgs_2 = self.update_mgr.get_hosted_packages()
|
||||
self.assertEqual(pkgs_2["linux"]["sha256"], sha1)
|
||||
# 2. Toggle Auto-Update OFF via API
|
||||
from server_models import ServerUpdateConfigRequest
|
||||
cfg_req = ServerUpdateConfigRequest(auto_update_enabled=False, update_mode="manual")
|
||||
res = Server.update_server_update_config(req=cfg_req, auth_user="admin")
|
||||
self.assertEqual(res["status"], "success")
|
||||
self.assertFalse(self.update_mgr.auto_update_enabled)
|
||||
self.assertEqual(self.update_mgr.update_mode, "manual")
|
||||
|
||||
# Invalidate cache on new registration
|
||||
with open(dummy_pkg, "wb") as f:
|
||||
f.write(b"LINUX_CLIENT_PAYLOAD_V2_UPDATED")
|
||||
self.update_mgr.register_client_package("linux", dummy_pkg, version="0.3.7.0")
|
||||
pkgs_3 = self.update_mgr.get_hosted_packages()
|
||||
self.assertEqual(pkgs_3["linux"]["version"], "0.3.7.0")
|
||||
self.assertNotEqual(pkgs_3["linux"]["sha256"], sha1)
|
||||
# Client update check while OFF -> blocked
|
||||
chk2 = Server.check_client_update(client_id="win-node", os="windows", current_version="0.3.5.2")
|
||||
self.assertFalse(chk2["update_available"])
|
||||
self.assertIn("disabled", chk2.get("reason", ""))
|
||||
|
||||
# 3. Toggle back ON with 'auto' mode
|
||||
cfg_req2 = ServerUpdateConfigRequest(auto_update_enabled=True, update_mode="auto")
|
||||
res2 = Server.update_server_update_config(req=cfg_req2, auth_user="admin")
|
||||
self.assertEqual(res2["status"], "success")
|
||||
self.assertTrue(self.update_mgr.auto_update_enabled)
|
||||
self.assertEqual(self.update_mgr.update_mode, "auto")
|
||||
|
||||
# 4. Verify WebUI renders without download buttons in packages table
|
||||
html = Server.WebUIRenderer.render_dashboard_html(
|
||||
server_state=Server.SERVER_STATE,
|
||||
ingest_queue=Server.INGEST_QUEUE
|
||||
)
|
||||
self.assertNotIn("/api/client/update/download?os=windows", html)
|
||||
self.assertNotIn("/api/client/update/download?os=linux", html)
|
||||
self.assertIn("Fleet Auto-Update Service", html)
|
||||
self.assertIn("Image Acquisition & Hosting Mode", html)
|
||||
self.assertIn("autoUpdateEnabledRadio", html)
|
||||
self.assertIn("updateModeRadio", html)
|
||||
self.assertIn("✅ Hosted & Ready for Fleet", html)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user