Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-implement: agent interface information #329

Merged
merged 6 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 13 additions & 53 deletions proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"os"
"regexp"
Expand Down Expand Up @@ -154,16 +153,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)
}
Expand Down Expand Up @@ -363,54 +368,9 @@ 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
}

// deprecated use *VmRef.GetAgentInformation() instead
func (c *Client) GetVmAgentNetworkInterfaces(vmr *VmRef) ([]AgentNetworkInterface, error) {
var ifs []AgentNetworkInterface
err := c.doAgentGet(vmr, "network-get-interfaces", &ifs)
return ifs, err
}

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)
return vmr.GetAgentInformation(c, true)
}

func (c *Client) CreateTemplate(vmr *VmRef) error {
Expand Down Expand Up @@ -2075,8 +2035,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
}
Expand All @@ -2099,8 +2059,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
}
Expand Down
7 changes: 0 additions & 7 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
83 changes: 83 additions & 0 deletions proxmox/data_qemu_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 net.HardwareAddr
IpAddresses []net.IP
Name string
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
}
173 changes: 173 additions & 0 deletions proxmox/data_qemu_agent_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
})
}
}
1 change: 1 addition & 0 deletions proxmox/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading