feat(hub): add three image acquisition sources (official repo, custom repo, manual provided)
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m54s
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m54s
This commit is contained in:
+16
-9
@@ -1530,19 +1530,26 @@ def update_server_update_config(req: ServerUpdateConfigRequest, auth_user: str =
|
|||||||
config = SERVER_STATE.get("config", {})
|
config = SERVER_STATE.get("config", {})
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
|
|
||||||
mode = (req.update_mode or "auto").lower()
|
mode = (req.update_mode or "official").lower()
|
||||||
if mode not in ("auto", "manual"):
|
if mode not in ("official", "custom", "manual", "auto"):
|
||||||
raise HTTPException(status_code=400, detail="Invalid update_mode. Must be 'auto' (fetch from upstream) or 'manual' (provided only).")
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="Invalid update_mode. Must be 'official' (Official production repo), 'custom' (Custom upstream repo), or 'manual' (Provided only)."
|
||||||
|
)
|
||||||
|
|
||||||
mgr.auto_update_enabled = bool(req.auto_update_enabled)
|
mgr.auto_update_enabled = bool(req.auto_update_enabled)
|
||||||
|
if mode == "auto":
|
||||||
|
mode = "official" if (not req.upstream_url or req.upstream_url == mgr.DEFAULT_GITEA_RELEASE_URL) else "custom"
|
||||||
mgr.update_mode = mode
|
mgr.update_mode = mode
|
||||||
if req.upstream_url:
|
|
||||||
|
if mode == "official":
|
||||||
|
mgr.upstream_url = mgr.DEFAULT_GITEA_RELEASE_URL
|
||||||
|
elif mode == "custom" and req.upstream_url:
|
||||||
mgr.upstream_url = req.upstream_url.strip()
|
mgr.upstream_url = req.upstream_url.strip()
|
||||||
|
|
||||||
config["auto_update_enabled"] = mgr.auto_update_enabled
|
config["auto_update_enabled"] = mgr.auto_update_enabled
|
||||||
config["update_mode"] = mgr.update_mode
|
config["update_mode"] = mgr.update_mode
|
||||||
if req.upstream_url:
|
config["upstream_release_url"] = mgr.upstream_url
|
||||||
config["upstream_release_url"] = mgr.upstream_url
|
|
||||||
|
|
||||||
cfg_file = config.get("config_file_path") or CONFIG_FILE_NAME
|
cfg_file = config.get("config_file_path") or CONFIG_FILE_NAME
|
||||||
try:
|
try:
|
||||||
@@ -1551,8 +1558,8 @@ def update_server_update_config(req: ServerUpdateConfigRequest, auth_user: str =
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[!] Warning: Could not save config file: {e}")
|
print(f"[!] Warning: Could not save config file: {e}")
|
||||||
|
|
||||||
# If enabled and in auto mode, trigger upstream check
|
# If enabled and in automatic fetch mode (official or custom), trigger upstream check
|
||||||
if mgr.auto_update_enabled and mgr.update_mode == "auto":
|
if mgr.auto_update_enabled and mgr.update_mode in ("official", "custom", "auto"):
|
||||||
try:
|
try:
|
||||||
mgr.check_upstream_updates(force=False, auto_download_clients=True)
|
mgr.check_upstream_updates(force=False, auto_download_clients=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -2372,7 +2379,7 @@ async def update_checker_loop(interval_hours: int = 1):
|
|||||||
try:
|
try:
|
||||||
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
mgr: ServerUpdateManager = SERVER_STATE.get("update_manager") or UPDATE_MANAGER
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
should_dl = bool(mgr.auto_update_enabled and mgr.update_mode == "auto")
|
should_dl = bool(mgr.auto_update_enabled and mgr.update_mode in ("official", "custom", "auto"))
|
||||||
await loop.run_in_executor(None, lambda: mgr.check_upstream_updates(force=False, auto_download_clients=should_dl))
|
await loop.run_in_executor(None, lambda: mgr.check_upstream_updates(force=False, auto_download_clients=should_dl))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[!] Warning in update checker background loop: {e}")
|
print(f"[!] Warning in update checker background loop: {e}")
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ServerUpdateCheckRequest(BaseModel):
|
|||||||
|
|
||||||
class ServerUpdateConfigRequest(BaseModel):
|
class ServerUpdateConfigRequest(BaseModel):
|
||||||
auto_update_enabled: bool = True
|
auto_update_enabled: bool = True
|
||||||
update_mode: str = "auto" # "auto" | "manual"
|
update_mode: str = "official" # "official" | "custom" | "manual"
|
||||||
upstream_url: Optional[str] = None
|
upstream_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,16 @@ class ServerUpdateManager:
|
|||||||
update_mode: str = "auto"
|
update_mode: str = "auto"
|
||||||
):
|
):
|
||||||
self.update_dir = update_dir or os.path.join(os.getcwd(), "updates")
|
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.upstream_url = upstream_url or self.DEFAULT_GITEA_RELEASE_URL
|
||||||
self.check_enabled = check_enabled
|
self.check_enabled = check_enabled
|
||||||
self.auto_update_enabled = auto_update_enabled
|
self.auto_update_enabled = auto_update_enabled
|
||||||
self.update_mode = update_mode.lower() if update_mode else "auto"
|
raw_mode = (update_mode or "official").lower()
|
||||||
|
if raw_mode in ("official", "custom", "manual"):
|
||||||
|
self.update_mode = raw_mode
|
||||||
|
elif raw_mode == "auto":
|
||||||
|
self.update_mode = "official" if (not upstream_url or upstream_url == self.DEFAULT_GITEA_RELEASE_URL) else "custom"
|
||||||
|
else:
|
||||||
|
self.update_mode = "official"
|
||||||
|
|
||||||
# Cached upstream release state
|
# Cached upstream release state
|
||||||
self.last_checked_utc: Optional[str] = None
|
self.last_checked_utc: Optional[str] = None
|
||||||
@@ -454,8 +459,8 @@ class ServerUpdateManager:
|
|||||||
if self.server_update_available:
|
if self.server_update_available:
|
||||||
print(f"[!] 🔔 LOGAR Server Update Available: Current v{LOGAR_VERSION} -> Upstream v{upstream_ver}")
|
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 in auto mode
|
# Automatically download and cache client update packages on the Hub if in official or custom repo mode
|
||||||
should_auto_download = auto_download_clients and (self.update_mode == "auto" or force)
|
should_auto_download = auto_download_clients and (self.update_mode in ("official", "custom", "auto") or force)
|
||||||
if should_auto_download and assets:
|
if should_auto_download and assets:
|
||||||
hosted = self.get_hosted_packages()
|
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_win = not hosted.get("windows", {}).get("exists") or is_newer_version(upstream_ver, hosted.get("windows", {}).get("version", "0.0.0"))
|
||||||
|
|||||||
+48
-15
@@ -193,8 +193,13 @@ class WebUIRenderer:
|
|||||||
hosted_client_ver = update_status.get("hosted_client_version") or LOGAR_VERSION
|
hosted_client_ver = update_status.get("hosted_client_version") or LOGAR_VERSION
|
||||||
hosted_packages = update_status.get("hosted_packages", {})
|
hosted_packages = update_status.get("hosted_packages", {})
|
||||||
auto_update_enabled = update_status.get("auto_update_enabled", True)
|
auto_update_enabled = update_status.get("auto_update_enabled", True)
|
||||||
update_mode = update_status.get("update_mode", "auto")
|
update_mode = (update_status.get("update_mode") or "official").lower()
|
||||||
upstream_url = update_status.get("upstream_url") or "https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest"
|
upstream_url = update_status.get("upstream_url") or "https://gitea.eibl.tech/api/v1/repos/me0nline/LOGAR/releases/latest"
|
||||||
|
is_mode_official = (update_mode in ("official", "auto")) and ("gitea.eibl.tech" in str(upstream_url))
|
||||||
|
is_mode_custom = (update_mode == "custom") or (update_mode == "auto" and "gitea.eibl.tech" not in str(upstream_url))
|
||||||
|
is_mode_manual = (update_mode == "manual")
|
||||||
|
if not is_mode_official and not is_mode_custom and not is_mode_manual:
|
||||||
|
is_mode_official = True
|
||||||
|
|
||||||
# Fetch clients safely
|
# Fetch clients safely
|
||||||
conn = sqlite3.connect(db_path)
|
conn = sqlite3.connect(db_path)
|
||||||
@@ -712,30 +717,44 @@ class WebUIRenderer:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Switch 2: Image Acquisition & Hosting Mode (Auto Fetch vs Manual Provided) -->
|
<!-- Switch 2: Image Acquisition Source & Hosting Mode -->
|
||||||
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px; {upload_container_style}">
|
<div style="background: #0f172a; padding: 18px; border-radius: 8px; border: 1px solid var(--border); margin-bottom: 16px; {upload_container_style}">
|
||||||
<div style="font-size: 13px; font-weight: 700; color: #60a5fa; margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
|
<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
|
<span>📥</span> Image Acquisition Source & Hosting Mode
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
<div style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px;">
|
||||||
|
<!-- Option 1: Official Repository -->
|
||||||
<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;">
|
<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}>
|
<input type="radio" name="updateModeRadio" id="modeOfficial" value="official" onchange="toggleUpdateModeUi()" {'checked' if (is_mode_official and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; font-size: 13px; color: #34d399;">🌐 Automatic (Hub Downloads & Hosts Upstream Images)</div>
|
<div style="font-weight: 600; font-size: 13px; color: #34d399;">🌐 Official Repository (gitea.eibl.tech)</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 style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Connect directly to the official upstream LOGAR release repository, automatically acquiring and hosting verified Windows (.exe) and Linux (.bin) client binaries.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- Option 2: Custom Repository -->
|
||||||
<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;">
|
<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}>
|
<input type="radio" name="updateModeRadio" id="modeCustom" value="custom" onchange="toggleUpdateModeUi()" {'checked' if (is_mode_custom and is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight: 600; font-size: 13px; color: #f59e0b;">📁 Manually Provided (Upload Only)</div>
|
<div style="font-weight: 600; font-size: 13px; color: #38bdf8;">🛠️ Custom Repository (Private Mirror / API)</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 style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">Connect to an internal private mirror, enterprise git instance, or custom release API endpoint to acquire binary packages.</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Option 3: Manual Provided / Upload Only -->
|
||||||
|
<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" onchange="toggleUpdateModeUi()" {'checked' if (is_mode_manual or not is_auto_update_unlocked) else ''} {upload_disabled_attr}>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; font-size: 13px; color: #f59e0b;">📁 Manual (Upload Only / Air-Gapped)</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-muted); margin-top: 2px;">The LOGAR Hub will not contact external networks for downloads. Client packages must be manually provided and uploaded by an administrator below.</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-bottom: 0;">
|
|
||||||
<label class="form-label" style="font-size: 12px;">Upstream Release API Endpoint</label>
|
<!-- Custom URL Field (shown when custom is selected) -->
|
||||||
<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 id="customUpstreamSection" class="form-group" style="margin-bottom: 0; display: {'block' if (is_mode_custom and is_auto_update_unlocked) else 'none'};">
|
||||||
|
<label class="form-label" style="font-size: 12px;">Custom Upstream Release API Endpoint</label>
|
||||||
|
<input type="text" id="upstreamReleaseUrl" class="form-control" value="{html.escape(str(upstream_url))}" placeholder="https://git.yourcompany.internal/api/v1/repos/security/LOGAR/releases/latest" {upload_disabled_attr}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2017,11 +2036,25 @@ class WebUIRenderer:
|
|||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
function toggleUpdateModeUi() {{
|
||||||
|
const isCustom = document.getElementById('modeCustom') && document.getElementById('modeCustom').checked;
|
||||||
|
const customSec = document.getElementById('customUpstreamSection');
|
||||||
|
if (customSec) {{
|
||||||
|
customSec.style.display = isCustom ? 'block' : 'none';
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
async function saveUpdateConfig(e) {{
|
async function saveUpdateConfig(e) {{
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
const isEnabled = document.getElementById('autoUpdateOn') ? document.getElementById('autoUpdateOn').checked : true;
|
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';
|
let mode = 'official';
|
||||||
|
if (document.getElementById('modeCustom') && document.getElementById('modeCustom').checked) {{
|
||||||
|
mode = 'custom';
|
||||||
|
}} else if (document.getElementById('modeManual') && document.getElementById('modeManual').checked) {{
|
||||||
|
mode = 'manual';
|
||||||
|
}}
|
||||||
|
|
||||||
const upstreamUrl = document.getElementById('upstreamReleaseUrl') ? document.getElementById('upstreamReleaseUrl').value.trim() : '';
|
const upstreamUrl = document.getElementById('upstreamReleaseUrl') ? document.getElementById('upstreamReleaseUrl').value.trim() : '';
|
||||||
|
|
||||||
showAlert('Saving fleet auto-update settings...');
|
showAlert('Saving fleet auto-update settings...');
|
||||||
@@ -2032,7 +2065,7 @@ class WebUIRenderer:
|
|||||||
body: JSON.stringify({{
|
body: JSON.stringify({{
|
||||||
auto_update_enabled: isEnabled,
|
auto_update_enabled: isEnabled,
|
||||||
update_mode: mode,
|
update_mode: mode,
|
||||||
upstream_url: upstreamUrl || undefined
|
upstream_url: (mode === 'custom' && upstreamUrl) ? upstreamUrl : undefined
|
||||||
}})
|
}})
|
||||||
}});
|
}});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
+19
-7
@@ -227,14 +227,25 @@ class TestUpdateSystem(unittest.TestCase):
|
|||||||
self.assertFalse(chk2["update_available"])
|
self.assertFalse(chk2["update_available"])
|
||||||
self.assertIn("disabled", chk2.get("reason", ""))
|
self.assertIn("disabled", chk2.get("reason", ""))
|
||||||
|
|
||||||
# 3. Toggle back ON with 'auto' mode
|
# 3. Toggle back ON with 'official' mode
|
||||||
cfg_req2 = ServerUpdateConfigRequest(auto_update_enabled=True, update_mode="auto")
|
cfg_req2 = ServerUpdateConfigRequest(auto_update_enabled=True, update_mode="official")
|
||||||
res2 = Server.update_server_update_config(req=cfg_req2, auth_user="admin")
|
res2 = Server.update_server_update_config(req=cfg_req2, auth_user="admin")
|
||||||
self.assertEqual(res2["status"], "success")
|
self.assertEqual(res2["status"], "success")
|
||||||
self.assertTrue(self.update_mgr.auto_update_enabled)
|
self.assertTrue(self.update_mgr.auto_update_enabled)
|
||||||
self.assertEqual(self.update_mgr.update_mode, "auto")
|
self.assertEqual(self.update_mgr.update_mode, "official")
|
||||||
|
|
||||||
# 4. Verify WebUI renders without download buttons in packages table
|
# 4. Configure 'custom' mode with custom upstream URL
|
||||||
|
cfg_req3 = ServerUpdateConfigRequest(
|
||||||
|
auto_update_enabled=True,
|
||||||
|
update_mode="custom",
|
||||||
|
upstream_url="https://git.corp.local/api/v1/releases/latest"
|
||||||
|
)
|
||||||
|
res3 = Server.update_server_update_config(req=cfg_req3, auth_user="admin")
|
||||||
|
self.assertEqual(res3["status"], "success")
|
||||||
|
self.assertEqual(self.update_mgr.update_mode, "custom")
|
||||||
|
self.assertEqual(self.update_mgr.upstream_url, "https://git.corp.local/api/v1/releases/latest")
|
||||||
|
|
||||||
|
# 5. Verify WebUI renders official, custom, manual mode switches and no download buttons
|
||||||
html = Server.WebUIRenderer.render_dashboard_html(
|
html = Server.WebUIRenderer.render_dashboard_html(
|
||||||
server_state=Server.SERVER_STATE,
|
server_state=Server.SERVER_STATE,
|
||||||
ingest_queue=Server.INGEST_QUEUE
|
ingest_queue=Server.INGEST_QUEUE
|
||||||
@@ -242,9 +253,10 @@ class TestUpdateSystem(unittest.TestCase):
|
|||||||
self.assertNotIn("/api/client/update/download?os=windows", html)
|
self.assertNotIn("/api/client/update/download?os=windows", html)
|
||||||
self.assertNotIn("/api/client/update/download?os=linux", html)
|
self.assertNotIn("/api/client/update/download?os=linux", html)
|
||||||
self.assertIn("Fleet Auto-Update Service", html)
|
self.assertIn("Fleet Auto-Update Service", html)
|
||||||
self.assertIn("Image Acquisition & Hosting Mode", html)
|
self.assertIn("Image Acquisition Source & Hosting Mode", html)
|
||||||
self.assertIn("autoUpdateEnabledRadio", html)
|
self.assertIn("modeOfficial", html)
|
||||||
self.assertIn("updateModeRadio", html)
|
self.assertIn("modeCustom", html)
|
||||||
|
self.assertIn("modeManual", html)
|
||||||
self.assertIn("Ready", html)
|
self.assertIn("Ready", html)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user