Phase 0 : configuration YAML et moteur (lecture, plan, apply sous verrou SDN, vérification)
This commit is contained in:
1 parent
471df91646
commit
f52276eccd
3 files changed
+637
No files matched your search
@@ -0,0 +1,165 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"pve-confsync/internal/adapter"
|
||||
"pve-confsync/internal/model"
|
||||
"pve-confsync/internal/pve"
|
||||
)
|
||||
|
||||
// Step is the outcome of one API action, for the execution journal.
|
||||
type Step struct {
|
||||
At time.Time `json:"at"`
|
||||
Action string `json:"action"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Result of an apply on one client.
|
||||
type Result struct {
|
||||
Client string `json:"client"`
|
||||
Steps []Step `json:"steps"`
|
||||
Err error `json:"-"`
|
||||
}
|
||||
|
||||
func (r *Result) log(action string, err error) {
|
||||
s := Step{At: time.Now().UTC(), Action: action}
|
||||
if err != nil {
|
||||
s.Error = err.Error()
|
||||
}
|
||||
r.Steps = append(r.Steps, s)
|
||||
}
|
||||
|
||||
// TaskPoll is the poll interval of the SDN reload task.
|
||||
var TaskPoll = 2 * time.Second
|
||||
|
||||
// Apply executes a plan. Non SDN changes are applied directly; SDN changes are
|
||||
// written under the global SDN lock (pve-network >= 1.1.3, PVE 9.0), then
|
||||
// committed with PUT /cluster/sdn. Any failure before the commit rolls the
|
||||
// pending SDN changes back (POST /cluster/sdn/rollback).
|
||||
func Apply(ctx context.Context, c *pve.Client, plan Plan) Result {
|
||||
res := Result{Client: plan.Client}
|
||||
if len(plan.Errors) > 0 {
|
||||
res.Err = fmt.Errorf("plan has %d error(s), nothing applied", len(plan.Errors))
|
||||
return res
|
||||
}
|
||||
if _, err := c.CheckVersion(ctx); err != nil {
|
||||
res.Err = err
|
||||
res.log("version check", err)
|
||||
return res
|
||||
}
|
||||
var sdn []Change
|
||||
for _, ch := range plan.Changes {
|
||||
a, _ := adapter.ByKind(ch.Kind)
|
||||
if a.SDN() {
|
||||
sdn = append(sdn, ch)
|
||||
continue
|
||||
}
|
||||
err := run(ctx, c, a, ch, "")
|
||||
res.log(describe(ch), err)
|
||||
if err != nil {
|
||||
res.Err = err
|
||||
return res
|
||||
}
|
||||
}
|
||||
if len(sdn) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
var token string
|
||||
err := c.Post(ctx, "cluster/sdn/lock", nil, &token)
|
||||
res.log("sdn lock", err)
|
||||
if err != nil {
|
||||
res.Err = fmt.Errorf("SDN lock (pending changes on the client?): %w", err)
|
||||
return res
|
||||
}
|
||||
rollback := func(cause error) {
|
||||
f := url.Values{"lock-token": {token}, "release-lock": {"1"}}
|
||||
rerr := c.Post(ctx, "cluster/sdn/rollback", f, nil)
|
||||
res.log("sdn rollback", rerr)
|
||||
res.Err = cause
|
||||
if rerr != nil {
|
||||
res.Err = errors.Join(cause, fmt.Errorf("rollback failed, lock still held: %w", rerr))
|
||||
}
|
||||
}
|
||||
for _, ch := range sdn {
|
||||
a, _ := adapter.ByKind(ch.Kind)
|
||||
err := run(ctx, c, a, ch, token)
|
||||
res.log(describe(ch), err)
|
||||
if err != nil {
|
||||
rollback(err)
|
||||
return res
|
||||
}
|
||||
}
|
||||
var upid string
|
||||
err = c.Put(ctx, "cluster/sdn", url.Values{"lock-token": {token}, "release-lock": {"1"}}, &upid)
|
||||
res.log("sdn apply", err)
|
||||
if err != nil {
|
||||
rollback(err)
|
||||
return res
|
||||
}
|
||||
// The commit is done: from here a failure is in the network reload of the
|
||||
// nodes, the running configuration is already the new one.
|
||||
err = c.WaitTask(ctx, upid, TaskPoll)
|
||||
res.log("sdn reload task "+upid, err)
|
||||
if err != nil {
|
||||
res.Err = fmt.Errorf("SDN configuration committed but reload failed: %w", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func run(ctx context.Context, c *pve.Client, a adapter.Adapter, ch Change, lock string) error {
|
||||
switch ch.Op {
|
||||
case OpCreate:
|
||||
return a.Create(ctx, c, model.Object{Kind: ch.Kind, ID: ch.ID, Fields: ch.Set}, lock)
|
||||
case OpUpdate:
|
||||
return a.Update(ctx, c, ch.Target, ch.Set, ch.Unset, lock)
|
||||
case OpDelete:
|
||||
return a.Delete(ctx, c, ch.Target, lock)
|
||||
}
|
||||
return fmt.Errorf("unknown op %q", ch.Op)
|
||||
}
|
||||
|
||||
func describe(ch Change) string {
|
||||
return fmt.Sprintf("%s %s/%s", ch.Op, ch.Kind, ch.ID)
|
||||
}
|
||||
|
||||
// Verify re-reads the client and checks that nothing is left to do and that
|
||||
// no node reports an SDN zone in error (GET /nodes/{node}/sdn/zones).
|
||||
func Verify(ctx context.Context, c *pve.Client, plan func(actual []model.Object) Plan, kinds []model.Kind) error {
|
||||
snap, err := Read(ctx, c, kinds, adapter.ReadCurrent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var errs []error
|
||||
if p := plan(snap.Objects); !p.Empty() {
|
||||
for _, ch := range p.Changes {
|
||||
errs = append(errs, fmt.Errorf("still different after apply: %s", describe(ch)))
|
||||
}
|
||||
}
|
||||
var status []map[string]any
|
||||
if err := c.Get(ctx, "cluster/status", nil, &status); err != nil {
|
||||
return errors.Join(append(errs, err)...)
|
||||
}
|
||||
for _, s := range status {
|
||||
if pve.Str(s["type"]) != "node" || pve.Str(s["online"]) != "1" {
|
||||
continue
|
||||
}
|
||||
node := pve.Str(s["name"])
|
||||
var zones []map[string]any
|
||||
if err := c.Get(ctx, "nodes/"+url.PathEscape(node)+"/sdn/zones", nil, &zones); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
for _, z := range zones {
|
||||
if pve.Str(z["status"]) == "error" {
|
||||
errs = append(errs, fmt.Errorf("node %s: SDN zone %s in error", node, pve.Str(z["zone"])))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
Reference in new issue
Block a user