v0.80.0: PBS DR tier slice 2 — the apply-bridge (pbs_dr consumer, felhom-pbs-apply set-only wrapper, verify-pin-before-consume, adoption-first, loud consumed-failed, escrow seed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-10 21:50:36 +02:00
parent a6e8bcb475
commit e5e8f3920a
15 changed files with 1309 additions and 2 deletions
+45
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/url"
"strings"
)
// Read-only query operations. All API-backed (Datastore.Audit / VM.Audit /
@@ -108,6 +109,50 @@ func (c *Client) NodeStorage(ctx context.Context) ([]Storage, error) {
return ss, c.get(ctx, "/nodes/"+c.node+"/storage", &ss)
}
// StorageEntryConfig is the storage-entry CONFIG as served by GET /storage/{id} (PBS DR slice 2
// adoption probe — the pbs-type fields the bridge compares against the descriptor). All fields
// non-secret; the token secret lives in /etc/pve/priv/storage/<id>.pw, never in this response.
type StorageEntryConfig struct {
Storage string `json:"storage"`
Type string `json:"type"`
Server string `json:"server,omitempty"`
Datastore string `json:"datastore,omitempty"`
Namespace string `json:"namespace,omitempty"`
Username string `json:"username,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"`
EncryptionKey string `json:"encryption-key,omitempty"` // K's OWN fingerprint (not the key)
}
// StorageEntry returns GET /storage/{id} — the entry's configuration, or found=false when the id
// does not exist (PVE answers HTTP 500 "does not exist" — mapped here so the adoption probe can
// branch without string-matching upstream).
func (c *Client) StorageEntry(ctx context.Context, id string) (*StorageEntryConfig, bool, error) {
var e StorageEntryConfig
err := c.get(ctx, "/storage/"+url.PathEscape(id), &e)
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
return nil, false, nil
}
return nil, false, err
}
return &e, true, nil
}
// StorageActive returns whether GET /nodes/{node}/storage/{id}/status reports the entry active —
// for a pbs entry that means PVE connected to the PBS with the stored credentials (the
// post-apply/adoption health probe).
func (c *Client) StorageActive(ctx context.Context, id string) (bool, error) {
var st struct {
Active int `json:"active"`
Enabled int `json:"enabled"`
}
path := fmt.Sprintf("/nodes/%s/storage/%s/status", c.node, url.PathEscape(id))
if err := c.get(ctx, path, &st); err != nil {
return false, err
}
return st.Active == 1, nil
}
// StorageContent returns GET /nodes/{node}/storage/{store}/content (e.g. vzdump
// archives + CT templates available for a restore).
func (c *Client) StorageContent(ctx context.Context, store string) ([]StorageContent, error) {