Delete stops and destroys a VM of ours under the per-name lock, then deletes its seed on the VM's node, or on every node when no VM of ours was left. A hibernated VM of ours is Permanent, as in Create. The Checker verifies each node concurrently (version, node status, ISO storage, templates) and adds CertificateInvalid, CredentialsUnavailable, StorageNotFound and TemplateNotFound to the shared reasons. The ProviderHealth type comment gains proxmox. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
175 lines
5.6 KiB
Go
175 lines
5.6 KiB
Go
// Package proxmox implements the provider contract on a Proxmox VE cluster
|
|
// through its REST API: VMs are full clones of prepared templates, seeded
|
|
// with a NoCloud ISO uploaded to a storage and addressed through the guest
|
|
// agent. The design is in docs/proxmox-provider-spec.md.
|
|
package proxmox
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/go-logr/logr"
|
|
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider/keyedlock"
|
|
)
|
|
|
|
const (
|
|
defaultVMIDMin = 100000
|
|
defaultVMIDMax = 199999
|
|
defaultRequestTimeoutSeconds = 30
|
|
)
|
|
|
|
// budgets bounds every wait of the provider (spec entry 18); tests shrink
|
|
// them.
|
|
type budgets struct {
|
|
clone time.Duration // the clone task, and waiting for a VM's lock
|
|
upload time.Duration
|
|
resize time.Duration
|
|
start time.Duration
|
|
stop time.Duration
|
|
destroy time.Duration
|
|
seedDelete time.Duration
|
|
running time.Duration // from the start task's end to status running
|
|
poll time.Duration // task and status poll interval
|
|
}
|
|
|
|
func defaultBudgets() budgets {
|
|
return budgets{
|
|
clone: 10 * time.Minute,
|
|
upload: 2 * time.Minute,
|
|
resize: 2 * time.Minute,
|
|
start: 2 * time.Minute,
|
|
stop: 2 * time.Minute,
|
|
destroy: 5 * time.Minute,
|
|
seedDelete: time.Minute,
|
|
running: 60 * time.Second,
|
|
poll: time.Second,
|
|
}
|
|
}
|
|
|
|
// Provider is one configured proxmox entry: one cluster reached through one
|
|
// API URL with one token.
|
|
type Provider struct {
|
|
name string
|
|
cfg provider.ProxmoxConfig
|
|
api api
|
|
now func() time.Time
|
|
budgets budgets
|
|
|
|
// nameLocks serialises Create and Delete per instance name (spec entry
|
|
// 19), so the reconciler's Delete and GC's Delete never race on stop and
|
|
// destroy, and a Delete cannot interleave with a Create's steps.
|
|
nameLocks *keyedlock.Locks
|
|
}
|
|
|
|
var _ provider.Provider = (*Provider)(nil)
|
|
|
|
// New builds a Provider from pc.Proxmox. It never makes a request: the token
|
|
// file is only checked for existence, and a missing one is a warning, not an
|
|
// error, because the Secret mount is optional and may land later.
|
|
func New(ctx context.Context, pc provider.ProviderConfig) (provider.Provider, error) {
|
|
cfg := provider.ProxmoxConfig{}
|
|
if pc.Proxmox != nil {
|
|
cfg = *pc.Proxmox
|
|
}
|
|
client, err := newHTTPAPI(withDefaults(cfg))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("proxmox provider %s: %w", pc.Name, err)
|
|
}
|
|
p := newProvider(pc.Name, cfg, observe(client, pc.Name))
|
|
log := p.logger(ctx)
|
|
if _, err := os.Stat(p.cfg.TokenFile); err != nil {
|
|
log.Info("WARNING: token file is not readable; every call fails until it is", "path", p.cfg.TokenFile, "reason", err.Error())
|
|
}
|
|
if p.cfg.InsecureSkipTLSVerify {
|
|
log.Info("WARNING: insecureSkipTLSVerify is set; the API's certificate is not verified")
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// newProvider is the constructor tests use, with an in-memory api.
|
|
func newProvider(name string, cfg provider.ProxmoxConfig, a api) *Provider {
|
|
return &Provider{
|
|
name: name,
|
|
cfg: withDefaults(cfg),
|
|
api: a,
|
|
now: time.Now,
|
|
budgets: defaultBudgets(),
|
|
nameLocks: keyedlock.New(),
|
|
}
|
|
}
|
|
|
|
// withDefaults returns cfg with every optional field filled in, so the rest
|
|
// of the package never re-checks for an empty value. Slices, maps and
|
|
// pointers are copied, not shared with the caller.
|
|
func withDefaults(cfg provider.ProxmoxConfig) provider.ProxmoxConfig {
|
|
cfg.Nodes = append([]string(nil), cfg.Nodes...)
|
|
if cfg.VMIDRange == nil {
|
|
cfg.VMIDRange = &provider.ProxmoxVMIDRange{Min: defaultVMIDMin, Max: defaultVMIDMax}
|
|
} else {
|
|
r := *cfg.VMIDRange
|
|
cfg.VMIDRange = &r
|
|
}
|
|
if cfg.RequestTimeoutSeconds == 0 {
|
|
cfg.RequestTimeoutSeconds = defaultRequestTimeoutSeconds
|
|
}
|
|
install := true
|
|
if cfg.InstallGuestAgent != nil {
|
|
install = *cfg.InstallGuestAgent
|
|
}
|
|
cfg.InstallGuestAgent = &install
|
|
templates := make(map[string]provider.ProxmoxTemplate, len(cfg.Templates))
|
|
for k, v := range cfg.Templates {
|
|
templates[k] = v
|
|
}
|
|
cfg.Templates = templates
|
|
machineTypes := make(map[string]provider.ProxmoxMachineType, len(cfg.MachineTypes))
|
|
for k, v := range cfg.MachineTypes {
|
|
machineTypes[k] = v
|
|
}
|
|
cfg.MachineTypes = machineTypes
|
|
return cfg
|
|
}
|
|
|
|
func (p *Provider) logger(ctx context.Context) logr.Logger {
|
|
return logf.FromContext(ctx).WithName("proxmox").WithValues("provider", p.name)
|
|
}
|
|
|
|
// node resolves placement.zone to a configured node. An empty zone is
|
|
// allowed only when exactly one node is configured.
|
|
func (p *Provider) node(zone string) (string, error) {
|
|
if zone == "" {
|
|
if len(p.cfg.Nodes) == 1 {
|
|
return p.cfg.Nodes[0], nil
|
|
}
|
|
return "", permanentf("placement.zone is required when proxmox.nodes lists more than one node")
|
|
}
|
|
for _, n := range p.cfg.Nodes {
|
|
if n == zone {
|
|
return n, nil
|
|
}
|
|
}
|
|
return "", permanentf("placement.zone %q is not in proxmox.nodes", zone)
|
|
}
|
|
|
|
// wrapErr is the single exit for errors: it classifies err and wraps it with
|
|
// the configured provider name, op and id.
|
|
func (p *Provider) wrapErr(op, id string, err error) error {
|
|
return provider.Wrap(classify(op, err), op, p.name, id, err)
|
|
}
|
|
|
|
// lockName takes the per-name lock of spec entry 19, or gives up with ctx,
|
|
// so a caller whose ctx ends while another caller's Create waits on a clone
|
|
// returns promptly.
|
|
func (p *Provider) lockName(ctx context.Context, name string) (func(), error) {
|
|
release, err := p.nameLocks.Acquire(ctx, name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: waiting for the lock on %s: %w", provider.ErrTransient, name, err)
|
|
}
|
|
return release, nil
|
|
}
|