fix: pct_create make vmid optional, fix dhcp+gw, longer boot settle
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Follow-ups found while verifying the approve→provision path end to end:
- vmid is now optional: the early required-field check rejected vmid:0
  before the cluster VMID guard could auto-assign a free id. Only hostname
  is required now; 0 (or a collision) resolves to `pvesh get /cluster/nextid`.
- net0: use ip=dhcp with no gateway when no static IP is given (Proxmox
  rejects gw alongside dhcp); only attach gw for a static CIDR.
- bump post-create settle to 10s so a DHCP lease is up before apt runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 00:50:54 +02:00
parent 8ed2b88495
commit ac86302f52

View File

@@ -230,10 +230,12 @@ func executeApprovedAction(ctx context.Context, pool *db.Pool, execID uuid.UUID,
emitExecutionEvent(ctx, pool, execID, "failed", map[string]any{"target": targetSlug, "error": err.Error()})
return
}
if cfg.VMID == 0 || cfg.Hostname == "" {
// Only hostname is required. vmid is optional — when 0 (or later found
// to collide) the VMID guard below assigns a free cluster id.
if cfg.Hostname == "" {
pool.Exec(ctx, `UPDATE executions SET status='failed', result=$2::jsonb WHERE entity_id=$1`,
execID, `{"error":"pct_create: vmid and hostname are required"}`)
emitExecutionEvent(ctx, pool, execID, "failed", map[string]any{"target": targetSlug, "error": "missing vmid or hostname"})
execID, `{"error":"pct_create: hostname is required"}`)
emitExecutionEvent(ctx, pool, execID, "failed", map[string]any{"target": targetSlug, "error": "missing hostname"})
return
}
if cfg.Cores == 0 {
@@ -329,11 +331,23 @@ func executeApprovedAction(ctx context.Context, pool *db.Pool, execID uuid.UUID,
nestingFlag = fmt.Sprintf(" --features %s", strings.Join(features, ","))
}
// net0: DHCP when no static IP is given (or ip=="dhcp"). Proxmox
// rejects a gateway alongside ip=dhcp, so only add gw for a static IP.
net0 := "name=eth0,bridge=vmbr0,"
if cfg.IP == "" || strings.EqualFold(cfg.IP, "dhcp") {
net0 += "ip=dhcp"
} else {
net0 += "ip=" + cfg.IP
if cfg.GW != "" {
net0 += ",gw=" + cfg.GW
}
}
templatePath := fmt.Sprintf("/var/lib/vz/template/cache/%s", cfg.Template)
createCmd := fmt.Sprintf(
"pct create %d %s --hostname %s --cores %d --memory %d --rootfs %s:%d %s --net0 name=eth0,bridge=vmbr0,ip=%s,gw=%s%s --start 1",
"pct create %d %s --hostname %s --cores %d --memory %d --rootfs %s:%d %s --net0 %s%s --start 1",
cfg.VMID, templatePath, cfg.Hostname, cfg.Cores, cfg.Memory,
cfg.Storage, cfg.DiskGB, privFlag, cfg.IP, cfg.GW, nestingFlag)
cfg.Storage, cfg.DiskGB, privFlag, net0, nestingFlag)
if cfg.Nameserver != "" {
createCmd += fmt.Sprintf(" --nameserver %s", cfg.Nameserver)
@@ -358,8 +372,9 @@ func executeApprovedAction(ctx context.Context, pool *db.Pool, execID uuid.UUID,
// with a boot settle; failures are appended to output and mark the
// execution failed so the operator sees exactly which step broke.
if err == nil && (len(cfg.Services) > 0 || cfg.PostInstall != "") {
// Give the container a moment to finish booting before exec.
steps := []string{fmt.Sprintf("sleep 5")}
// Give the container time to boot and (for DHCP) acquire a lease
// before apt needs the network.
steps := []string{"sleep 10"}
if len(cfg.Services) > 0 {
pkgs := strings.Join(sanitizePkgs(cfg.Services), " ")
steps = append(steps, fmt.Sprintf("pct exec %d -- bash -lc 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s'", cfg.VMID, pkgs))