Phase 2 of the proxmox provider plan. Adds the proxmox: config block with its validation, registers the type in the config allow-list and the constructor map, and starts internal/provider/proxmox: the api seam and its data types, providerIDs (vms/<vmid>/<name>) with the UID-derived VMID, the description mark, state mapping, and error classification. Tests read the responses captured from pv1-1 in Phase 0 rather than repeating Proxmox's wording, and every matcher is run against every captured error so none fires on the wrong message. The contract methods are Transient stubs until Phases 5 to 7. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
115 lines
4.4 KiB
Go
115 lines
4.4 KiB
Go
package proxmox
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
|
)
|
|
|
|
func TestMapState_table(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
status, qmp, ip string
|
|
want provider.InstanceState
|
|
}{
|
|
{"running", "running", "192.168.4.147", provider.StateRunning},
|
|
{"running", "running", "", provider.StateProvisioning},
|
|
{"running", "", "192.168.4.147", provider.StateRunning},
|
|
{"running", "", "", provider.StateProvisioning},
|
|
{"running", "prelaunch", "", provider.StateProvisioning},
|
|
{"running", "paused", "192.168.4.147", provider.StateStopped},
|
|
{"running", "suspended", "", provider.StateStopped},
|
|
{"running", "io-error", "", provider.StateStopped},
|
|
{"running", "something-new", "", provider.StateStopped},
|
|
{"stopped", "stopped", "", provider.StateStopped},
|
|
{"stopped", "", "", provider.StateStopped},
|
|
{"unknown", "", "", provider.StateStopped},
|
|
{"", "", "", provider.StateStopped},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.status+"/"+tc.qmp+"/"+tc.ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := mapState(tc.status, tc.qmp, tc.ip); got != tc.want {
|
|
t.Errorf("mapState(%q, %q, %q) = %q, want %q", tc.status, tc.qmp, tc.ip, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestMapState_fromCaptures feeds status/current as Proxmox reported it for
|
|
// a running, a stopped and a paused VM.
|
|
func TestMapState_fromCaptures(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
capture string
|
|
ip string
|
|
want provider.InstanceState
|
|
}{
|
|
{"status_current_running", "192.168.0.150", provider.StateRunning},
|
|
{"status_current_running", "", provider.StateProvisioning},
|
|
{"status_current_stopped", "", provider.StateStopped},
|
|
{"status_current_paused", "192.168.0.150", provider.StateStopped},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.capture+"/"+tc.ip, func(t *testing.T) {
|
|
t.Parallel()
|
|
s := decodeData[vmStatus](t, tc.capture)
|
|
if got := mapState(s.Status, s.QMPStatus, tc.ip); got != tc.want {
|
|
t.Errorf("mapState(%q, %q, %q) = %q, want %q", s.Status, s.QMPStatus, tc.ip, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFirstIPv4_filters(t *testing.T) {
|
|
t.Parallel()
|
|
ip := func(addr, typ string) agentIP { return agentIP{Address: addr, Type: typ} }
|
|
tests := []struct {
|
|
name string
|
|
ifaces []agentInterface
|
|
want string
|
|
}{
|
|
{name: "none", want: ""},
|
|
{name: "loopback interface only", ifaces: []agentInterface{{Name: "lo", IPAddresses: []agentIP{ip("127.0.0.1", "ipv4")}}}, want: ""},
|
|
{name: "ipv6 only", ifaces: []agentInterface{{Name: "ens18", IPAddresses: []agentIP{ip("fe80::1", "ipv6"), ip("2001:db8::5", "ipv6")}}}, want: ""},
|
|
{name: "link-local skipped", ifaces: []agentInterface{{Name: "ens18", IPAddresses: []agentIP{ip("169.254.3.4", "ipv4"), ip("192.168.4.20", "ipv4")}}}, want: "192.168.4.20"},
|
|
{name: "unspecified skipped", ifaces: []agentInterface{{Name: "ens18", IPAddresses: []agentIP{ip("0.0.0.0", "ipv4")}}}, want: ""},
|
|
{name: "loopback address on another interface skipped", ifaces: []agentInterface{{Name: "dummy0", IPAddresses: []agentIP{ip("127.0.0.2", "ipv4")}}}, want: ""},
|
|
{name: "type and literal disagree", ifaces: []agentInterface{{Name: "ens18", IPAddresses: []agentIP{ip("fe80::1", "ipv4"), ip("10.0.0.9", "ipv6")}}}, want: ""},
|
|
{name: "unparsable skipped", ifaces: []agentInterface{{Name: "ens18", IPAddresses: []agentIP{ip("not-an-ip", "ipv4"), ip("10.0.0.9", "ipv4")}}}, want: "10.0.0.9"},
|
|
{name: "agent order decides", ifaces: []agentInterface{
|
|
{Name: "ens18", IPAddresses: []agentIP{ip("192.168.4.20", "ipv4")}},
|
|
{Name: "ens19", IPAddresses: []agentIP{ip("10.0.0.9", "ipv4")}},
|
|
}, want: "192.168.4.20"},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := firstIPv4(tc.ifaces); got != tc.want {
|
|
t.Errorf("firstIPv4() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFirstIPv4_fromCaptures reads the guest agent's answer from the two
|
|
// Phase 0 runs, untagged and on VLAN 4.
|
|
func TestFirstIPv4_fromCaptures(t *testing.T) {
|
|
t.Parallel()
|
|
tests := map[string]string{
|
|
"agent_network_get_interfaces": "192.168.0.150",
|
|
"agent_network_get_interfaces_vlan": "192.168.4.147",
|
|
}
|
|
for capture, want := range tests {
|
|
t.Run(capture, func(t *testing.T) {
|
|
t.Parallel()
|
|
res := decodeData[struct {
|
|
Result []agentInterface `json:"result"`
|
|
}](t, capture)
|
|
if got := firstIPv4(res.Result); got != want {
|
|
t.Errorf("firstIPv4(%s) = %q, want %q", capture, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|