From 2e330cd96a4eb6216df2a097a8d9bfe51d55adcb Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:11:36 +0200 Subject: [PATCH 1/6] feat: allow checking for extra errors --- proxmox/client.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/proxmox/client.go b/proxmox/client.go index cda34016..cf0b5c8e 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -154,16 +154,22 @@ func (c *Client) GetVersion() (version Version, err error) { return } -func (c *Client) GetJsonRetryable(url string, data *map[string]interface{}, tries int) error { +func (c *Client) GetJsonRetryable(url string, data *map[string]interface{}, tries int, errorString ...string) error { var statErr error for ii := 0; ii < tries; ii++ { _, statErr = c.session.GetJSON(url, nil, nil, data) if statErr == nil { return nil } + // TODO can probable check for `500` status code instead of providing a list of error strings to check for if strings.Contains(statErr.Error(), "500 no such resource") { return statErr } + for _, e := range errorString { + if strings.Contains(statErr.Error(), e) { + return statErr + } + } // fmt.Printf("[DEBUG][GetJsonRetryable] Sleeping for %d seconds before asking url %s", ii+1, url) time.Sleep(time.Duration(ii+1) * time.Second) } @@ -2075,8 +2081,8 @@ func (c *Client) UpdateSDNZone(id string, params map[string]interface{}) error { } // Shared -func (c *Client) GetItemConfigMapStringInterface(url, text, message string) (map[string]interface{}, error) { - data, err := c.GetItemConfig(url, text, message) +func (c *Client) GetItemConfigMapStringInterface(url, text, message string, errorString ...string) (map[string]interface{}, error) { + data, err := c.GetItemConfig(url, text, message, errorString...) if err != nil { return nil, err } @@ -2099,8 +2105,8 @@ func (c *Client) GetItemConfigInterfaceArray(url, text, message string) ([]inter return data["data"].([]interface{}), err } -func (c *Client) GetItemConfig(url, text, message string) (config map[string]interface{}, err error) { - err = c.GetJsonRetryable(url, &config, 3) +func (c *Client) GetItemConfig(url, text, message string, errorString ...string) (config map[string]interface{}, err error) { + err = c.GetJsonRetryable(url, &config, 3, errorString...) if err != nil { return nil, err } From 69dcb19bd824f54cd36d198d72fda82648c9f3a6 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Mon, 22 Apr 2024 23:13:28 +0200 Subject: [PATCH 2/6] refactor: move type to different file --- proxmox/config_qemu.go | 7 ------- proxmox/data_qemu_agent.go | 12 ++++++++++++ 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 proxmox/data_qemu_agent.go diff --git a/proxmox/config_qemu.go b/proxmox/config_qemu.go index a12cfc44..84b0ef7a 100644 --- a/proxmox/config_qemu.go +++ b/proxmox/config_qemu.go @@ -29,13 +29,6 @@ type ( IpconfigMap map[int]interface{} ) -type AgentNetworkInterface struct { - MACAddress string - IPAddresses []net.IP - Name string - Statistics map[string]int64 -} - // ConfigQemu - Proxmox API QEMU options type ConfigQemu struct { Agent *QemuGuestAgent `json:"agent,omitempty"` diff --git a/proxmox/data_qemu_agent.go b/proxmox/data_qemu_agent.go new file mode 100644 index 00000000..3c2b7ba0 --- /dev/null +++ b/proxmox/data_qemu_agent.go @@ -0,0 +1,12 @@ +package proxmox + +import ( + "net" +) + +type AgentNetworkInterface struct { + MACAddress string + IPAddresses []net.IP + Name string + Statistics map[string]int64 +} From c964b1febede66cd21b7560993b8f588ede2a51b Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:26:16 +0200 Subject: [PATCH 3/6] refactor: get `AgentNetworkInterface` --- proxmox/client.go | 34 +---------------- proxmox/data_qemu_agent.go | 77 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 36 deletions(-) diff --git a/proxmox/client.go b/proxmox/client.go index cf0b5c8e..a71070e5 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "mime/multipart" - "net" "net/http" "os" "regexp" @@ -369,39 +368,8 @@ func (c *Client) GetVmSpiceProxy(vmr *VmRef) (vmSpiceProxy map[string]interface{ return } -func (a *AgentNetworkInterface) UnmarshalJSON(b []byte) error { - var intermediate struct { - HardwareAddress string `json:"hardware-address"` - IPAddresses []struct { - IPAddress string `json:"ip-address"` - IPAddressType string `json:"ip-address-type"` - Prefix int `json:"prefix"` - } `json:"ip-addresses"` - Name string `json:"name"` - Statistics map[string]int64 `json:"statistics"` - } - err := json.Unmarshal(b, &intermediate) - if err != nil { - return err - } - - a.IPAddresses = make([]net.IP, len(intermediate.IPAddresses)) - for idx, ip := range intermediate.IPAddresses { - a.IPAddresses[idx] = net.ParseIP((strings.Split(ip.IPAddress, "%"))[0]) - if a.IPAddresses[idx] == nil { - return fmt.Errorf("could not parse %s as IP", ip.IPAddress) - } - } - a.MACAddress = intermediate.HardwareAddress - a.Name = intermediate.Name - a.Statistics = intermediate.Statistics - return nil -} - func (c *Client) GetVmAgentNetworkInterfaces(vmr *VmRef) ([]AgentNetworkInterface, error) { - var ifs []AgentNetworkInterface - err := c.doAgentGet(vmr, "network-get-interfaces", &ifs) - return ifs, err + return vmr.GetAgentInformation(c, true) } func (c *Client) doAgentGet(vmr *VmRef, command string, output interface{}) error { diff --git a/proxmox/data_qemu_agent.go b/proxmox/data_qemu_agent.go index 3c2b7ba0..dc2541d0 100644 --- a/proxmox/data_qemu_agent.go +++ b/proxmox/data_qemu_agent.go @@ -2,11 +2,82 @@ package proxmox import ( "net" + "strconv" ) +func (vmr *VmRef) GetAgentInformation(c *Client, statistics bool) ([]AgentNetworkInterface, error) { + if err := c.CheckVmRef(vmr); err != nil { + return nil, err + } + vmid := strconv.FormatInt(int64(vmr.vmId), 10) + params, err := c.GetItemConfigMapStringInterface( + "/nodes/"+vmr.node+"/qemu/"+vmid+"/agent/network-get-interfaces", "guest agent", "data", + "500 QEMU guest agent is not running", + "500 VM "+vmid+" is not running") + if err != nil { + return nil, err + } + return AgentNetworkInterface{}.mapToSDK(params, statistics), nil +} + type AgentNetworkInterface struct { - MACAddress string - IPAddresses []net.IP + MacAddress net.HardwareAddr + IpAddresses []net.IP Name string - Statistics map[string]int64 + Statistics *AgentInterfaceStatistics +} + +func (AgentNetworkInterface) mapToSDK(params map[string]interface{}, statistics bool) []AgentNetworkInterface { + var interfaces []interface{} + if v, isSet := params["result"]; isSet { + interfaces = v.([]interface{}) + } + if len(interfaces) == 0 { + return nil + } + agentInterfaces := make([]AgentNetworkInterface, len(interfaces)) + for i, e := range interfaces { + iFace := e.(map[string]interface{}) + agentInterfaces[i] = AgentNetworkInterface{} + if v, isSet := iFace["hardware-address"]; isSet { + agentInterfaces[i].MacAddress, _ = net.ParseMAC(v.(string)) + } + if v, isSet := iFace["ip-addresses"]; isSet { + ips := v.([]interface{}) + agentInterfaces[i].IpAddresses = make([]net.IP, len(ips)) + for ii, ee := range ips { + ip := ee.(map[string]interface{}) + agentInterfaces[i].IpAddresses[ii], _, _ = net.ParseCIDR(ip["ip-address"].(string) + "/" + strconv.FormatInt(int64(ip["prefix"].(float64)), 10)) + } + } + if v, isSet := iFace["name"]; isSet { + agentInterfaces[i].Name = v.(string) + } + if statistics { + if v, isSet := iFace["statistics"]; isSet { + stats := v.(map[string]interface{}) + agentInterfaces[i].Statistics = &AgentInterfaceStatistics{ + RxBytes: uint(stats["rx-bytes"].(float64)), + RxDropped: uint(stats["rx-dropped"].(float64)), + RxErrors: uint(stats["rx-errs"].(float64)), + RxPackets: uint(stats["rx-packets"].(float64)), + TxBytes: uint(stats["tx-bytes"].(float64)), + TxDropped: uint(stats["tx-dropped"].(float64)), + TxErrors: uint(stats["tx-errs"].(float64)), + TxPackets: uint(stats["tx-packets"].(float64))} + } + } + } + return agentInterfaces +} + +type AgentInterfaceStatistics struct { + RxBytes uint + RxDropped uint + RxErrors uint + RxPackets uint + TxBytes uint + TxDropped uint + TxErrors uint + TxPackets uint } From dd561f41d8f6c58e695bef43f3c4daf663a16909 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:44:51 +0200 Subject: [PATCH 4/6] test: `AgentNetworkInterface{}` --- proxmox/data_qemu_agent_test.go | 173 ++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 proxmox/data_qemu_agent_test.go diff --git a/proxmox/data_qemu_agent_test.go b/proxmox/data_qemu_agent_test.go new file mode 100644 index 00000000..94f157cc --- /dev/null +++ b/proxmox/data_qemu_agent_test.go @@ -0,0 +1,173 @@ +package proxmox + +import ( + "net" + "testing" + + "github.com/Telmate/proxmox-api-go/internal/util" + "github.com/stretchr/testify/require" +) + +func Test_AgentNetworkInterface_mapToSDK(t *testing.T) { + parseMAC := func(mac string) net.HardwareAddr { + parsedMac, _ := net.ParseMAC(mac) + return parsedMac + } + parseCIDR := func(cidr string) (ip net.IP) { + ip, _, _ = net.ParseCIDR(cidr) + return + } + type testInput struct { + params map[string]interface{} + statistics *bool // nil is false and true at the same time + } + baseInput := func(statistics *bool, params []interface{}) testInput { + return testInput{ + params: map[string]interface{}{"result": params}, + statistics: statistics} + } + inputFullTest := func() []interface{} { + return []interface{}{ + map[string]interface{}{ + "hardware-address": string("54:1a:12:8f:7b:ed"), + "ip-addresses": []interface{}{ + map[string]interface{}{"ip-address": string("127.0.0.1"), "prefix": float64(8)}, + map[string]interface{}{"ip-address": string("::1"), "prefix": float64(128)}}, + "name": string("lo")}, + map[string]interface{}{ + "hardware-address": string("7a:b1:8f:2e:4d:6c"), + "name": string("eth0")}, + map[string]interface{}{ + "hardware-address": string("1a:2b:3c:4d:5e:6f"), + "ip-addresses": []interface{}{ + map[string]interface{}{"ip-address": string("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), "prefix": float64(64)}, + map[string]interface{}{"ip-address": string("192.168.0.1"), "prefix": float64(24)}, + map[string]interface{}{"ip-address": string("10.20.30.244"), "prefix": float64(16)}}, + "name": string("eth1"), + "statistics": map[string]interface{}{ + "rx-bytes": float64(8), + "rx-packets": float64(7), + "rx-errs": float64(6), + "rx-dropped": float64(5), + "tx-bytes": float64(4), + "tx-packets": float64(3), + "tx-errs": float64(2), + "tx-dropped": float64(1)}}} + } + tests := []struct { + name string + input testInput + output []AgentNetworkInterface + }{ + {name: `IpAddresses Empty`, + input: baseInput(nil, []interface{}{map[string]interface{}{ + "ip-addresses": []interface{}{}}}), + output: []AgentNetworkInterface{{IpAddresses: []net.IP{}}}}, + {name: `IpAddresses Single`, + input: baseInput(nil, []interface{}{map[string]interface{}{ + "ip-addresses": []interface{}{map[string]interface{}{ + "ip-address": string("127.0.0.1"), + "prefix": float64(8)}}}}), + output: []AgentNetworkInterface{{IpAddresses: []net.IP{ + parseCIDR("127.0.0.1/8")}}}}, + {name: `IpAddresses multiple`, + input: baseInput(nil, []interface{}{map[string]interface{}{ + "ip-addresses": []interface{}{ + map[string]interface{}{ + "ip-address": string("127.0.0.1"), + "prefix": float64(8)}, + map[string]interface{}{ + "ip-address": string("::1"), + "prefix": float64(128)}}}}), + output: []AgentNetworkInterface{{IpAddresses: []net.IP{ + parseCIDR("127.0.0.1/8"), + parseCIDR("::1/128")}}}}, + {name: `MacAddress`, + input: baseInput(nil, []interface{}{map[string]interface{}{ + "hardware-address": string("54:1a:12:8f:7b:ed")}}), + output: []AgentNetworkInterface{{MacAddress: parseMAC("54:1a:12:8f:7b:ed")}}}, + {name: `Name`, + input: baseInput(nil, []interface{}{map[string]interface{}{ + "name": "test"}}), + output: []AgentNetworkInterface{{Name: string("test")}}}, + {name: `Statistics false full`, + input: baseInput(util.Pointer(false), []interface{}{map[string]interface{}{ + "statistics": map[string]interface{}{ + "rx-bytes": float64(1)}}}), + output: []AgentNetworkInterface{{Statistics: nil}}}, + {name: `Statistics true full`, + input: baseInput(util.Pointer(true), []interface{}{map[string]interface{}{ + "statistics": map[string]interface{}{ + "rx-bytes": float64(1), + "rx-packets": float64(2), + "rx-errs": float64(3), + "rx-dropped": float64(4), + "tx-bytes": float64(5), + "tx-packets": float64(6), + "tx-errs": float64(7), + "tx-dropped": float64(8)}}}), + output: []AgentNetworkInterface{{Statistics: &AgentInterfaceStatistics{ + RxBytes: 1, + RxPackets: 2, + RxErrors: 3, + RxDropped: 4, + TxBytes: 5, + TxPackets: 6, + TxErrors: 7, + TxDropped: 8}}}}, + {name: `Statistics true&false empty`, + input: baseInput(nil, []interface{}{map[string]interface{}{}}), + output: []AgentNetworkInterface{{Statistics: nil}}}, + {name: `Full true`, + input: baseInput(util.Pointer(true), inputFullTest()), + output: []AgentNetworkInterface{ + {Name: string("lo"), + MacAddress: parseMAC("54:1a:12:8f:7b:ed"), + IpAddresses: []net.IP{ + parseCIDR("127.0.0.1/8"), + parseCIDR("::1/128")}}, + {Name: string("eth0"), + MacAddress: parseMAC("7a:b1:8f:2e:4d:6c")}, + {Name: string("eth1"), + MacAddress: parseMAC("1a:2b:3c:4d:5e:6f"), + IpAddresses: []net.IP{ + parseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"), + parseCIDR("192.168.0.1/24"), + parseCIDR("10.20.30.244/16")}, + Statistics: &AgentInterfaceStatistics{ + RxBytes: 8, + RxPackets: 7, + RxErrors: 6, + RxDropped: 5, + TxBytes: 4, + TxPackets: 3, + TxErrors: 2, + TxDropped: 1}}}}, + {name: `Full false`, + input: baseInput(util.Pointer(false), inputFullTest()), + output: []AgentNetworkInterface{ + {Name: string("lo"), + MacAddress: parseMAC("54:1a:12:8f:7b:ed"), + IpAddresses: []net.IP{ + parseCIDR("127.0.0.1/8"), + parseCIDR("::1/128")}}, + {Name: string("eth0"), + MacAddress: parseMAC("7a:b1:8f:2e:4d:6c")}, + {Name: string("eth1"), + MacAddress: parseMAC("1a:2b:3c:4d:5e:6f"), + IpAddresses: []net.IP{ + parseCIDR("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64"), + parseCIDR("192.168.0.1/24"), + parseCIDR("10.20.30.244/16")}}}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.input.statistics != nil { + require.Equal(t, test.output, AgentNetworkInterface{}.mapToSDK(test.input.params, *test.input.statistics)) + } else { + require.Equal(t, test.output, AgentNetworkInterface{}.mapToSDK(test.input.params, false)) + require.Equal(t, test.output, AgentNetworkInterface{}.mapToSDK(test.input.params, true)) + } + }) + } +} From df9fdb357708ddc6c59014a717f2b03fabec739e Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:59:36 +0200 Subject: [PATCH 5/6] Mark as deprecated --- proxmox/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proxmox/client.go b/proxmox/client.go index a71070e5..8209a6c9 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -368,6 +368,7 @@ func (c *Client) GetVmSpiceProxy(vmr *VmRef) (vmSpiceProxy map[string]interface{ return } +// deprecated use *VmRef.GetAgentInformation() instead func (c *Client) GetVmAgentNetworkInterfaces(vmr *VmRef) ([]AgentNetworkInterface, error) { return vmr.GetAgentInformation(c, true) } From dfa6aa53334aa164c7b2646acfce492771200515 Mon Sep 17 00:00:00 2001 From: Tinyblargon <76069640+Tinyblargon@users.noreply.github.com> Date: Thu, 25 Apr 2024 23:16:20 +0200 Subject: [PATCH 6/6] Remove unused code --- proxmox/client.go | 15 --------------- proxmox/session.go | 1 + 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/proxmox/client.go b/proxmox/client.go index 8209a6c9..21bdf6b2 100644 --- a/proxmox/client.go +++ b/proxmox/client.go @@ -373,21 +373,6 @@ func (c *Client) GetVmAgentNetworkInterfaces(vmr *VmRef) ([]AgentNetworkInterfac return vmr.GetAgentInformation(c, true) } -func (c *Client) doAgentGet(vmr *VmRef, command string, output interface{}) error { - err := c.CheckVmRef(vmr) - if err != nil { - return err - } - - url := fmt.Sprintf("/nodes/%s/%s/%d/agent/%s", vmr.node, vmr.vmType, vmr.vmId, command) - resp, err := c.session.Get(url, nil, nil) - if err != nil { - return err - } - - return TypedResponse(resp, output) -} - func (c *Client) CreateTemplate(vmr *VmRef) error { err := c.CheckVmRef(vmr) if err != nil { diff --git a/proxmox/session.go b/proxmox/session.go index cd124bee..5cf39203 100644 --- a/proxmox/session.go +++ b/proxmox/session.go @@ -145,6 +145,7 @@ func ResponseJSON(resp *http.Response) (jbody map[string]interface{}, err error) return jbody, err } +// Is this needed? func TypedResponse(resp *http.Response, v interface{}) error { var intermediate struct { Data struct {