Compare commits
2 Commits
7cce19797e
...
7109e88e07
| Author | SHA1 | Date | |
|---|---|---|---|
| 7109e88e07 | |||
| 242b835a19 |
@@ -1,5 +1,29 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.81.0 — retire the drive-activation banner; add a standalone "Kiszolgáló újraindítása" button (2026-06-23)
|
||||||
|
- **Removed the obsolete drive-activation banner.** In the intermediary-mount model an enrolled drive
|
||||||
|
binds **live** into the running guest (agent `disks.go` — no `pct set -mpN`, no slot, no reboot), so
|
||||||
|
the "… meghajtó aktiválásra vár / Újraindítás most (~30 mp)" banner was a relic of the old per-drive
|
||||||
|
reboot model. It was also effectively dead since v0.78 (`pendingActivationDrives` keyed `attached` by
|
||||||
|
the agent's RAW `MountPath` but compared it to the now-STABLE `sp.Path`). Removed: the
|
||||||
|
`{{if .PendingDrives}}` banner block + `window.activatePendingDrives` JS (`settings.html`), the
|
||||||
|
`data["PendingDrives"]` feed (`handlers.go`), and the dead `pendingActivationDrives` helper +
|
||||||
|
its now-unused `internal/system` import (`storage_handlers.go`).
|
||||||
|
- **Repointed the reboot endpoint to a non-storage route.** Renamed `handleStorageActivate` →
|
||||||
|
`HandleServerReboot` and split out a testable `serverReboot` core (mirrors `runStorageInit`); removed
|
||||||
|
the `/api/storage/activate` case from `ServeStorageAPI`; mounted the handler at the new
|
||||||
|
`/api/server/reboot` (same `RequireAuth` + `CsrfProtect`) in `cmd/controller/main.go`. The agent
|
||||||
|
`GuestReboot` primitive is reused unchanged. (`/api/storage/activate` now returns 404.)
|
||||||
|
*Note:* the handler is exported (`HandleServerReboot`) because `cmd/controller/main.go` wires it
|
||||||
|
cross-package — same convention as every other web handler mounted there.
|
||||||
|
- **Added the standalone "Kiszolgáló újraindítása" settings card.** A deliberate full-server (guest)
|
||||||
|
restart affordance, a sibling to the existing "Vezérlő újraindítása" controller-only restart.
|
||||||
|
New `settings-card` + `restartServer()` JS (reuses the existing `pollRestart()` loop) in
|
||||||
|
`settings.html`; posts to `/api/server/reboot`.
|
||||||
|
- **Test:** `TestHandleServerReboot_CallsGuestReboot` (`storage_handlers_test.go`) — a fake `diskAgent`
|
||||||
|
asserts `GuestReboot` is invoked exactly once and the response is 202 `{ok:true, rebooting:true}`.
|
||||||
|
`diskAgent`/`mockAgent` gained `GuestReboot`. Green gate: `go build ./... && go vet ./... && go test ./...`.
|
||||||
|
|
||||||
### v0.80.0 — disk card: show + act on the stable path, not the raw host mount (2026-06-23)
|
### v0.80.0 — disk card: show + act on the stable path, not the raw host mount (2026-06-23)
|
||||||
- Follow-up to v0.78/0.79. The storage disk card displayed the drive's **raw** host PVE mount
|
- Follow-up to v0.78/0.79. The storage disk card displayed the drive's **raw** host PVE mount
|
||||||
(`/mnt/<name>`) — which doesn't exist inside the guest — instead of the **stable** in-guest path
|
(`/mnt/<name>`) — which doesn't exist inside the guest — instead of the **stable** in-guest path
|
||||||
|
|||||||
@@ -1,60 +1,46 @@
|
|||||||
# REPORT — controller v0.80.0: disk card shows + acts on the stable path
|
# REPORT — retire drive-activation banner; add standalone "Kiszolgáló újraindítása" button
|
||||||
|
|
||||||
## Baseline
|
**Repo:** felhom-controller · **Baseline:** `main` @ `7cce19797` (v0.80.0) → **v0.81.0**
|
||||||
- felhom-controller `main` @ `9596c8e`, **v0.79.0 → v0.80.0**. Trunk-based, `main`.
|
**Commit:** `242b835` (code + CHANGELOG + README). **Date:** 2026-06-23.
|
||||||
- Follow-up to v0.78.0 (stable-path registration) + v0.79.0 (registered badge on stable path).
|
|
||||||
|
|
||||||
## Problem (operator-reported, screenshot)
|
## Why
|
||||||
The storage disk card still displayed each external drive's **raw** host PVE mount `/mnt/<name>`
|
In the intermediary-mount model an enrolled drive binds **live** into the running guest (agent
|
||||||
(`usb · /dev/sdc1 · /mnt/felhom-flash`). Inside the guest/controller the drive lives at the **stable**
|
`disks.go` — no `pct set -mpN`, no slot, no reboot), so the "… meghajtó aktiválásra vár /
|
||||||
path `/mnt/felhom-drives/<name>` (the agent's `guest_path`) — which is what the registry, app `HDD_PATH`,
|
Újraindítás most (~30 mp)" banner was an obsolete relic of the old per-drive reboot model. It was
|
||||||
and FileBrowser use; the raw path doesn't even exist in the guest. Two latent bugs rode on the same
|
also effectively **dead since v0.78**: `pendingActivationDrives` keyed `attached` by the agent's RAW
|
||||||
mismatch: the Leválasztás/Törlés buttons posted the **raw** path, so `handleStorageEject`/`handleStorageWipe`
|
`MountPath` but compared it to the now-STABLE `sp.Path`. Retired it; added a deliberate full-server
|
||||||
would unmount the drive but `RemoveStoragePath(raw)` would miss the **stable** registry entry (orphan);
|
restart affordance in its place (sibling to the controller-only restart).
|
||||||
and the impact warning `/api/storage/impact?where=raw` found no apps (HDD_PATH is the stable path).
|
|
||||||
|
|
||||||
## Fix
|
## Files changed
|
||||||
**`controller/internal/web/templates/settings.html`** (disk-view JS):
|
- `controller/internal/web/storage_handlers.go`
|
||||||
- Card sub-line now shows `regKey(d)` (= `d.guest_path || d.mount_path`) — the stable in-guest path.
|
- Removed dead `pendingActivationDrives` helper + the now-unused `internal/system` import.
|
||||||
- `actions()` passes the **stable** path to `confirmEject`/`confirmWipe` (handlers map it to raw for the
|
- Renamed `handleStorageActivate` → `HandleServerReboot`; split out testable `serverReboot(w,r,agent)` core.
|
||||||
agent via `agentWhere()` AND deregister the stable registry entry). `registerDrive` keeps the **raw**
|
- Added `GuestReboot(ctx) error` to the `diskAgent` interface (`*agentapi.Client` already satisfies it).
|
||||||
`mount_path` (its agent guest-attach operates on raw; `handleStorageRegister` maps to stable).
|
- Removed the `/api/storage/activate` case from `ServeStorageAPI` (→ 404).
|
||||||
- `confirmEject`/`confirmWipe` derive the type-to-confirm name from the **basename**
|
- `controller/cmd/controller/main.go` — mounted `/api/server/reboot` (`RequireAuth`+`CsrfProtect`) next to the storage route.
|
||||||
(`where.split('/').filter(Boolean).pop()`) so it still equals the server's `path.Base(where)` check
|
- `controller/internal/web/handlers.go` — removed the `data["PendingDrives"]` feed.
|
||||||
when `where` is the stable path.
|
- `controller/internal/web/templates/settings.html` — removed the `{{if .PendingDrives}}` banner block and `window.activatePendingDrives`; added the **"Kiszolgáló újraindítása"** settings card + `restartServer()` JS (reuses the existing `pollRestart()` loop).
|
||||||
|
- `controller/internal/web/storage_handlers_test.go` — `mockAgent` gained `GuestReboot`; new test.
|
||||||
|
- `CHANGELOG.md` (v0.81.0 entry), `controller/README.md` (full-server-restart section).
|
||||||
|
|
||||||
**`controller/internal/web/storage_handlers.go`** (`handleStorageWipe`):
|
### Note on naming
|
||||||
- The agent eject now uses `agentWhere(req.Where)` (was `req.Where` verbatim), matching
|
The HTTP handler is **exported** (`HandleServerReboot`), not the lowercase name in the spec snippet:
|
||||||
`handleStorageEject`. So a stable registered path maps to the raw mount for the agent while
|
`cmd/controller/main.go` wires it cross-package, and every web handler mounted there is exported. The
|
||||||
`RemoveStoragePath(req.Where)` deregisters the stable entry. Agent op is the identical raw path as
|
unexported `serverReboot` core carries the logic and is what the test exercises (same split-out pattern
|
||||||
before; only the registry bookkeeping is corrected. The format step is unchanged (keyed on `device`).
|
as `runStorageInit`).
|
||||||
|
|
||||||
Agent-facing operations (eject unmount, format) produce the IDENTICAL raw paths/device as before — only
|
## Test
|
||||||
the controller's display + its own registry bookkeeping/impact lookups now use the stable path.
|
`TestHandleServerReboot_CallsGuestReboot` (`storage_handlers_test.go`): a fake `diskAgent` asserts
|
||||||
|
`GuestReboot` is invoked **exactly once** and the response is **202** with `{ok:true, rebooting:true}`.
|
||||||
|
|
||||||
## Tests
|
```
|
||||||
- `go build ./... && go vet ./... && go test ./...` — **all green** (incl. `TestTemplatesParse`).
|
go build ./... → BUILD_OK
|
||||||
- `handleStorageWipe`'s new mapping rides on `agentWhere`, already covered by `TestAgentWhere`
|
go vet ./... → VET_OK
|
||||||
(stable→raw, raw idempotent). JS changes covered by template parse (no JS unit harness — existing
|
go test ./... → ok (internal/web 1.640s, new test PASS); all packages ok
|
||||||
pattern).
|
```
|
||||||
|
|
||||||
## Deploy (v0.80.0 → guest 9201)
|
Grep confirmed **zero** remaining references to `PendingDrives`, `pendingActivationDrives`,
|
||||||
- Built + pushed `gitea.dooplex.hu/admin/felhom-controller:0.80.0` on 192.168.0.180; deployed to 9201
|
`activatePendingDrives`, `activate-drives-btn`, `/api/storage/activate`, `handleStorageActivate`.
|
||||||
(pull → `/etc/felhom-controller-image` → restart bootstrap unit); `docker ps` → `:0.80.0 Up (healthy)`.
|
|
||||||
|
|
||||||
## Verification (served page JS + data the card renders from)
|
## Deploy + live verification
|
||||||
- Served `/settings` JS (the disk card is built client-side from these): sub-line
|
_To be filled after the build/deploy + §9 verification (banner gone, button present, route 200/404, regression)._
|
||||||
`… +(d.mount_path?' · '+esc(regKey(d)):'')` (stable); `confirmEject('+reg+')` and
|
|
||||||
`confirmWipe('+dev+','+reg+')` (stable); `registerDrive('+mpRaw+')` (raw). Confirms v0.80.0 shipped.
|
|
||||||
- `GET /api/disks`: `felhom-flash → guest_path=/mnt/felhom-drives/felhom-flash`,
|
|
||||||
`felhom-usb → guest_path=/mnt/felhom-drives/felhom-usb` ⇒ the card renders the stable path and the
|
|
||||||
eject/wipe buttons carry it.
|
|
||||||
- Regression: `[PASS] Storage paths: 2 connected, 0 disconnected`; `[PASS] System data path: /mnt/sys_drive`.
|
|
||||||
- NOT live-clicked: the destructive eject/wipe buttons (would unmount/format the demo drives). Verified
|
|
||||||
the path-mapping logic by inspection + the agent ops are unchanged (raw via `agentWhere`/device); the
|
|
||||||
rendering is pure client-side JS over the confirmed-correct inputs (bridge not attached this session).
|
|
||||||
|
|
||||||
## Observations (recorded, NOT acted on)
|
|
||||||
- `pendingActivationDrives` still keys off the agent's raw `MountPath` (the "~30 mp újraindítás" banner);
|
|
||||||
obsolete in the live-activating intermediary model — key off `GuestPath`/`BoundUnderParent` or retire.
|
|
||||||
- Free-form settings storage-add (`handlers.go`) registers a user-typed path verbatim (advanced flow).
|
|
||||||
|
|||||||
@@ -1271,6 +1271,8 @@ Bearer token authentication, 3-attempt retry with 5-second backoff. Push status
|
|||||||
|
|
||||||
**Manual restart (v0.70.0):** `POST /api/selfrestart` (session auth + CSRF via the `/api/` mount) runs the same helper — surfaced as the **"Vezérlő újraindítása"** button on the settings page (confirm → POST → poll `GET /` every 2 s → reload), so a customer can recover the controller without rebooting the whole guest.
|
**Manual restart (v0.70.0):** `POST /api/selfrestart` (session auth + CSRF via the `/api/` mount) runs the same helper — surfaced as the **"Vezérlő újraindítása"** button on the settings page (confirm → POST → poll `GET /` every 2 s → reload), so a customer can recover the controller without rebooting the whole guest.
|
||||||
|
|
||||||
|
**Full-server restart (v0.81.0):** `POST /api/server/reboot` (session auth + CSRF) reboots the **whole guest** via the host agent's `GuestReboot` primitive (`HandleServerReboot` in `internal/web/storage_handlers.go`, delegating to the testable `serverReboot` core; agent reboots detached + returns 202). Surfaced as a separate **"Kiszolgáló újraindítása"** settings card alongside the controller-only restart, reusing the same `pollRestart()` reload loop. It replaces the retired drive-activation banner (v0.81.0): in the intermediary-mount model an enrolled drive binds **live** into the running guest, so storage no longer needs a reboot to activate — this button is purely a deliberate full-system restart.
|
||||||
|
|
||||||
#### App Telemetry (`internal/metrics/telemetry.go`, `internal/metrics/logscanner.go`, `internal/report/telemetry.go`)
|
#### App Telemetry (`internal/metrics/telemetry.go`, `internal/metrics/logscanner.go`, `internal/report/telemetry.go`)
|
||||||
|
|
||||||
Each report push now includes per-app telemetry data:
|
Each report push now includes per-app telemetry data:
|
||||||
|
|||||||
@@ -726,6 +726,9 @@ func main() {
|
|||||||
mux.Handle("/api/disks/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeDiskAPI))))
|
mux.Handle("/api/disks/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeDiskAPI))))
|
||||||
// Guided storage provisioning (init/attach/eject orchestration over the agent disk API + registry).
|
// Guided storage provisioning (init/attach/eject orchestration over the agent disk API + registry).
|
||||||
mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI))))
|
mux.Handle("/api/storage/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeStorageAPI))))
|
||||||
|
// Standalone full-server (guest) restart — the "Kiszolgáló újraindítása" maintenance affordance,
|
||||||
|
// a sibling to the controller-only /api/selfrestart. Reuses the agent GuestReboot primitive.
|
||||||
|
mux.Handle("/api/server/reboot", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.HandleServerReboot))))
|
||||||
// Whole-guest (appliance) backup visibility + manual trigger. Distinct prefix from apiRouter's
|
// Whole-guest (appliance) backup visibility + manual trigger. Distinct prefix from apiRouter's
|
||||||
// app-data /api/backup/{run,status} (DB dumps) to avoid shadowing the /api/ catch-all subtree.
|
// app-data /api/backup/{run,status} (DB dumps) to avoid shadowing the /api/ catch-all subtree.
|
||||||
mux.Handle("/api/guest-backup/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeBackupAPI))))
|
mux.Handle("/api/guest-backup/", webServer.RequireAuth(webServer.CsrfProtect(http.HandlerFunc(webServer.ServeBackupAPI))))
|
||||||
|
|||||||
@@ -903,9 +903,6 @@ func (s *Server) settingsData() map[string]interface{} {
|
|||||||
storageViews = append(storageViews, view)
|
storageViews = append(storageViews, view)
|
||||||
}
|
}
|
||||||
data["StoragePaths"] = storageViews
|
data["StoragePaths"] = storageViews
|
||||||
// Drives enrolled but not yet activated in the guest (slice 10 P2): they need the user-triggered
|
|
||||||
// "Újraindítás most" to take effect (the host-side live inject is blocked on an unprivileged guest).
|
|
||||||
data["PendingDrives"] = s.pendingActivationDrives()
|
|
||||||
|
|
||||||
// Recovery info for emergency section
|
// Recovery info for emergency section
|
||||||
data["RetrievalPassword"] = s.settings.GetRetrievalPassword()
|
data["RetrievalPassword"] = s.settings.GetRetrievalPassword()
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||||
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Guided storage provisioning (rebuilt on the agent-delegated disk model). The controller is a thin
|
// Guided storage provisioning (rebuilt on the agent-delegated disk model). The controller is a thin
|
||||||
@@ -35,6 +34,7 @@ type diskAgent interface {
|
|||||||
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
|
EjectDisk(ctx context.Context, where string) (agentapi.EjectResult, error)
|
||||||
Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error)
|
Decommission(ctx context.Context, where string) (agentapi.DecommissionResult, error)
|
||||||
GuestAttach(ctx context.Context, where string) error
|
GuestAttach(ctx context.Context, where string) error
|
||||||
|
GuestReboot(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// mountNameRe is the safe `/mnt/<name>` component (DNS-ish: letters, digits, _ , -).
|
// mountNameRe is the safe `/mnt/<name>` component (DNS-ish: letters, digits, _ , -).
|
||||||
@@ -164,44 +164,6 @@ func (s *Server) runStorageAttach(ctx context.Context, agent diskAgent, device,
|
|||||||
return storageInitResult{Registered: true, Where: stable}, nil
|
return storageInitResult{Registered: true, Where: stable}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// pendingActivationDrives returns registered storage paths that are NOT yet live-mounted in this
|
|
||||||
// container but whose backing drive the agent reports present+attached — enrolled drives waiting for
|
|
||||||
// the guest restart that activates their bind (slice 10 P2; the host-side live inject is blocked on an
|
|
||||||
// unprivileged guest). The customer activates them with the "Újraindítás most" button (one restart
|
|
||||||
// batches all). Best-effort: agent unreachable → none.
|
|
||||||
func (s *Server) pendingActivationDrives() []string {
|
|
||||||
paths := s.settings.GetStoragePaths()
|
|
||||||
if len(paths) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
agent, err := s.agentClient()
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
resp, err := agent.Disks(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
attached := map[string]bool{}
|
|
||||||
for _, d := range resp.Disks {
|
|
||||||
if d.MountPath != "" && d.State == "attached" {
|
|
||||||
attached[d.MountPath] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var pending []string
|
|
||||||
for _, sp := range paths {
|
|
||||||
if sp.Decommissioned {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if attached[sp.Path] && !system.IsMountPoint(sp.Path) {
|
|
||||||
pending = append(pending, sp.Path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pending
|
|
||||||
}
|
|
||||||
|
|
||||||
// reEnrollClearMarker un-retires a re-plugged decommissioned drive (Change 4): clears the soft marker
|
// reEnrollClearMarker un-retires a re-plugged decommissioned drive (Change 4): clears the soft marker
|
||||||
// and restores Schedulable so its apps' "missing storage" indicator clears. Returns true if it acted.
|
// and restores Schedulable so its apps' "missing storage" indicator clears. Returns true if it acted.
|
||||||
func (s *Server) reEnrollClearMarker(where string) (bool, error) {
|
func (s *Server) reEnrollClearMarker(where string) (bool, error) {
|
||||||
@@ -283,8 +245,6 @@ func (s *Server) ServeStorageAPI(w http.ResponseWriter, r *http.Request) {
|
|||||||
s.handleStorageImpact(w, r)
|
s.handleStorageImpact(w, r)
|
||||||
case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost:
|
case r.URL.Path == "/api/storage/register" && r.Method == http.MethodPost:
|
||||||
s.handleStorageRegister(w, r)
|
s.handleStorageRegister(w, r)
|
||||||
case r.URL.Path == "/api/storage/activate" && r.Method == http.MethodPost:
|
|
||||||
s.handleStorageActivate(w, r)
|
|
||||||
case r.URL.Path == "/api/storage/migrate" && r.Method == http.MethodPost:
|
case r.URL.Path == "/api/storage/migrate" && r.Method == http.MethodPost:
|
||||||
s.handleStorageMigrate(w, r)
|
s.handleStorageMigrate(w, r)
|
||||||
case r.URL.Path == "/api/storage/migrate-app" && r.Method == http.MethodPost:
|
case r.URL.Path == "/api/storage/migrate-app" && r.Method == http.MethodPost:
|
||||||
@@ -643,21 +603,31 @@ func (s *Server) handleStorageWipe(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"device": req.Device, "wiped": fr.Formatted, "durable_id": fr.DurableID})
|
writeDiskJSON(w, http.StatusOK, true, "", map[string]any{"device": req.Device, "wiped": fr.Formatted, "durable_id": fr.DurableID})
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStorageActivate reboots the guest to activate pending drive binds (slice 10 P2). The agent
|
// HandleServerReboot reboots the whole guest (server) as a deliberate maintenance action — the
|
||||||
// reboots detached + returns 202; this controller restarts with the guest, so the caller's response
|
// standalone "Kiszolgáló újraindítása" affordance, a sibling to the controller-only restart
|
||||||
// may be cut short — the UI handles that and reloads after the restart window.
|
// (/api/selfrestart). The agent reboots detached + returns 202; this controller restarts with the
|
||||||
func (s *Server) handleStorageActivate(w http.ResponseWriter, r *http.Request) {
|
// guest, so the caller's response may be cut short — the UI handles that and reloads after the
|
||||||
|
// restart window. (It reuses the agent GuestReboot primitive that previously backed the now-retired
|
||||||
|
// drive-activation banner; in the intermediary-mount model a drive binds live, so no reboot is needed
|
||||||
|
// to activate storage — this button is purely a full-server restart.)
|
||||||
|
func (s *Server) HandleServerReboot(w http.ResponseWriter, r *http.Request) {
|
||||||
agent, err := s.agentClient()
|
agent, err := s.agentClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
writeDiskJSON(w, http.StatusServiceUnavailable, false, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.serverReboot(w, r, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serverReboot is the testable core of HandleServerReboot: invoke the agent's guest reboot and return
|
||||||
|
// the 202 envelope (or the agent error). Split out so it can be exercised with a fake diskAgent.
|
||||||
|
func (s *Server) serverReboot(w http.ResponseWriter, r *http.Request, agent diskAgent) {
|
||||||
if err := agent.GuestReboot(r.Context()); err != nil {
|
if err := agent.GuestReboot(r.Context()); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] guest reboot (activate pending drives) failed: %v", err)
|
s.logger.Printf("[ERROR] [web] server reboot (guest restart) failed: %v", err)
|
||||||
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
writeDiskJSON(w, http.StatusBadGateway, false, err.Error(), nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.logger.Printf("[WARN] [web] guest restart requested to activate pending drive binds")
|
s.logger.Printf("[WARN] [web] full server (guest) restart requested by operator")
|
||||||
writeDiskJSON(w, http.StatusAccepted, true, "", map[string]any{"rebooting": true})
|
writeDiskJSON(w, http.StatusAccepted, true, "", map[string]any{"rebooting": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ type mockAgent struct {
|
|||||||
formatCalls []formatCall
|
formatCalls []formatCall
|
||||||
guestAttachCalls []string
|
guestAttachCalls []string
|
||||||
decommissionCalls []string
|
decommissionCalls []string
|
||||||
|
guestRebootCalls int
|
||||||
|
guestRebootErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type assignCall struct{ uuid, where, fstype string }
|
type assignCall struct{ uuid, where, fstype string }
|
||||||
@@ -71,6 +73,10 @@ func (m *mockAgent) GuestAttach(_ context.Context, where string) error {
|
|||||||
m.guestAttachCalls = append(m.guestAttachCalls, where)
|
m.guestAttachCalls = append(m.guestAttachCalls, where)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockAgent) GuestReboot(context.Context) error {
|
||||||
|
m.guestRebootCalls++
|
||||||
|
return m.guestRebootErr
|
||||||
|
}
|
||||||
|
|
||||||
func testServer(t *testing.T) *Server {
|
func testServer(t *testing.T) *Server {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -255,6 +261,38 @@ func TestHandleStorageRegister_RegistersStablePath(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHandleServerReboot_CallsGuestReboot exercises the standalone "Kiszolgáló újraindítása" core:
|
||||||
|
// it must invoke the agent's GuestReboot exactly once and return the 202 envelope. (The HTTP handler
|
||||||
|
// HandleServerReboot delegates to this core after building the live agent client — same split-out
|
||||||
|
// pattern as runStorageInit, so the fake diskAgent can be injected here.)
|
||||||
|
func TestHandleServerReboot_CallsGuestReboot(t *testing.T) {
|
||||||
|
s := testServer(t)
|
||||||
|
agent := &mockAgent{}
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/server/reboot", nil)
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
s.serverReboot(rr, req, agent)
|
||||||
|
|
||||||
|
if agent.guestRebootCalls != 1 {
|
||||||
|
t.Fatalf("expected GuestReboot invoked exactly once, got %d", agent.guestRebootCalls)
|
||||||
|
}
|
||||||
|
if rr.Code != http.StatusAccepted {
|
||||||
|
t.Fatalf("expected 202 Accepted, got %d (body %s)", rr.Code, rr.Body.String())
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Data struct {
|
||||||
|
Rebooting bool `json:"rebooting"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||||
|
t.Fatalf("decode response: %v", err)
|
||||||
|
}
|
||||||
|
if !resp.OK || !resp.Data.Rebooting {
|
||||||
|
t.Fatalf("expected {ok:true, data.rebooting:true}, got %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFSUUIDForDevice(t *testing.T) {
|
func TestFSUUIDForDevice(t *testing.T) {
|
||||||
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
disks := agentapi.DisksResponse{Disks: []agentapi.DiskInfo{
|
||||||
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
|
{BackingDevice: "/dev/sda1", DurableID: "uuid:AAAA"},
|
||||||
|
|||||||
@@ -408,15 +408,6 @@ function pollUntilBack() {
|
|||||||
<a href="/settings/storage/attach" class="btn btn-sm btn-outline">🔗 Meglévő meghajtó csatolása</a>
|
<a href="/settings/storage/attach" class="btn btn-sm btn-outline">🔗 Meglévő meghajtó csatolása</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .PendingDrives}}
|
|
||||||
<div class="alert alert-warning" style="margin-top:1rem">
|
|
||||||
<strong>{{len .PendingDrives}} meghajtó aktiválásra vár.</strong>
|
|
||||||
Az újonnan csatolt adatmeghajtók a vendég rövid (~30 mp) újraindítása után válnak elérhetővé az alkalmazások számára.
|
|
||||||
<div style="margin-top:.6rem"><button class="btn btn-sm btn-primary" id="activate-drives-btn" onclick="activatePendingDrives()">Újraindítás most (~30 mp)</button></div>
|
|
||||||
<div id="activate-result" style="margin-top:.5rem"></div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
<div style="margin-top:1.5rem">
|
||||||
<h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4>
|
<h4 style="margin-bottom:.25rem">Meghajtók (ügynök nézet)</h4>
|
||||||
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> az ügynök saját vizsgálattal állapítja meg: a rendszer- és biztonsági-mentés meghajtók védettek (csak operátori aláírással módosíthatók), a felhasználói adatmeghajtókat Ön kezeli.</p>
|
<p class="form-hint" style="margin-bottom:.75rem">A host-ügynök által észlelt meghajtók élő nézete. A meghajtó <strong>szerepkörét</strong> az ügynök saját vizsgálattal állapítja meg: a rendszer- és biztonsági-mentés meghajtók védettek (csak operátori aláírással módosíthatók), a felhasználói adatmeghajtókat Ön kezeli.</p>
|
||||||
@@ -564,16 +555,6 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
location.reload();
|
location.reload();
|
||||||
}catch(e){ alert('Hiba: '+e.message); }
|
}catch(e){ alert('Hiba: '+e.message); }
|
||||||
};
|
};
|
||||||
// Activate pending drive binds by rebooting the guest (~30s). The reboot takes the controller down
|
|
||||||
// too, so the fetch may not resolve — we reload after the restart window regardless.
|
|
||||||
window.activatePendingDrives=function(){
|
|
||||||
if(!confirm('A vendég újraindul (~30 másodperc). Eközben az alkalmazások és a vezérlőpult rövid időre nem elérhetők. Folytatja?')) return;
|
|
||||||
var btn=document.getElementById('activate-drives-btn'); var out=document.getElementById('activate-result');
|
|
||||||
if(btn) btn.disabled=true;
|
|
||||||
if(out) out.innerHTML='<span class="form-hint">Újraindítás folyamatban… az oldal automatikusan újratöltődik.</span>';
|
|
||||||
fetch('/api/storage/activate',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},csrfHeaders())}).catch(function(){});
|
|
||||||
setTimeout(function(){location.reload();}, 45000);
|
|
||||||
};
|
|
||||||
window.confirmEject=function(where){
|
window.confirmEject=function(where){
|
||||||
// The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may
|
// The confirm name is the drive BASENAME (matches the server's path.Base(where) check); `where` may
|
||||||
// be the stable /mnt/felhom-drives/<name> path, so strip the whole directory, not just the /mnt/ prefix.
|
// be the stable /mnt/felhom-drives/<name> path, so strip the whole directory, not just the /mnt/ prefix.
|
||||||
@@ -1144,6 +1125,16 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
|
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Kiszolgáló újraindítása</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor használja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.
|
||||||
|
</p>
|
||||||
|
<div id="server-restart-status"></div>
|
||||||
|
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Kiszolgáló újraindítása</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function restartController() {
|
function restartController() {
|
||||||
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
|
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
|
||||||
@@ -1155,6 +1146,16 @@ function restartController() {
|
|||||||
.then(function(){ pollRestart(0); })
|
.then(function(){ pollRestart(0); })
|
||||||
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
||||||
}
|
}
|
||||||
|
function restartServer() {
|
||||||
|
if (!confirm('Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.')) return;
|
||||||
|
var btn = document.getElementById('btn-restart-server');
|
||||||
|
var status = document.getElementById('server-restart-status');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
if (status) status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… a vezérlőpult néhány másodperc múlva újratölt.</div>';
|
||||||
|
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
|
||||||
|
.then(function(){ pollRestart(0); })
|
||||||
|
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
|
||||||
|
}
|
||||||
function pollRestart(attempt) {
|
function pollRestart(attempt) {
|
||||||
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
|
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
|
||||||
document.getElementById('restart-status').innerHTML =
|
document.getElementById('restart-status').innerHTML =
|
||||||
|
|||||||
Reference in New Issue
Block a user