netstorage: verify-before-commit orchestration — agentapi verify fields, uid-1000 re-exec probe, detached add job, orphan rows
Controller half of the verify pipeline (SPIKE-nas-verify b57f6c1): AddNetStorage gains verify/job fields + typed NetAddRefusedError; NetVerifyStatus polls the agent slot; --netprobe hidden re-exec mode (SysProcAttr.Credential uid/gid 1000, no shell) proves in-guest writability; the add handler starts a detached single-flight job (agent_add → verifying → probing → registering LAST) with full rollback on any failure incl. verify-lost-after-restart (Scenario F); §3.2 Hungarian error map server-side; live-but-unregistered shares surface as remove- only 'Árva megosztás' rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -652,7 +652,9 @@ type AddNetStorageRequest struct {
|
||||
Password string `json:"password,omitempty"` // SMB secret — pass-through, never persisted
|
||||
}
|
||||
|
||||
// NetStorageAddResult mirrors the agent's add response (the in-guest path the media app's data dir points at).
|
||||
// NetStorageAddResult mirrors the agent's add response (the in-guest path the media app's data dir
|
||||
// points at). Since agent v0.81.0 (verify-before-commit) a successful add means "units installed,
|
||||
// verify STARTED" — Verify/JobID carry the detached verify job the caller polls via NetVerifyStatus.
|
||||
type NetStorageAddResult struct {
|
||||
Name string `json:"name"`
|
||||
Protocol string `json:"protocol"`
|
||||
@@ -660,18 +662,67 @@ type NetStorageAddResult struct {
|
||||
GuestPath string `json:"guest_path"`
|
||||
HostUID int `json:"host_uid"`
|
||||
HostGID int `json:"host_gid"`
|
||||
Verify string `json:"verify"` // "started" on a verify-before-commit agent (v0.81.0+)
|
||||
JobID string `json:"job_id"`
|
||||
Code string `json:"code"` // set on a categorized SYNC refusal (e.g. "unreachable", "busy")
|
||||
}
|
||||
|
||||
// AddNetStorage mounts a NAS share host-side (the agent automounts it; it propagates into this guest via
|
||||
// the shared bind). Returns the in-guest path the media app's data dir is pointed at.
|
||||
// NetAddRefusedError is the agent's CATEGORIZED sync refusal of a netstorage add (the 2 s TCP
|
||||
// pre-probe "unreachable", or "busy" single-flight). Code is the verify-category vocabulary the UI
|
||||
// maps to Hungarian; nothing was installed agent-side.
|
||||
type NetAddRefusedError struct {
|
||||
Code string
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e *NetAddRefusedError) Error() string {
|
||||
return "agentapi: netstorage add refused (" + e.Code + "): " + e.Msg
|
||||
}
|
||||
|
||||
// NetVerifyStatus mirrors the agent's GET /netstorage/verify-status: the single verify slot.
|
||||
// Phase "none" is a REAL signal — after an agent restart the in-memory job is gone; the caller
|
||||
// treats none-after-install as verify-lost and rolls the add back (Scenario F).
|
||||
type NetVerifyStatus struct {
|
||||
Phase string `json:"phase"` // none | running | done | failed
|
||||
Name string `json:"name"`
|
||||
Where string `json:"where"`
|
||||
Protocol string `json:"protocol"`
|
||||
Code string `json:"code"` // failure category (the agent's classifier vocabulary)
|
||||
Detail string `json:"detail"` // operator hint + raw journal fragment
|
||||
JobID string `json:"job_id"`
|
||||
}
|
||||
|
||||
// AddNetStorage installs a NAS share host-side and starts the agent's detached verify job (agent
|
||||
// v0.81.0 verify-before-commit). A categorized sync refusal returns *NetAddRefusedError carrying
|
||||
// the category code (the result body also carries it); other failures are plain errors.
|
||||
func (c *Client) AddNetStorage(ctx context.Context, req AddNetStorageRequest) (NetStorageAddResult, error) {
|
||||
var out NetStorageAddResult
|
||||
body, err := c.post(ctx, "/netstorage/add", req)
|
||||
env, status, err := c.postWithStatus(ctx, "/netstorage/add", req)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if len(env.Data) > 0 {
|
||||
_ = json.Unmarshal(env.Data, &out) // best-effort; the refusal body carries {code}
|
||||
}
|
||||
if rerr := refusalError("/netstorage/add", status, env); rerr != nil {
|
||||
if out.Code != "" {
|
||||
return out, &NetAddRefusedError{Code: out.Code, Msg: truncateErr(env.Error, 300)}
|
||||
}
|
||||
return out, rerr
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// NetVerifyStatus polls the agent's verify slot (short GET — fits the client's global 15 s timeout;
|
||||
// the LONG wait lives in the caller's poll loop, never in one HTTP call).
|
||||
func (c *Client) NetVerifyStatus(ctx context.Context) (NetVerifyStatus, error) {
|
||||
var out NetVerifyStatus
|
||||
body, err := c.get(ctx, "/netstorage/verify-status")
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /netstorage/add: %w", err)
|
||||
return out, fmt.Errorf("agentapi: decode /netstorage/verify-status: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user