controller v0.92.0: NAS network storage Part A2 (registry + UI + per-share health)

Controller-side of NAS network storage, proxying to agent A1 /netstorage/*. Distinct
'network' storage kind (no drive lifecycle); add/list/remove + per-share health UI;
unreachable NAS is a recoverable warning, never the drive missing/stop cascade; SMB
creds pass through to the agent, never persisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
This commit is contained in:
2026-06-30 14:40:47 +02:00
parent 9391f4ea71
commit 364dc50794
13 changed files with 837 additions and 1 deletions
+43 -1
View File
@@ -115,9 +115,22 @@ type CrossDriveBackup struct {
PreferredTarget string `json:"preferred_target,omitempty"`
}
// Storage path kinds. A DRIVE is a physical disk with the full enroll/eject/decommission/migrate/wipe
// lifecycle (agent /disks). A NETWORK share is a NAS (Part A2) proxied to the agent /netstorage — a
// DISTINCT class with NO drive lifecycle (no eject/decommission/migrate/wipe/SMART; remove is the only
// lifecycle action). An empty Kind means "drive" (back-compat with already-persisted paths).
const (
StorageKindDrive = "drive"
StorageKindNetwork = "network"
)
// NetworkMountRoot is the in-guest path under which the agent propagates NAS shares (mirrors the agent's
// /mnt/felhom-drives bind root). A registered network path is NetworkMountRoot + "/" + <share name>.
const NetworkMountRoot = "/mnt/felhom-drives"
// StoragePath represents a registered external storage location.
type StoragePath struct {
Path string `json:"path"` // e.g., "/mnt/hdd_1"
Path string `json:"path"` // e.g., "/mnt/hdd_1" (drive) or "/mnt/felhom-drives/<name>" (network)
Label string `json:"label,omitempty"` // e.g., "Külső HDD 1TB"
IsDefault bool `json:"is_default,omitempty"` // new apps use this by default
Schedulable bool `json:"schedulable"` // whether new apps can be deployed here
@@ -128,8 +141,23 @@ type StoragePath struct {
Decommissioned bool `json:"decommissioned,omitempty"` // true when drive data migrated to another
DecommissionedAt string `json:"decommissioned_at,omitempty"` // RFC3339 timestamp
MigratedTo string `json:"migrated_to,omitempty"` // path of target drive
// Kind discriminates a physical drive ("" / "drive") from a NAS network share ("network"). A network
// share is bulk-media only and carries the fields below; the drive lifecycle does NOT apply to it.
Kind string `json:"kind,omitempty"`
// Network-storage descriptors (Kind=="network" only; mirror the agent A1 add request). NO password
// is stored — the SMB credential is passed through to the agent at add-time and never persisted here.
Protocol string `json:"protocol,omitempty"` // nfs | smb
Server string `json:"server,omitempty"`
Export string `json:"export,omitempty"`
MappedUID int `json:"mapped_uid,omitempty"`
MappedGID int `json:"mapped_gid,omitempty"`
}
// IsNetwork reports whether this is a NAS network-storage path (vs a physical drive). The drive
// lifecycle (eject/decommission/migrate/wipe/SMART) must NEVER be applied to a network path.
func (p StoragePath) IsNetwork() bool { return p.Kind == StorageKindNetwork }
// NotificationPrefs holds customer notification preferences.
type NotificationPrefs struct {
Email string `json:"email,omitempty"`
@@ -846,6 +874,20 @@ func (s *Settings) IsStoragePathKnown(path string) bool {
return false
}
// IsNetworkStoragePath reports whether `path` belongs to a registered NAS network-storage path
// (Kind=="network"). The drive lifecycle (eject/decommission/migrate/wipe) must refuse such a path —
// a NAS has no device lifecycle. Matches the exact path or a child under it.
func (s *Settings) IsNetworkStoragePath(path string) bool {
s.mu.RLock()
defer s.mu.RUnlock()
for _, sp := range s.StoragePaths {
if path == sp.Path || strings.HasPrefix(path, sp.Path+"/") {
return sp.IsNetwork()
}
}
return false
}
// IsStoragePathSchedulable returns whether a path belongs to a registered,
// schedulable (active) storage path. Returns false if the path is unknown,
// disconnected, decommissioned, or inactive.