slice 8C Phase B.1: agentapi disk client (Disks/AssignDisk/EjectDisk/FormatDisk)
ErrFormatRefused surfaces the agent's data-bearing refusal distinctly. Tests: list, blank format OK, data-bearing refused, eject dependents. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -174,6 +174,99 @@ func (c *Client) BackupStatus(ctx context.Context) (StatusResponse, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ---- slice 8C: disk management (execution is the agent's) --------------------------------
|
||||
|
||||
// DiskInfo mirrors the agent's GET /disks entry.
|
||||
type DiskInfo struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
State string `json:"state"`
|
||||
BackingDevice string `json:"backing_device"`
|
||||
MountPath string `json:"mount_path"`
|
||||
Class string `json:"class"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
DataReason string `json:"data_reason"`
|
||||
}
|
||||
|
||||
// DisksResponse mirrors GET /disks.
|
||||
type DisksResponse struct {
|
||||
VMID int `json:"vmid"`
|
||||
Disks []DiskInfo `json:"disks"`
|
||||
}
|
||||
|
||||
// FormatResult mirrors POST /disks/format (the success/refusal payload).
|
||||
type FormatResult struct {
|
||||
VMID int `json:"vmid"`
|
||||
Device string `json:"device"`
|
||||
Formatted bool `json:"formatted"`
|
||||
DataBearing bool `json:"data_bearing"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// ErrFormatRefused is returned by FormatDisk when the agent refuses a data-bearing format
|
||||
// (pending operator authorization — the 8C invariant). The UI surfaces this distinctly.
|
||||
var ErrFormatRefused = fmt.Errorf("agentapi: format refused — device is data-bearing (operator authorization required)")
|
||||
|
||||
// Disks lists the host drives the agent manages, with a data-bearing flag per drive.
|
||||
func (c *Client) Disks(ctx context.Context) (DisksResponse, error) {
|
||||
var out DisksResponse
|
||||
body, err := c.get(ctx, "/disks")
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /disks: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AssignDisk attaches a drive (by fs-UUID) as a host mount (benign, self-serve).
|
||||
func (c *Client) AssignDisk(ctx context.Context, uuid, where, fstype, options string) error {
|
||||
_, err := c.post(ctx, "/disks/assign", map[string]string{
|
||||
"uuid": uuid, "where": where, "fstype": fstype, "options": options,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// EjectResult mirrors POST /disks/eject (the dependent-guest warning).
|
||||
type EjectResult struct {
|
||||
VMID int `json:"vmid"`
|
||||
Ejected string `json:"ejected"`
|
||||
DependentGuests []int `json:"dependent_guests"`
|
||||
}
|
||||
|
||||
// EjectDisk safe-unmounts a host mount (data preserved) and returns the dependent guests.
|
||||
func (c *Client) EjectDisk(ctx context.Context, where string) (EjectResult, error) {
|
||||
var out EjectResult
|
||||
body, err := c.post(ctx, "/disks/eject", map[string]string{"where": where})
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /disks/eject: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// FormatDisk asks the agent to format a device. The AGENT inspects the device and decides
|
||||
// data-bearing-ness — a data-bearing device is refused (ErrFormatRefused), the controller's claim
|
||||
// is irrelevant. Only a device the agent reads as blank is formatted.
|
||||
func (c *Client) FormatDisk(ctx context.Context, device, fstype string) (FormatResult, error) {
|
||||
var out FormatResult
|
||||
body, err := c.post(ctx, "/disks/format", map[string]string{"device": device, "fstype": fstype})
|
||||
if err != nil {
|
||||
// A data-bearing refusal comes back as HTTP 403 (the post helper turns it into an error).
|
||||
if strings.Contains(err.Error(), "HTTP 403") {
|
||||
return FormatResult{Device: device, Formatted: false, DataBearing: true}, ErrFormatRefused
|
||||
}
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /disks/format: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// get issues an authenticated GET and unwraps the {ok,data,error} envelope.
|
||||
func (c *Client) get(ctx context.Context, path string) (json.RawMessage, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
|
||||
|
||||
Reference in New Issue
Block a user