Merge feature/update-client: Complete EPIC_000008 Client-Side Update Mechanism
This commit is contained in:
18
CHANGELOG.md
18
CHANGELOG.md
@ -21,6 +21,24 @@ By: Codex (GPT-5)
|
||||
| 28.12.2025 | 🏗️ Planning | ID: Update-Format festgelegt (JSON + tar.gz). By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: Update-Service URL auf update.wlkns.org festgelegt. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: EPIC_000009 Update Webservice (External Team) dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: TASK_000027-TASK_000029 fuer EPIC_000008 ausgearbeitet. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | ⚙️ Code | ID: Update-Config Keys in Settings/ENV/README definiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: Update-Flow fuer Client dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: Update-Flow Prototyp dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: Update-Status-Schema dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | ⚙️ Code | ID: Update-Status-URL und Report im Client-Prototyp. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: US_000029-US_000032 fuer Update-UI dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: US_000033 Rollback-UI dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: Update-UI Anforderungen um Backup-Voraussetzung ergaenzt. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 🏗️ Planning | ID: TASK_000030-TASK_000034 fuer Update-UI ausgearbeitet. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: Update-API Endpunkte dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | 📝 Req | ID: Update-Status/Log Dateien dokumentiert. By: Codex (GPT-5) |
|
||||
| 28.12.2025 | ⚙️ Code | ID: Update-Client Prototyp-Skript hinzugefuegt. By: Codex (GPT-5) |
|
||||
| 29.12.2025 | ⚙️ Code | ID: Update-Backend Logik implementiert (backend/update.py). By: Codex (GPT-5) |
|
||||
| 29.12.2025 | ⚙️ Code | ID: Update-Models und Settings erweitert (Models, Status-File Paths). By: Codex (GPT-5) |
|
||||
| 29.12.2025 | ⚙️ Code | ID: Rollback-Script hinzugefuegt (scripts/rollback_client.sh). By: Codex (GPT-5) |
|
||||
| 30.12.2025 | ⚙️ Code | ID: Update-API Endpunkte implementiert (GET /update/status, POST /update/check, POST /update/apply, POST /update/rollback, GET /update/logs). By: Claude Sonnet 4.5 |
|
||||
| 30.12.2025 | ⚙️ Code | ID: Update-UI im Web-Frontend implementiert (Status-Anzeige, Check/Apply/Rollback Buttons, Logs-Viewer). By: Claude Sonnet 4.5 |
|
||||
|
||||
---
|
||||
## Legende
|
||||
|
||||
145
CLAUDE.md
Normal file
145
CLAUDE.md
Normal file
@ -0,0 +1,145 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Safe Kiddo Daemon (SKD) is a FastAPI-based service for managing local user accounts on kids' laptops. It provides account locking/unlocking with countdown notifications, optional sound alerts, and shutdown capabilities. The service exposes a REST API with bearer token auth (PAM or OIDC) and serves a minimal web UI.
|
||||
|
||||
## Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# Run the service manually (uses .venv, binds to 0.0.0.0:80)
|
||||
./scripts/run.sh
|
||||
|
||||
# Install service and dependencies
|
||||
sudo make install
|
||||
|
||||
# Service management
|
||||
sudo make up # Start service
|
||||
sudo make down # Stop service
|
||||
sudo make update # Pull latest from git, reinstall deps, restart
|
||||
|
||||
# Generate/set API token
|
||||
make token
|
||||
|
||||
# Health check (requires token)
|
||||
make healthcheck
|
||||
```
|
||||
|
||||
### Installation & Deployment
|
||||
```bash
|
||||
# Full install (creates service user, venv, systemd unit, PAM config)
|
||||
sudo ./scripts/install.sh
|
||||
|
||||
# Deploy to remote host (requires deploy_hosts.yml)
|
||||
./scripts/deploy.sh <host-name>
|
||||
|
||||
# Manual update on target
|
||||
ssh user@target 'cd /opt/sk && ./scripts/update.sh'
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# Python syntax validation
|
||||
python -m py_compile backend/*.py
|
||||
|
||||
# Run specific tests (no formal test runner yet; tests/ is empty)
|
||||
# Use curl for API testing:
|
||||
token=$(curl -s -X POST -H "Content-Type: application/json" \
|
||||
-d '{"username":"root","password":"..."}' \
|
||||
http://localhost/login | jq -r .token)
|
||||
curl -H "Authorization: Bearer $token" http://localhost/users
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Structure
|
||||
- `backend/`: FastAPI application
|
||||
- `app.py`: Main FastAPI app with route handlers
|
||||
- `actions.py`: User management actions (lock/unlock, notifications, shutdown logic)
|
||||
- `auth.py`: PAM authentication, JWT tokens, user/group authorization checks
|
||||
- `oidc.py`: OIDC client (dynamic discovery, token exchange, claims validation)
|
||||
- `update.py`: Update client logic (check/status/logs, triggers async update/rollback scripts)
|
||||
- `settings.py`: Environment-based configuration (Settings class, singleton via lru_cache)
|
||||
- `models.py`: Pydantic models for API requests/responses
|
||||
- `templates/`: Jinja2 templates for web UI
|
||||
- `scripts/`: Deployment and lifecycle scripts
|
||||
- `install.sh`: System setup (user, venv, systemd, PAM config)
|
||||
- `run.sh`: Manual service start
|
||||
- `update.sh`: Local git pull and service restart
|
||||
- `update_client.sh`: Full update flow with backup/rollback
|
||||
- `rollback_client.sh`: Restore from backup if update fails
|
||||
- `deploy.sh`: SSH-based deployment to remote hosts
|
||||
- `register_oidc_client.sh`: OIDC dynamic client registration helper
|
||||
- `sk.sh`: Legacy bash script (CLI fallback for direct SSH use)
|
||||
- `src/`: Hexagonal architecture skeleton (core/ports/adapters/ui) - currently empty placeholders
|
||||
- `docs/`: Detailed specs for OIDC validation, update API, status/log formats
|
||||
- `Makefile`: Convenience targets for install, service control, updates, token management
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
**Dual Authentication**: PAM-based local auth (root/sudo users) is always available; OIDC is optional if `SKD_OIDC_ISSUER`, `SKD_OIDC_CLIENT_ID`, and `SKD_OIDC_CLIENT_SECRET` are configured. Both modes issue JWT bearer tokens.
|
||||
|
||||
**Settings Management**: All config via environment variables (loaded from `/etc/skd/env` in production). `settings.py` provides a singleton `Settings` instance via `get_settings()` using `lru_cache`. FastAPI dependencies inject settings into route handlers.
|
||||
|
||||
**Action Execution**: `actions.py` wraps all privileged operations (usermod, pkill, shutdown) via `_run()` helper. Dry-run mode (`SKD_DRY_RUN=true`) logs commands without executing them.
|
||||
|
||||
**Update Flow**: `update.py` checks remote update service for new versions, writes status to JSON files, and triggers async scripts (`update_client.sh`, `rollback_client.sh`) that create backups, apply updates, and handle rollbacks on failure.
|
||||
|
||||
**Authorization**: `auth.py` checks both user allowlists (`SKD_AUTH_ALLOWED_USERS`) and group membership (`SKD_AUTH_ALLOWED_GROUPS`, defaults to `sudo`). UID 0 (root) always allowed for PAM. OIDC validates against `preferred_username`, `email`, or `sub` claims.
|
||||
|
||||
**Manageable Users**: Only system users with UID >= 1000, real shells (not nologin/false), and optional allowlist (`SKD_ALLOWED_USERS`) are exposed via API. Root accounts are never manageable.
|
||||
|
||||
## Configuration
|
||||
|
||||
Deployment config lives in `/etc/skd/env` (see `env.example` in repo root):
|
||||
- `SKD_AUTH_SECRET`: HMAC secret for JWT signing (must be strong in production)
|
||||
- `SKD_AUTH_ALLOWED_USERS`: Comma-separated user allowlist (for login and OIDC claims)
|
||||
- `SKD_AUTH_ALLOWED_GROUPS`: Groups whose members may log in (PAM only, default `sudo`)
|
||||
- `SKD_AUTH_PAM_SERVICE`: PAM service name (Ubuntu/Debian use `skd`, others may use `login`)
|
||||
- `SKD_OIDC_*`: OIDC provider config (ISSUER, CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, SCOPES)
|
||||
- `SKD_ALLOWED_USERS`: Comma-separated list of manageable system accounts (optional)
|
||||
- `SKD_DRY_RUN`: If `true`, logs all privileged commands without executing
|
||||
- `SKD_UPDATE_*`: Update service URL, token, interval, status/log file paths
|
||||
|
||||
## Important Workflows
|
||||
|
||||
### Disable User Flow
|
||||
1. API call to `/users/{username}/disable` with optional `{countdown, sound, message}`
|
||||
2. `actions.disable_user()` locks account via `usermod -L`
|
||||
3. If user logged in: sends desktop notifications, plays sound (if enabled), countdown loop with periodic reminders
|
||||
4. Terminates sessions via `pkill -KILL -u`
|
||||
5. Triggers `shutdown now` only if user was logged in
|
||||
|
||||
### OIDC Login Flow
|
||||
1. User accesses `/login/oidc/start` → redirected to provider with state cookie
|
||||
2. Provider redirects to `/login/oidc/callback` with code + state
|
||||
3. Validates state, exchanges code for tokens, extracts username from claims
|
||||
4. Issues JWT session cookie if user in allowlist
|
||||
|
||||
### Update Flow
|
||||
1. `check_update()` polls remote update service for latest manifest (version, artifact_url, sha256)
|
||||
2. `start_update()` writes "in_progress" status, launches `update_client.sh` in background
|
||||
3. Script creates backup, downloads artifact, verifies checksum, installs, restarts service
|
||||
4. On failure: `rollback_client.sh` restores from backup
|
||||
5. Status/logs written to JSON files at `SKD_UPDATE_STATUS_FILE` and `SKD_UPDATE_LOG_FILE`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Service runs as root by default (required for PAM, usermod, pkill, shutdown). Limit exposure via firewall.
|
||||
- Set strong `SKD_AUTH_SECRET` and rotate by changing value + restarting service.
|
||||
- Restrict API/Web UI to LAN/VPN; consider mTLS or IP allowlisting.
|
||||
- `skd` user/group created by install script; consider sudoers rules to limit privileges to specific commands.
|
||||
- OIDC redirect URI must match exactly (no wildcards); re-register client if host/port changes.
|
||||
- Validate TLS certificates in production; self-signed certs require CA trust or fallback to PAM.
|
||||
|
||||
## Notes
|
||||
|
||||
- Legacy `sk.sh` remains for emergency CLI fallback; API is preferred for all operations.
|
||||
- `src/` hexagonal architecture skeleton is currently unused; logic lives in `backend/`.
|
||||
- `tests/` directory exists but is empty; use manual curl-based API testing.
|
||||
- Both German and English comments exist in code; favor English going forward.
|
||||
- Deployment via `deploy.sh` supports both YAML (`deploy_hosts.yml`) and JSON host configs.
|
||||
- PAM service file (`/etc/pam.d/skd`) created by `scripts/install.sh` on Ubuntu/Debian; other distros may need manual setup.
|
||||
@ -32,6 +32,13 @@ Set in `/etc/skd/env` (see `env.example`):
|
||||
- `SKD_DEFAULT_COUNTDOWN`, `SKD_DEFAULT_SOUND`, `SKD_NOTIFY_TIMEOUT`: behavior defaults.
|
||||
- `SKD_DRY_RUN=true` to test without real account changes or shutdown.
|
||||
- `SKD_SOUND_PLAYER`/`SKD_SOUND_FILE`, `SKD_NOTIFY_SEND_PATH` if defaults differ.
|
||||
- Update client:
|
||||
- `SKD_UPDATE_URL` (default `https://update.wlkns.org`)
|
||||
- `SKD_UPDATE_TOKEN` (API token for update service)
|
||||
- `SKD_UPDATE_INTERVAL` (seconds; default 3600)
|
||||
- `SKD_UPDATE_STATUS_URL` (default `https://update.wlkns.org/status`)
|
||||
- `SKD_UPDATE_STATUS_FILE` (default `/var/lib/skd/update_status.json`)
|
||||
- `SKD_UPDATE_LOG_FILE` (default `/var/lib/skd/update_logs.jsonl`)
|
||||
Notes:
|
||||
- `./scripts/install.sh` will create `/etc/skd/env` from `env.example` if missing (edit afterwards) and ensure the `skd` service user/group exist.
|
||||
|
||||
|
||||
@ -15,9 +15,20 @@ from backend.auth import (
|
||||
issue_token,
|
||||
list_manageable_users,
|
||||
)
|
||||
from backend.models import ActionRequest, ActionResponse, LoginRequest, LoginResponse, UserStatus
|
||||
from backend.models import (
|
||||
ActionRequest,
|
||||
ActionResponse,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
UpdateActionResponse,
|
||||
UpdateCheckResponse,
|
||||
UpdateLogEntry,
|
||||
UpdateStatus,
|
||||
UserStatus,
|
||||
)
|
||||
from backend.oidc import OIDCClient, OIDCError
|
||||
from backend.settings import Settings, get_settings
|
||||
from backend import update
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@ -219,6 +230,67 @@ def enable_user(
|
||||
)
|
||||
|
||||
|
||||
@app.get("/update/status", response_model=UpdateStatus, dependencies=[Depends(get_current_admin)])
|
||||
def update_status(settings: Settings = Depends(get_settings)) -> UpdateStatus:
|
||||
status_data = update.get_status(settings)
|
||||
return UpdateStatus(**status_data)
|
||||
|
||||
|
||||
@app.post("/update/check", response_model=UpdateCheckResponse, dependencies=[Depends(get_current_admin)])
|
||||
def update_check(settings: Settings = Depends(get_settings)) -> UpdateCheckResponse:
|
||||
try:
|
||||
check_data = update.check_update(settings)
|
||||
except Exception as exc:
|
||||
logger.exception("Update check failed")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Update check failed: {str(exc)}",
|
||||
) from exc
|
||||
return UpdateCheckResponse(**check_data)
|
||||
|
||||
|
||||
@app.post("/update/apply", response_model=UpdateActionResponse, dependencies=[Depends(get_current_admin)])
|
||||
def update_apply(
|
||||
settings: Settings = Depends(get_settings),
|
||||
payload: dict | None = Body(default=None),
|
||||
) -> UpdateActionResponse:
|
||||
version = payload.get("version") if payload else None
|
||||
try:
|
||||
update.start_update(settings, version)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to start update")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to start update: {str(exc)}",
|
||||
) from exc
|
||||
return UpdateActionResponse(started=True, message="Update started")
|
||||
|
||||
|
||||
@app.post("/update/rollback", response_model=UpdateActionResponse, dependencies=[Depends(get_current_admin)])
|
||||
def update_rollback(settings: Settings = Depends(get_settings)) -> UpdateActionResponse:
|
||||
try:
|
||||
update.start_rollback(settings)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to start rollback")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to start rollback: {str(exc)}",
|
||||
) from exc
|
||||
return UpdateActionResponse(started=True, message="Rollback started")
|
||||
|
||||
|
||||
@app.get("/update/logs", dependencies=[Depends(get_current_admin)])
|
||||
def update_logs(settings: Settings = Depends(get_settings), limit: int = 200) -> list[dict]:
|
||||
try:
|
||||
return update.get_logs(settings, limit)
|
||||
except Exception as exc:
|
||||
logger.exception("Failed to retrieve update logs")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to retrieve logs: {str(exc)}",
|
||||
) from exc
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index(request: Request) -> HTMLResponse:
|
||||
return templates.TemplateResponse("index.html", {"request": request})
|
||||
|
||||
@ -34,3 +34,32 @@ class LoginRequest(BaseModel):
|
||||
class LoginResponse(BaseModel):
|
||||
token: str
|
||||
expires_in: int
|
||||
|
||||
|
||||
class UpdateStatus(BaseModel):
|
||||
current_version: str
|
||||
last_status: str
|
||||
last_error: Optional[str] = None
|
||||
last_timestamp: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateCheckResponse(BaseModel):
|
||||
available: bool
|
||||
latest_version: str
|
||||
artifact_url: str
|
||||
sha256: str
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class UpdateActionResponse(BaseModel):
|
||||
started: bool
|
||||
message: str
|
||||
|
||||
|
||||
class UpdateLogEntry(BaseModel):
|
||||
timestamp: str
|
||||
status: str
|
||||
message: Optional[str] = None
|
||||
version: Optional[str] = None
|
||||
error: Optional[str] = None
|
||||
device_id: Optional[str] = None
|
||||
|
||||
@ -39,6 +39,18 @@ class Settings:
|
||||
self.default_sound: bool = os.getenv("SKD_DEFAULT_SOUND", "false").lower() == "true"
|
||||
self.notify_timeout: int = int(os.getenv("SKD_NOTIFY_TIMEOUT", "5"))
|
||||
self.dry_run: bool = os.getenv("SKD_DRY_RUN", "false").lower() == "true"
|
||||
self.update_url: str = os.getenv("SKD_UPDATE_URL", "https://update.wlkns.org")
|
||||
self.update_token: str = os.getenv("SKD_UPDATE_TOKEN", "")
|
||||
self.update_interval: int = int(os.getenv("SKD_UPDATE_INTERVAL", "3600"))
|
||||
self.update_status_url: str = os.getenv(
|
||||
"SKD_UPDATE_STATUS_URL", "https://update.wlkns.org/status"
|
||||
)
|
||||
self.update_status_file: str = os.getenv(
|
||||
"SKD_UPDATE_STATUS_FILE", "/var/lib/skd/update_status.json"
|
||||
)
|
||||
self.update_log_file: str = os.getenv(
|
||||
"SKD_UPDATE_LOG_FILE", "/var/lib/skd/update_logs.jsonl"
|
||||
)
|
||||
# Paths/tools
|
||||
self.notify_send_path: str = os.getenv("SKD_NOTIFY_SEND_PATH", "notify-send")
|
||||
self.sound_player: str = os.getenv("SKD_SOUND_PLAYER", "paplay")
|
||||
|
||||
@ -84,6 +84,27 @@
|
||||
<div id="result" class="log"></div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>Update-Verwaltung</h3>
|
||||
<div id="updateStatus" class="log" style="margin-bottom: 1rem; padding: 1rem; background: var(--pico-card-background-color); border-radius: var(--pico-border-radius);">
|
||||
Lade Update-Status...
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<button id="checkUpdateBtn" type="button">Nach Updates suchen</button>
|
||||
<button id="applyUpdateBtn" type="button" disabled>Update installieren</button>
|
||||
<button id="rollbackBtn" type="button">Rollback durchführen</button>
|
||||
</div>
|
||||
|
||||
<div id="updateResult" class="log" style="margin-top: 1rem;"></div>
|
||||
|
||||
<details style="margin-top: 1.5rem;">
|
||||
<summary>Update-Logs anzeigen</summary>
|
||||
<button id="refreshLogsBtn" type="button" style="margin-top: 0.5rem;">Logs neu laden</button>
|
||||
<div id="updateLogs" class="log" style="margin-top: 1rem; max-height: 400px; overflow-y: auto;"></div>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
@ -221,6 +242,169 @@
|
||||
|
||||
checkSession();
|
||||
checkOidcStatus();
|
||||
|
||||
// ==================== Update Management ====================
|
||||
const updateStatusDiv = document.getElementById('updateStatus');
|
||||
const updateResultDiv = document.getElementById('updateResult');
|
||||
const updateLogsDiv = document.getElementById('updateLogs');
|
||||
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
|
||||
const applyUpdateBtn = document.getElementById('applyUpdateBtn');
|
||||
const rollbackBtn = document.getElementById('rollbackBtn');
|
||||
const refreshLogsBtn = document.getElementById('refreshLogsBtn');
|
||||
|
||||
let latestUpdateCheck = null;
|
||||
|
||||
async function refreshUpdateStatus() {
|
||||
try {
|
||||
const data = await api('/update/status');
|
||||
const statusText = `
|
||||
Version: ${data.current_version}
|
||||
Letzter Status: ${data.last_status || 'unbekannt'}
|
||||
${data.last_error ? `Fehler: ${data.last_error}` : ''}
|
||||
${data.last_timestamp ? `Zeitstempel: ${new Date(data.last_timestamp).toLocaleString('de-DE')}` : ''}
|
||||
`.trim();
|
||||
updateStatusDiv.textContent = statusText;
|
||||
} catch (err) {
|
||||
updateStatusDiv.textContent = `Fehler beim Laden des Update-Status: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
checkUpdateBtn.addEventListener('click', async () => {
|
||||
updateResultDiv.textContent = 'Prüfe auf Updates...';
|
||||
checkUpdateBtn.disabled = true;
|
||||
try {
|
||||
const data = await api('/update/check', { method: 'POST' });
|
||||
latestUpdateCheck = data;
|
||||
|
||||
if (data.available) {
|
||||
updateResultDiv.textContent = `
|
||||
✅ Update verfügbar!
|
||||
Version: ${data.latest_version}
|
||||
${data.message ? `Info: ${data.message}` : ''}
|
||||
|
||||
Klicke auf "Update installieren" um fortzufahren.
|
||||
`.trim();
|
||||
applyUpdateBtn.disabled = false;
|
||||
} else {
|
||||
updateResultDiv.textContent = `✓ Keine Updates verfügbar. Aktuelle Version ist aktuell.`;
|
||||
applyUpdateBtn.disabled = true;
|
||||
}
|
||||
} catch (err) {
|
||||
updateResultDiv.textContent = `❌ Fehler beim Update-Check: ${err.message}`;
|
||||
applyUpdateBtn.disabled = true;
|
||||
} finally {
|
||||
checkUpdateBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
applyUpdateBtn.addEventListener('click', async () => {
|
||||
if (!latestUpdateCheck || !latestUpdateCheck.available) {
|
||||
updateResultDiv.textContent = '❌ Bitte zuerst nach Updates suchen.';
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmed = confirm(
|
||||
`Update auf Version ${latestUpdateCheck.latest_version} installieren?\n\n` +
|
||||
`⚠️ WICHTIG:\n` +
|
||||
`- Ein Backup wird automatisch erstellt\n` +
|
||||
`- Der Service wird neu gestartet\n` +
|
||||
`- Bei Fehlern erfolgt automatischer Rollback\n\n` +
|
||||
`Fortfahren?`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
updateResultDiv.textContent = 'Update wird gestartet... (läuft im Hintergrund)';
|
||||
applyUpdateBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const data = await api('/update/apply', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ version: latestUpdateCheck.latest_version })
|
||||
});
|
||||
updateResultDiv.textContent = `
|
||||
✓ ${data.message}
|
||||
|
||||
Das Update läuft jetzt im Hintergrund.
|
||||
Aktualisiere den Status in wenigen Sekunden, um den Fortschritt zu sehen.
|
||||
`.trim();
|
||||
|
||||
// Auto-refresh nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
refreshUpdateStatus();
|
||||
applyUpdateBtn.disabled = true;
|
||||
}, 5000);
|
||||
|
||||
} catch (err) {
|
||||
updateResultDiv.textContent = `❌ Fehler beim Starten des Updates: ${err.message}`;
|
||||
applyUpdateBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
rollbackBtn.addEventListener('click', async () => {
|
||||
const confirmed = confirm(
|
||||
`Rollback zum letzten Backup durchführen?\n\n` +
|
||||
`⚠️ WICHTIG:\n` +
|
||||
`- Dies stellt die vorherige Version wieder her\n` +
|
||||
`- Der Service wird neu gestartet\n` +
|
||||
`- Ein Backup muss vorhanden sein\n\n` +
|
||||
`Fortfahren?`
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
updateResultDiv.textContent = 'Rollback wird gestartet... (läuft im Hintergrund)';
|
||||
rollbackBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const data = await api('/update/rollback', { method: 'POST' });
|
||||
updateResultDiv.textContent = `
|
||||
✓ ${data.message}
|
||||
|
||||
Der Rollback läuft jetzt im Hintergrund.
|
||||
Aktualisiere den Status in wenigen Sekunden.
|
||||
`.trim();
|
||||
|
||||
// Auto-refresh nach 5 Sekunden
|
||||
setTimeout(() => {
|
||||
refreshUpdateStatus();
|
||||
}, 5000);
|
||||
|
||||
} catch (err) {
|
||||
updateResultDiv.textContent = `❌ Fehler beim Rollback: ${err.message}`;
|
||||
} finally {
|
||||
rollbackBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
async function refreshUpdateLogs() {
|
||||
updateLogsDiv.textContent = 'Lade Logs...';
|
||||
try {
|
||||
const logs = await api('/update/logs');
|
||||
if (!logs || logs.length === 0) {
|
||||
updateLogsDiv.textContent = 'Keine Logs vorhanden.';
|
||||
return;
|
||||
}
|
||||
|
||||
// Reverse chronological (newest first)
|
||||
const logEntries = logs.reverse().map(entry => {
|
||||
const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString('de-DE') : 'unbekannt';
|
||||
const status = entry.status || 'unknown';
|
||||
const version = entry.version || '-';
|
||||
const error = entry.error ? `\n Fehler: ${entry.error}` : '';
|
||||
return `[${timestamp}] ${status} - Version: ${version}${error}`;
|
||||
});
|
||||
|
||||
updateLogsDiv.textContent = logEntries.join('\n\n');
|
||||
} catch (err) {
|
||||
updateLogsDiv.textContent = `Fehler beim Laden der Logs: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
refreshLogsBtn.addEventListener('click', refreshUpdateLogs);
|
||||
|
||||
// Initial load
|
||||
refreshUpdateStatus();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
142
backend/update.py
Normal file
142
backend/update.py
Normal file
@ -0,0 +1,142 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import httpx
|
||||
|
||||
from backend.settings import Settings
|
||||
|
||||
|
||||
def _project_root() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _read_version() -> str:
|
||||
try:
|
||||
return (_project_root() / "VERSION").read_text(encoding="utf-8").strip()
|
||||
except OSError:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _status_path(settings: Settings) -> Path:
|
||||
return Path(settings.update_status_file)
|
||||
|
||||
|
||||
def _log_path(settings: Settings) -> Path:
|
||||
return Path(settings.update_log_file)
|
||||
|
||||
|
||||
def _ensure_parent(path: Path) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def _write_status(settings: Settings, status: str, version: str, error: str | None = None) -> None:
|
||||
status_path = _status_path(settings)
|
||||
_ensure_parent(status_path)
|
||||
payload = {
|
||||
"device_id": os.uname().nodename,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"error": error or "",
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
}
|
||||
status_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
_append_log(settings, payload)
|
||||
|
||||
|
||||
def _append_log(settings: Settings, payload: Dict[str, Any]) -> None:
|
||||
log_path = _log_path(settings)
|
||||
_ensure_parent(log_path)
|
||||
with log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
|
||||
|
||||
def get_status(settings: Settings) -> Dict[str, Any]:
|
||||
current_version = _read_version()
|
||||
status_path = _status_path(settings)
|
||||
if status_path.exists():
|
||||
try:
|
||||
data = json.loads(status_path.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
|
||||
return {
|
||||
"current_version": current_version,
|
||||
"last_status": data.get("status", "unknown"),
|
||||
"last_error": data.get("error") or None,
|
||||
"last_timestamp": data.get("timestamp"),
|
||||
}
|
||||
|
||||
|
||||
def _parse_version(value: str) -> List[int]:
|
||||
return [int(part) for part in value.split(".")]
|
||||
|
||||
|
||||
def check_update(settings: Settings) -> Dict[str, Any]:
|
||||
headers = {}
|
||||
if settings.update_token:
|
||||
headers["Authorization"] = f"Bearer {settings.update_token}"
|
||||
|
||||
with httpx.Client(timeout=10.0) as client:
|
||||
response = client.get(settings.update_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
manifest = response.json()
|
||||
|
||||
latest_version = manifest.get("version", "")
|
||||
artifact_url = manifest.get("artifact_url", "")
|
||||
sha256 = manifest.get("sha256", "")
|
||||
message = manifest.get("message")
|
||||
|
||||
available = False
|
||||
current_version = _read_version()
|
||||
try:
|
||||
available = _parse_version(latest_version) > _parse_version(current_version)
|
||||
except ValueError:
|
||||
if latest_version and latest_version != current_version:
|
||||
available = True
|
||||
|
||||
return {
|
||||
"available": available,
|
||||
"latest_version": latest_version,
|
||||
"artifact_url": artifact_url,
|
||||
"sha256": sha256,
|
||||
"message": message,
|
||||
}
|
||||
|
||||
|
||||
def _run_async(script_path: Path, settings: Settings) -> None:
|
||||
env = os.environ.copy()
|
||||
env["SKD_UPDATE_STATUS_FILE"] = settings.update_status_file
|
||||
env["SKD_UPDATE_LOG_FILE"] = settings.update_log_file
|
||||
subprocess.Popen([str(script_path)], env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def start_update(settings: Settings, version: str | None = None) -> None:
|
||||
_write_status(settings, "in_progress", version or _read_version())
|
||||
script = _project_root() / "scripts" / "update_client.sh"
|
||||
_run_async(script, settings)
|
||||
|
||||
|
||||
def start_rollback(settings: Settings) -> None:
|
||||
_write_status(settings, "in_progress", _read_version())
|
||||
script = _project_root() / "scripts" / "rollback_client.sh"
|
||||
_run_async(script, settings)
|
||||
|
||||
|
||||
def get_logs(settings: Settings, limit: int = 200) -> List[Dict[str, Any]]:
|
||||
log_path = _log_path(settings)
|
||||
if not log_path.exists():
|
||||
return []
|
||||
lines = log_path.read_text(encoding="utf-8").splitlines()
|
||||
entries: List[Dict[str, Any]] = []
|
||||
for line in lines[-limit:]:
|
||||
try:
|
||||
entries.append(json.loads(line))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return entries
|
||||
60
docs/update-api.md
Normal file
60
docs/update-api.md
Normal file
@ -0,0 +1,60 @@
|
||||
ID: DOC_000006 | Version: 0.1.0 | Status: Draft
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# Update API (Kiddo Backend)
|
||||
|
||||
## Purpose
|
||||
Definiert interne API-Endpunkte fuer Update-Status, Check, Apply, Rollback und Logs.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### GET /update/status
|
||||
Antwort:
|
||||
```json
|
||||
{
|
||||
"current_version": "0.1.2",
|
||||
"last_status": "success|failed|unknown",
|
||||
"last_error": "<optional>",
|
||||
"last_timestamp": "2025-12-28T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /update/check
|
||||
Antwort:
|
||||
```json
|
||||
{
|
||||
"available": true,
|
||||
"latest_version": "0.1.3",
|
||||
"artifact_url": "https://update.wlkns.org/kiddo/kiddo-0.1.3.tar.gz",
|
||||
"sha256": "<hex>",
|
||||
"message": "<optional>"
|
||||
}
|
||||
```
|
||||
|
||||
### POST /update/apply
|
||||
Body (optional):
|
||||
```json
|
||||
{ "version": "0.1.3" }
|
||||
```
|
||||
Antwort:
|
||||
```json
|
||||
{ "started": true, "message": "update started" }
|
||||
```
|
||||
|
||||
### POST /update/rollback
|
||||
Antwort:
|
||||
```json
|
||||
{ "started": true, "message": "rollback started" }
|
||||
```
|
||||
|
||||
### GET /update/logs
|
||||
Antwort:
|
||||
```json
|
||||
[
|
||||
{"timestamp":"2025-12-28T12:34:56Z","status":"success","message":"updated to 0.1.2"}
|
||||
]
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Alle Endpunkte erfordern Auth (Session/Bearer).
|
||||
- Apply/Rollback starten async; UI pollt /update/status.
|
||||
49
docs/update-client.md
Normal file
49
docs/update-client.md
Normal file
@ -0,0 +1,49 @@
|
||||
ID: DOC_000004 | Version: 0.1.0 | Status: Draft
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# Client Update Flow (Kiddo)
|
||||
|
||||
## Purpose
|
||||
Definiert den Client-seitigen Ablauf fuer das Pull-Update vom Update-Service.
|
||||
|
||||
## Manifest Format (JSON)
|
||||
Beispiel:
|
||||
```json
|
||||
{
|
||||
"version": "0.1.2",
|
||||
"artifact_url": "https://update.wlkns.org/kiddo/kiddo-0.1.2.tar.gz",
|
||||
"sha256": "<hex>",
|
||||
"sig_url": "https://update.wlkns.org/kiddo/kiddo-0.1.2.sig"
|
||||
}
|
||||
```
|
||||
|
||||
## Flow (High Level)
|
||||
1. Manifest abrufen (auth optional via Bearer Token).
|
||||
2. `artifact_url` herunterladen.
|
||||
3. SHA256 pruefen (Signatur optional).
|
||||
4. In Staging-Verzeichnis entpacken.
|
||||
5. Service stoppen.
|
||||
6. Atomic swap: aktuelles Verzeichnis sichern, Staging nach `/opt/sk` verschieben.
|
||||
7. Service starten.
|
||||
8. Bei Fehlern Rollback auf Backup.
|
||||
|
||||
## Prototype Script
|
||||
- `scripts/update_client.sh` implementiert den Flow als CLI-Prototyp.
|
||||
- Erfordert `curl`, `tar`, `sha256sum`, `python3` und `systemctl`.
|
||||
|
||||
## Rollback
|
||||
- Wenn Start fehlschlaegt: Backup nach `/opt/sk` zurueck, Service neu starten.
|
||||
- Backup-Verzeichnis benoetigt genuegend Speicher.
|
||||
|
||||
## Security Notes
|
||||
- Artefakte muessen checksum-verifiziert sein.
|
||||
- Token-Handling ueber `SKD_UPDATE_TOKEN`.
|
||||
|
||||
## Constraints
|
||||
- Update-Service ist extern (update.wlkns.org).
|
||||
- Service muss als root stoppen/starten koennen.
|
||||
|
||||
## Status Reporting
|
||||
- Status wird per HTTP POST an `https://update.wlkns.org/status` gemeldet.
|
||||
- Schema siehe `docs/update-status.md`.
|
||||
- Lokaler Status/Logs liegen unter `/var/lib/skd` (konfigurierbar via ENV).
|
||||
25
docs/update-status.md
Normal file
25
docs/update-status.md
Normal file
@ -0,0 +1,25 @@
|
||||
ID: DOC_000005 | Version: 0.1.0 | Status: Draft
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# Update Status Reporting
|
||||
|
||||
## Purpose
|
||||
Definiert das Status-Schema fuer Update-Resultate und den Uebertragungsweg.
|
||||
|
||||
## Status Schema (JSON)
|
||||
```json
|
||||
{
|
||||
"device_id": "<hostname>",
|
||||
"version": "0.1.2",
|
||||
"status": "success|failed",
|
||||
"error": "<optional message>",
|
||||
"timestamp": "2025-12-28T12:34:56Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Transport
|
||||
- HTTP POST an `https://update.wlkns.org/status`
|
||||
- Auth: Bearer Token (`SKD_UPDATE_TOKEN`)
|
||||
|
||||
## Notes
|
||||
- Statusmeldungen sind best-effort; Fehler beim Senden blockieren kein Update.
|
||||
@ -21,5 +21,12 @@ SKD_OIDC_STATE_COOKIE_NAME=skd_oidc_state
|
||||
SKD_DEFAULT_COUNTDOWN=60
|
||||
SKD_DEFAULT_SOUND=false
|
||||
SKD_NOTIFY_TIMEOUT=5
|
||||
# Update client configuration
|
||||
SKD_UPDATE_URL=https://update.wlkns.org
|
||||
SKD_UPDATE_TOKEN=
|
||||
SKD_UPDATE_INTERVAL=3600
|
||||
SKD_UPDATE_STATUS_URL=https://update.wlkns.org/status
|
||||
SKD_UPDATE_STATUS_FILE=/var/lib/skd/update_status.json
|
||||
SKD_UPDATE_LOG_FILE=/var/lib/skd/update_logs.jsonl
|
||||
# Set to true to test without performing real system changes
|
||||
SKD_DRY_RUN=false
|
||||
|
||||
@ -10,9 +10,9 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
||||
✅ Stabilization
|
||||
|
||||
## Aktueller Fokus
|
||||
1. Dokumentierter Ist-Zustand der Module.
|
||||
2. Pflege der Anforderungen bei neuen Features.
|
||||
3. Doku und Ops-Automation aktuell halten.
|
||||
1. Client-Update-Mechanik planen (EPIC_000008).
|
||||
2. Dokumentierter Ist-Zustand der Module.
|
||||
3. Pflege der Anforderungen bei neuen Features.
|
||||
|
||||
## Projekt-Tagebuch (Kurz, optional)
|
||||
| Datum | Typ | Beschreibung |
|
||||
@ -86,12 +86,22 @@ Sicheres, remote steuerbares System zum Sperren/Entsperren lokaler Nutzerkonten.
|
||||
- [x] TASK_000023: README runbook notes
|
||||
|
||||
### EPIC_000008: Client-Side Update Mechanism
|
||||
- [ ] US_000026: Client bezieht Updates (Pull)
|
||||
- [ ] TASK_000027: Update endpoint config
|
||||
- [ ] US_000027: Client verifiziert und wendet Updates an
|
||||
- [ ] TASK_000028: Verify and apply update
|
||||
- [ ] US_000028: Client meldet Update-Status
|
||||
- [ ] TASK_000029: Report update status
|
||||
- [x] US_000026: Client bezieht Updates (Pull)
|
||||
- [x] TASK_000027: Update endpoint config
|
||||
- [x] US_000027: Client verifiziert und wendet Updates an
|
||||
- [x] TASK_000028: Verify and apply update
|
||||
- [x] US_000028: Client meldet Update-Status
|
||||
- [x] TASK_000029: Report update status
|
||||
- [x] US_000029: Update-Status im Web-UI anzeigen
|
||||
- [x] TASK_000030: UI update status view
|
||||
- [x] US_000030: Update-Check im Web-UI ausloesen
|
||||
- [x] TASK_000031: UI update check trigger
|
||||
- [x] US_000031: Update im Web-UI anstossen
|
||||
- [x] TASK_000032: UI update apply action
|
||||
- [x] US_000032: Update-Logs im Web-UI anzeigen
|
||||
- [x] TASK_000033: UI update logs view
|
||||
- [x] US_000033: Rollback im Web-UI anstossen
|
||||
- [x] TASK_000034: UI rollback action
|
||||
|
||||
### EPIC_000009: Update Webservice (External Team)
|
||||
- [ ] US_000026: Client bezieht Updates (Pull)
|
||||
|
||||
@ -39,3 +39,8 @@ Ermoegliche einen robusten Client-Update-Flow mit Verifikation und Rollback.
|
||||
- US_000026: Client bezieht Updates (Pull)
|
||||
- US_000027: Client verifiziert und wendet Updates an
|
||||
- US_000028: Client meldet Update-Status
|
||||
- US_000029: Update-Status im Web-UI anzeigen
|
||||
- US_000030: Update-Check im Web-UI ausloesen
|
||||
- US_000031: Update im Web-UI anstossen
|
||||
- US_000032: Update-Logs im Web-UI anzeigen
|
||||
- US_000033: Rollback im Web-UI anstossen
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
ID: US_000026 | Version: 0.1.0 | Status: Draft
|
||||
ID: US_000026 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000026: Client bezieht Updates (Pull)
|
||||
|
||||
Status: Draft
|
||||
Status: Done
|
||||
|
||||
Als Betreiber moechte ich, dass der Client Updates per Pull von einem Update-Service bezieht, damit Deployments ohne SSH moeglich sind.
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
ID: US_000027 | Version: 0.1.0 | Status: Draft
|
||||
ID: US_000027 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000027: Client verifiziert und wendet Updates an
|
||||
|
||||
Status: Draft
|
||||
Status: Done
|
||||
|
||||
Als Betreiber moechte ich, dass der Client Updates verifiziert und sicher anwendet, damit fehlerhafte Pakete keine Ausfaelle verursachen.
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
ID: US_000028 | Version: 0.1.0 | Status: Draft
|
||||
ID: US_000028 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000028: Client meldet Update-Status
|
||||
|
||||
Status: Draft
|
||||
Status: Done
|
||||
|
||||
Als Betreiber moechte ich Statusmeldungen vom Client erhalten, damit Update-Ergebnisse nachvollziehbar sind.
|
||||
|
||||
|
||||
17
project-management/requirements/stories/US_000029.md
Normal file
17
project-management/requirements/stories/US_000029.md
Normal file
@ -0,0 +1,17 @@
|
||||
ID: US_000029 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000029: Update-Status im Web-UI anzeigen
|
||||
|
||||
Status: Done
|
||||
|
||||
Als Admin moechte ich die aktuelle Version und den Update-Status im Web-UI sehen, damit ich den Zustand schnell pruefen kann.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given die Web-UI ist erreichbar
|
||||
- When ich den Update-Bereich aufrufe
|
||||
- Then ich sehe die aktuell laufende Version
|
||||
- And ich sehe den letzten Update-Status (success/failed) mit Zeitstempel
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000030: UI update status view (Details bei Story-Start)
|
||||
17
project-management/requirements/stories/US_000030.md
Normal file
17
project-management/requirements/stories/US_000030.md
Normal file
@ -0,0 +1,17 @@
|
||||
ID: US_000030 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000030: Update-Check im Web-UI ausloesen
|
||||
|
||||
Status: Done
|
||||
|
||||
Als Admin moechte ich manuell nach Updates suchen koennen, damit ich Updates sofort pruefen kann.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given die Web-UI ist erreichbar
|
||||
- When ich auf "Nach Updates suchen" klicke
|
||||
- Then wird ein Check gegen `https://update.wlkns.org` gestartet
|
||||
- And das Ergebnis (neue Version verfuegbar/keine Updates/Fehler) wird angezeigt
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000031: UI update check trigger (Details bei Story-Start)
|
||||
19
project-management/requirements/stories/US_000031.md
Normal file
19
project-management/requirements/stories/US_000031.md
Normal file
@ -0,0 +1,19 @@
|
||||
ID: US_000031 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000031: Update im Web-UI anstossen
|
||||
|
||||
Status: Done
|
||||
|
||||
Als Admin moechte ich ein Update im Web-UI anstossen, damit der Client die neue Version installiert.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given ein Update ist verfuegbar
|
||||
- And eine vorherige Version ist gesichert oder wird vor dem Start gesichert
|
||||
- When ich "Update installieren" ausloese
|
||||
- Then wird der Update-Client gestartet
|
||||
- And der Fortschritt/Status wird im UI angezeigt
|
||||
- And Fehler werden klar im UI gemeldet
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000032: UI update apply action (Details bei Story-Start)
|
||||
17
project-management/requirements/stories/US_000032.md
Normal file
17
project-management/requirements/stories/US_000032.md
Normal file
@ -0,0 +1,17 @@
|
||||
ID: US_000032 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000032: Update-Logs im Web-UI anzeigen
|
||||
|
||||
Status: Done
|
||||
|
||||
Als Admin moechte ich Update-Logs im Web-UI einsehen, damit Fehler nachvollziehbar sind.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given ein Update-Versuch wurde ausgefuehrt
|
||||
- When ich die Update-Logs oeffne
|
||||
- Then sehe ich eine chronologische Liste mit Zeitstempel und Ergebnis
|
||||
- And sensible Daten (Tokens) werden nicht angezeigt
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000033: UI update logs view (Details bei Story-Start)
|
||||
19
project-management/requirements/stories/US_000033.md
Normal file
19
project-management/requirements/stories/US_000033.md
Normal file
@ -0,0 +1,19 @@
|
||||
ID: US_000033 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# US_000033: Rollback im Web-UI anstossen
|
||||
|
||||
Status: Done
|
||||
|
||||
Als Admin moechte ich einen Rollback im Web-UI anstossen, damit ich nach einem fehlerhaften Update schnell zur letzten Version zurueckkehre.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- Given ein vorheriger Update-Stand ist verfuegbar und wurde gesichert
|
||||
- And der Rollback darf nur starten, wenn das Backup verfuegbar ist
|
||||
- When ich "Rollback" ausloese
|
||||
- Then wird der Rollback-Mechanismus gestartet
|
||||
- And der Status/Fortschritt wird im UI angezeigt
|
||||
- And Fehler werden klar im UI gemeldet
|
||||
|
||||
## Task-Platzhalter
|
||||
- TASK_000034: UI rollback action (Details bei Story-Start)
|
||||
20
project-management/requirements/tasks/TASK_000027.md
Normal file
20
project-management/requirements/tasks/TASK_000027.md
Normal file
@ -0,0 +1,20 @@
|
||||
ID: TASK_000027 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000027: Update endpoint config
|
||||
|
||||
## Outcome
|
||||
Client kennt Update-Endpoint, Auth und Polling-Konfiguration.
|
||||
|
||||
## Story-Bezug
|
||||
US_000026
|
||||
|
||||
## Beschreibung
|
||||
- Konfigurationskeys definieren (`SKD_UPDATE_URL`, `SKD_UPDATE_TOKEN`, `SKD_UPDATE_INTERVAL`).
|
||||
- Default auf `https://update.wlkns.org` festlegen.
|
||||
- Lesen der Config in Settings/ENV beschreiben.
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Konfigurations-Keys dokumentiert (`env.example`, README).
|
||||
- Default-URL ist festgelegt (`https://update.wlkns.org`).
|
||||
- Settings laden die Update-Konfiguration aus ENV.
|
||||
23
project-management/requirements/tasks/TASK_000028.md
Normal file
23
project-management/requirements/tasks/TASK_000028.md
Normal file
@ -0,0 +1,23 @@
|
||||
ID: TASK_000028 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000028: Verify and apply update
|
||||
|
||||
## Outcome
|
||||
Update-Artefakt wird verifiziert und atomar angewendet.
|
||||
|
||||
## Story-Bezug
|
||||
US_000027
|
||||
|
||||
## Beschreibung
|
||||
- JSON-Manifest lesen (Version, `artifact_url`, `sha256`, optional `sig_url`).
|
||||
- `tar.gz` herunterladen und SHA256 pruefen.
|
||||
- Update atomar anwenden (staging, swap, rollback).
|
||||
- Fehlerfall dokumentieren (Rollback, Status).
|
||||
- Ablauf dokumentiert in `docs/update-client.md`.
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Verifikation (Checksum/Signatur) ist beschrieben.
|
||||
- Atomare Anwendung und Rollback-Strategie sind definiert.
|
||||
- Fehlerbilder sind erfasst.
|
||||
- Prototyp-Skript ist als Referenz vorhanden.
|
||||
21
project-management/requirements/tasks/TASK_000029.md
Normal file
21
project-management/requirements/tasks/TASK_000029.md
Normal file
@ -0,0 +1,21 @@
|
||||
ID: TASK_000029 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000029: Report update status
|
||||
|
||||
## Outcome
|
||||
Client meldet Update-Status mit Version und Fehlerbild.
|
||||
|
||||
## Story-Bezug
|
||||
US_000028
|
||||
|
||||
## Beschreibung
|
||||
- Status-Schema definieren (Version, Ergebnis, Fehler, Zeitpunkt).
|
||||
- Transportweg festlegen (z.B. HTTP POST oder Log-Export).
|
||||
- Erfolg/Fehler konsistent dokumentieren.
|
||||
- Schema dokumentiert in `docs/update-status.md`.
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Status-Schema dokumentiert.
|
||||
- Uebertragungspfad beschrieben.
|
||||
- Erfolg/Fehler werden eindeutig gemeldet.
|
||||
19
project-management/requirements/tasks/TASK_000030.md
Normal file
19
project-management/requirements/tasks/TASK_000030.md
Normal file
@ -0,0 +1,19 @@
|
||||
ID: TASK_000030 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000030: UI update status view
|
||||
|
||||
## Outcome
|
||||
Web-UI zeigt aktuelle Version und letzten Update-Status.
|
||||
|
||||
## Story-Bezug
|
||||
US_000029
|
||||
|
||||
## Beschreibung
|
||||
- Update-Status und Version im UI anzeigen.
|
||||
- Letzten Status mit Zeitstempel visualisieren.
|
||||
- API-Basis: `GET /update/status` (siehe `docs/update-api.md`).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- UI zeigt Version + letzten Status.
|
||||
- Fehler/keine Daten werden sauber angezeigt.
|
||||
19
project-management/requirements/tasks/TASK_000031.md
Normal file
19
project-management/requirements/tasks/TASK_000031.md
Normal file
@ -0,0 +1,19 @@
|
||||
ID: TASK_000031 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000031: UI update check trigger
|
||||
|
||||
## Outcome
|
||||
Web-UI kann einen Update-Check gegen `https://update.wlkns.org` ausloesen.
|
||||
|
||||
## Story-Bezug
|
||||
US_000030
|
||||
|
||||
## Beschreibung
|
||||
- Button/Action fuer "Nach Updates suchen".
|
||||
- Ergebnisanzeige (Update verfuegbar/keine Updates/Fehler).
|
||||
- API-Basis: `POST /update/check` (siehe `docs/update-api.md`).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- UI zeigt Ergebnis des Update-Checks.
|
||||
- Fehler werden klar angezeigt.
|
||||
21
project-management/requirements/tasks/TASK_000032.md
Normal file
21
project-management/requirements/tasks/TASK_000032.md
Normal file
@ -0,0 +1,21 @@
|
||||
ID: TASK_000032 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000032: UI update apply action
|
||||
|
||||
## Outcome
|
||||
Web-UI kann ein Update installieren.
|
||||
|
||||
## Story-Bezug
|
||||
US_000031
|
||||
|
||||
## Beschreibung
|
||||
- "Update installieren" Action.
|
||||
- Vor Start Backup-Pruefung/Backup-Erstellung erzwingen.
|
||||
- Fortschritt/Status im UI.
|
||||
- API-Basis: `POST /update/apply` (siehe `docs/update-api.md`).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Update startet nur mit vorhandenem Backup.
|
||||
- UI zeigt Fortschritt/Status.
|
||||
- Fehler sind sichtbar.
|
||||
19
project-management/requirements/tasks/TASK_000033.md
Normal file
19
project-management/requirements/tasks/TASK_000033.md
Normal file
@ -0,0 +1,19 @@
|
||||
ID: TASK_000033 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000033: UI update logs view
|
||||
|
||||
## Outcome
|
||||
Web-UI zeigt Update-Logs chronologisch.
|
||||
|
||||
## Story-Bezug
|
||||
US_000032
|
||||
|
||||
## Beschreibung
|
||||
- Logliste mit Zeitstempel und Ergebnis.
|
||||
- Keine sensiblen Daten anzeigen.
|
||||
- API-Basis: `GET /update/logs` (siehe `docs/update-api.md`).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Logs sind sichtbar und chronologisch sortiert.
|
||||
- Sensible Daten sind ausgefiltert.
|
||||
20
project-management/requirements/tasks/TASK_000034.md
Normal file
20
project-management/requirements/tasks/TASK_000034.md
Normal file
@ -0,0 +1,20 @@
|
||||
ID: TASK_000034 | Version: 0.1.0 | Status: Done
|
||||
By: Codex (GPT-5)
|
||||
|
||||
# TASK_000034: UI rollback action
|
||||
|
||||
## Outcome
|
||||
Web-UI kann einen Rollback ausloesen.
|
||||
|
||||
## Story-Bezug
|
||||
US_000033
|
||||
|
||||
## Beschreibung
|
||||
- "Rollback" Action.
|
||||
- Nur aktiv, wenn Backup vorhanden.
|
||||
- Fortschritt/Status im UI.
|
||||
- API-Basis: `POST /update/rollback` (siehe `docs/update-api.md`).
|
||||
|
||||
## Definition of Done (DoD)
|
||||
- Rollback startet nur bei verfuegbarem Backup.
|
||||
- UI zeigt Status/Fehler.
|
||||
76
scripts/rollback_client.sh
Executable file
76
scripts/rollback_client.sh
Executable file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_NAME="${SERVICE_NAME:-skd}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
write_status() {
|
||||
local status="$1"
|
||||
local error="${2:-}"
|
||||
local version="$3"
|
||||
SKD_STATUS="${status}" SKD_ERROR="${error}" SKD_VERSION="${version}" \
|
||||
SKD_STATUS_FILE="${STATUS_FILE}" SKD_LOG_FILE="${LOG_FILE}" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
status = os.environ["SKD_STATUS"]
|
||||
error = os.environ.get("SKD_ERROR", "")
|
||||
version = os.environ.get("SKD_VERSION", "unknown")
|
||||
status_file = Path(os.environ["SKD_STATUS_FILE"])
|
||||
log_file = Path(os.environ["SKD_LOG_FILE"])
|
||||
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"device_id": os.uname().nodename,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
}
|
||||
status_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with log_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
LATEST_BACKUP="$(ls -dt /opt/sk_backup_* 2>/dev/null | head -1 || true)"
|
||||
if [[ -z "${LATEST_BACKUP}" ]]; then
|
||||
log "No backup found; rollback aborted."
|
||||
write_status "failed" "no backup found" "unknown"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="unknown"
|
||||
if [[ -f "${LATEST_BACKUP}/VERSION" ]]; then
|
||||
VERSION="$(cat "${LATEST_BACKUP}/VERSION" | tr -d '\n')"
|
||||
fi
|
||||
|
||||
log "Stopping service ${SERVICE_NAME}..."
|
||||
sudo systemctl stop "${SERVICE_NAME}.service"
|
||||
|
||||
FAILED_DIR="${INSTALL_DIR}_failed_$(date +%s)"
|
||||
log "Swapping ${INSTALL_DIR} -> ${FAILED_DIR}..."
|
||||
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||
sudo mv "${INSTALL_DIR}" "${FAILED_DIR}"
|
||||
fi
|
||||
sudo mv "${LATEST_BACKUP}" "${INSTALL_DIR}"
|
||||
|
||||
log "Starting service ${SERVICE_NAME}..."
|
||||
if sudo systemctl start "${SERVICE_NAME}.service"; then
|
||||
log "Rollback completed."
|
||||
write_status "success" "" "${VERSION}"
|
||||
else
|
||||
log "Rollback failed."
|
||||
write_status "failed" "service start failed" "${VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
171
scripts/update_client.sh
Executable file
171
scripts/update_client.sh
Executable file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SERVICE_NAME="${SERVICE_NAME:-skd}"
|
||||
UPDATE_URL="${SKD_UPDATE_URL:-https://update.wlkns.org}"
|
||||
UPDATE_TOKEN="${SKD_UPDATE_TOKEN:-}"
|
||||
STATUS_URL="${SKD_UPDATE_STATUS_URL:-https://update.wlkns.org/status}"
|
||||
STATUS_FILE="${SKD_UPDATE_STATUS_FILE:-/var/lib/skd/update_status.json}"
|
||||
LOG_FILE="${SKD_UPDATE_LOG_FILE:-/var/lib/skd/update_logs.jsonl}"
|
||||
INSTALL_DIR="${INSTALL_DIR:-/opt/sk}"
|
||||
STAGING_DIR=""
|
||||
MANIFEST_FILE=""
|
||||
ARTIFACT_FILE=""
|
||||
BACKUP_DIR=""
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
|
||||
}
|
||||
|
||||
report_status() {
|
||||
local status="$1"
|
||||
local error="${2:-}"
|
||||
local timestamp
|
||||
timestamp="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
device_id = os.uname().nodename
|
||||
version = os.environ.get("SKD_VERSION", "unknown")
|
||||
status = os.environ.get("SKD_STATUS", "unknown")
|
||||
error = os.environ.get("SKD_ERROR", "")
|
||||
timestamp = os.environ.get("SKD_TIMESTAMP") or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
status_file = Path(os.environ["SKD_STATUS_FILE"])
|
||||
log_file = Path(os.environ["SKD_LOG_FILE"])
|
||||
|
||||
status_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
payload = {
|
||||
"device_id": device_id,
|
||||
"version": version,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"timestamp": timestamp,
|
||||
}
|
||||
|
||||
status_file.write_text(json.dumps(payload), encoding="utf-8")
|
||||
with log_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload) + "\n")
|
||||
PY
|
||||
local payload
|
||||
payload=$(cat <<JSON
|
||||
{"device_id":"$(hostname)","version":"${VERSION}","status":"${status}","error":"${error}","timestamp":"${timestamp}"}
|
||||
JSON
|
||||
)
|
||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||
curl -sS -X POST -H "Authorization: Bearer ${UPDATE_TOKEN}" -H "Content-Type: application/json" \
|
||||
-d "${payload}" "${STATUS_URL}" >/dev/null || true
|
||||
else
|
||||
curl -sS -X POST -H "Content-Type: application/json" -d "${payload}" "${STATUS_URL}" >/dev/null || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${STAGING_DIR}" && -d "${STAGING_DIR}" ]]; then
|
||||
rm -rf "${STAGING_DIR}"
|
||||
fi
|
||||
if [[ -n "${MANIFEST_FILE}" && -f "${MANIFEST_FILE}" ]]; then
|
||||
rm -f "${MANIFEST_FILE}"
|
||||
fi
|
||||
if [[ -n "${ARTIFACT_FILE}" && -f "${ARTIFACT_FILE}" ]]; then
|
||||
rm -f "${ARTIFACT_FILE}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
require_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "Required command not found: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
require_cmd curl
|
||||
require_cmd tar
|
||||
require_cmd sha256sum
|
||||
require_cmd python3
|
||||
|
||||
log "Fetching update manifest from ${UPDATE_URL}..."
|
||||
MANIFEST_FILE="$(mktemp)"
|
||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${UPDATE_URL}" -o "${MANIFEST_FILE}"
|
||||
else
|
||||
curl -fsS "${UPDATE_URL}" -o "${MANIFEST_FILE}"
|
||||
fi
|
||||
|
||||
read_manifest() {
|
||||
python3 - <<'PY' "${MANIFEST_FILE}"
|
||||
import json
|
||||
import sys
|
||||
path = sys.argv[1]
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
for key in ("version", "artifact_url", "sha256"):
|
||||
if key not in data:
|
||||
sys.exit(f"Manifest missing '{key}'")
|
||||
print(data["version"])
|
||||
print(data["artifact_url"])
|
||||
print(data["sha256"])
|
||||
PY
|
||||
}
|
||||
|
||||
readarray -t manifest_values < <(read_manifest)
|
||||
VERSION="${manifest_values[0]}"
|
||||
ARTIFACT_URL="${manifest_values[1]}"
|
||||
ARTIFACT_SHA256="${manifest_values[2]}"
|
||||
|
||||
log "Downloading artifact ${ARTIFACT_URL} (version ${VERSION})..."
|
||||
ARTIFACT_FILE="$(mktemp --suffix=.tar.gz)"
|
||||
if [[ -n "${UPDATE_TOKEN}" ]]; then
|
||||
curl -fsS -H "Authorization: Bearer ${UPDATE_TOKEN}" "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}"
|
||||
else
|
||||
curl -fsS "${ARTIFACT_URL}" -o "${ARTIFACT_FILE}"
|
||||
fi
|
||||
|
||||
log "Verifying checksum..."
|
||||
CALC_SHA="$(sha256sum "${ARTIFACT_FILE}" | awk '{print $1}')"
|
||||
if [[ "${CALC_SHA}" != "${ARTIFACT_SHA256}" ]]; then
|
||||
echo "Checksum mismatch: expected ${ARTIFACT_SHA256} got ${CALC_SHA}" >&2
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="checksum mismatch" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "checksum mismatch"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STAGING_DIR="$(mktemp -d /opt/sk_update.XXXXXX)"
|
||||
log "Extracting to staging ${STAGING_DIR}..."
|
||||
tar -xzf "${ARTIFACT_FILE}" -C "${STAGING_DIR}"
|
||||
|
||||
BACKUP_DIR="/opt/sk_backup_${VERSION}_$(date +%s)"
|
||||
log "Stopping service ${SERVICE_NAME}..."
|
||||
sudo systemctl stop "${SERVICE_NAME}.service"
|
||||
|
||||
log "Swapping ${INSTALL_DIR} -> ${BACKUP_DIR}..."
|
||||
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||
sudo mv "${INSTALL_DIR}" "${BACKUP_DIR}"
|
||||
fi
|
||||
sudo mv "${STAGING_DIR}" "${INSTALL_DIR}"
|
||||
STAGING_DIR=""
|
||||
|
||||
log "Starting service ${SERVICE_NAME}..."
|
||||
if sudo systemctl start "${SERVICE_NAME}.service"; then
|
||||
log "Update applied successfully."
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="success" SKD_ERROR="" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "success" ""
|
||||
else
|
||||
log "Service failed to start, rolling back..."
|
||||
sudo rm -rf "${INSTALL_DIR}"
|
||||
if [[ -d "${BACKUP_DIR}" ]]; then
|
||||
sudo mv "${BACKUP_DIR}" "${INSTALL_DIR}"
|
||||
fi
|
||||
sudo systemctl start "${SERVICE_NAME}.service" || true
|
||||
SKD_VERSION="${VERSION}" SKD_STATUS="failed" SKD_ERROR="service start failed" \
|
||||
SKD_TIMESTAMP="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" SKD_STATUS_FILE="${STATUS_FILE}" \
|
||||
SKD_LOG_FILE="${LOG_FILE}" report_status "failed" "service start failed"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user