proxmox: review fixes, real-host validation record, changelog

Phase 10. Two review passes (go-operator-reviewer, /code-review high)
over the branch, recorded in docs/reviews/2026-09-23-proxmox-provider-review/:
9 findings fixed, 3 accepted as documented limits, 7 rejected with a
reason.

- Delete removes the seed between the stop and the destroy, so a failed
  seed delete leaves the VM for the next Delete instead of leaking the
  ISO; a Permanent refusal on the every-node path fails the call.
- Create skips a probe VMID whose listed name is someone else's without
  waiting on its lock.
- The three answers Phase 0 never captured are fixtures now: a missing
  storage is a 400 (the Checker reports StorageNotFound), a hibernated
  VM is destroyable (Delete destroys ours), a paused VM stops.
- TestProxmox_lifecycle and TestProxmoxCheck_realHost passed on pv1-1
  (PVE 9.2.20); the docs record the run and drop "not yet run".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 10:28:28 +00:00
co-authored by Claude Opus 5.5
parent 7f2073f94b
commit 3d72507894
28 changed files with 882 additions and 201 deletions
+7 -14
View File
@@ -76,14 +76,13 @@ func newAPIError(status int, reason string, body []byte) *apiError {
// under testdata/.
var (
vmConfigMissing = regexp.MustCompile(`^Configuration file '[^']*' does not exist$`)
vmNotRunning = regexp.MustCompile(`^VM [0-9]+ is not running$`)
)
const (
msgLocked = "VM is locked"
msgAlreadyExists = "config file already exists"
msgAgentNotRunning = "QEMU guest agent is not running"
msgAgentNotConfigured = "No QEMU guest agent configured"
msgNoSuchStorage = "No such storage."
)
// server500 returns the message of err when it is, or wraps, an apiError
@@ -102,10 +101,12 @@ func isNotExist(err error) bool {
return ok && vmConfigMissing.MatchString(msg)
}
// isLocked reports a VM held by a running task ("VM is locked (clone)").
func isLocked(err error) bool {
msg, ok := server500(err)
return ok && strings.HasPrefix(msg, msgLocked)
// isNoSuchStorage reports a storage the cluster does not know: a 400 whose
// storage field error is exactly msgNoSuchStorage. Other storage field
// errors, such as a clone onto a storage without image support, are not.
func isNoSuchStorage(err error) bool {
var ae *apiError
return errors.As(err, &ae) && ae.Status == http.StatusBadRequest && ae.Errors["storage"] == msgNoSuchStorage
}
// isAlreadyExists reports a clone onto a VMID that is taken.
@@ -127,14 +128,6 @@ func isAgentNotConfigured(err error) bool {
return ok && msg == msgAgentNotConfigured
}
// isVMNotRunning reports an agent call on a VM that is stopped. It is
// matched on the whole message because " is not running" alone is also a
// suffix of the agent-not-running answer.
func isVMNotRunning(err error) bool {
msg, ok := server500(err)
return ok && vmNotRunning.MatchString(msg)
}
// classify maps any error reaching the provider's boundary to one of the
// four contract sentinels, per spec entry 13. op is the contract operation:
// a vanished VM inside Create is Transient, because the contract forbids