Skip to content

Commit

Permalink
Merge pull request #273 from Tinyblargon/Feature-Guest-Features
Browse files Browse the repository at this point in the history
Feature: guest features
  • Loading branch information
mleone87 authored Dec 6, 2023
2 parents efb8bd6 + daeb78f commit bbb71ca
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 11 deletions.
1 change: 1 addition & 0 deletions cli/command/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
_ "github.com/Telmate/proxmox-api-go/cli/command/delete"
_ "github.com/Telmate/proxmox-api-go/cli/command/example"
_ "github.com/Telmate/proxmox-api-go/cli/command/get"
_ "github.com/Telmate/proxmox-api-go/cli/command/get/guest"
_ "github.com/Telmate/proxmox-api-go/cli/command/get/id"
_ "github.com/Telmate/proxmox-api-go/cli/command/guest"
_ "github.com/Telmate/proxmox-api-go/cli/command/guest/qemu"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package get
package guest

import (
"github.com/Telmate/proxmox-api-go/cli"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/spf13/cobra"
)

var get_guestCmd = &cobra.Command{
Use: "guest GUESTID",
var configCmd = &cobra.Command{
Use: "config GUESTID",
Short: "Gets the configuration of the specified guest",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
Expand All @@ -29,11 +29,11 @@ var get_guestCmd = &cobra.Command{
if err != nil {
return
}
cli.PrintFormattedJson(GetCmd.OutOrStdout(), config)
cli.PrintFormattedJson(guestCmd.OutOrStdout(), config)
return
},
}

func init() {
GetCmd.AddCommand(get_guestCmd)
guestCmd.AddCommand(configCmd)
}
32 changes: 32 additions & 0 deletions cli/command/get/guest/get-guest-feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package guest

import (
"github.com/Telmate/proxmox-api-go/cli"
"github.com/Telmate/proxmox-api-go/proxmox"
"github.com/spf13/cobra"
)

var featureCmd = &cobra.Command{
Use: "feature GUESTID",
Short: "Gets the available features of the specified guest",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) (err error) {
id := cli.ValidateIntIDset(args, "GuestID")
vmr := proxmox.NewVmRef(id)
c := cli.NewClient()
err = c.CheckVmRef(vmr)
if err != nil {
return
}
features, err := proxmox.ListGuestFeatures(vmr, c)
if err != nil {
return
}
cli.PrintFormattedJson(guestCmd.OutOrStdout(), features)
return
},
}

func init() {
guestCmd.AddCommand(featureCmd)
}
15 changes: 15 additions & 0 deletions cli/command/get/guest/get-guest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package guest

import (
"github.com/Telmate/proxmox-api-go/cli/command/get"
"github.com/spf13/cobra"
)

var guestCmd = &cobra.Command{
Use: "guest",
Short: "Commands to get information of guests on Proxmox",
}

func init() {
get.GetCmd.AddCommand(guestCmd)
}
7 changes: 7 additions & 0 deletions proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type Client struct {
TaskTimeout int
}

const (
VmRef_Error_Nil string = "vm reference may not be nil"
)

// VmRef - virtual machine ref parts
// map[type:qemu node:proxmox1-xx id:qemu/132 diskread:5.57424738e+08 disk:0 netin:5.9297450593e+10 mem:3.3235968e+09 uptime:1.4567097e+07 vmid:132 template:0 maxcpu:2 netout:6.053310416e+09 maxdisk:3.4359738368e+10 maxmem:8.592031744e+09 diskwrite:1.49663619584e+12 status:running cpu:0.00386980694947209 name:appt-app1-dev.xxx.xx]
type VmRef struct {
Expand Down Expand Up @@ -177,6 +181,9 @@ func (c *Client) GetVmList() (map[string]interface{}, error) {
}

func (c *Client) CheckVmRef(vmr *VmRef) (err error) {
if vmr == nil {
return errors.New(VmRef_Error_Nil)
}
if vmr.node == "" || vmr.vmType == "" {
_, err = c.GetVmInfo(vmr)
}
Expand Down
81 changes: 76 additions & 5 deletions proxmox/config_guest.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package proxmox

import (
"errors"
"strconv"
"strings"
)
Expand Down Expand Up @@ -102,13 +103,69 @@ func (GuestResource) mapToStruct(params []interface{}) []GuestResource {
return resources
}

// Enum
type GuestFeature string

const (
GuestFeature_Clone GuestFeature = "clone"
GuestFeature_Copy GuestFeature = "copy"
GuestFeature_Snapshot GuestFeature = "snapshot"
)

func (GuestFeature) Error() error {
return errors.New("value should be one of (" + string(GuestFeature_Clone) + " ," + string(GuestFeature_Copy) + " ," + string(GuestFeature_Snapshot) + ")")
}

func (GuestFeature) mapToStruct(params map[string]interface{}) bool {
if value, isSet := params["hasFeature"]; isSet {
return Itob(int(value.(float64)))
}
return false
}

func (feature GuestFeature) Validate() error {
switch feature {
case GuestFeature_Copy, GuestFeature_Clone, GuestFeature_Snapshot:
return nil
}
return GuestFeature("").Error()
}

type GuestFeatures struct {
Clone bool `json:"clone"`
Copy bool `json:"copy"`
Snapshot bool `json:"snapshot"`
}

type GuestType string

const (
GuestLXC GuestType = "lxc"
GuestQemu GuestType = "qemu"
)

// check if the guest has the specified feature.
func GuestHasFeature(vmr *VmRef, client *Client, feature GuestFeature) (bool, error) {
err := feature.Validate()
if err != nil {
return false, err
}
err = client.CheckVmRef(vmr)
if err != nil {
return false, err
}
return guestHasFeature(vmr, client, feature)
}

func guestHasFeature(vmr *VmRef, client *Client, feature GuestFeature) (bool, error) {
var params map[string]interface{}
params, err := client.GetItemConfigMapStringInterface("/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/feature?feature=snapshot", "guest", "FEATURES")
if err != nil {
return false, err
}
return GuestFeature("").mapToStruct(params), nil
}

// Check if there are any pending changes that require a reboot to be applied.
func GuestHasPendingChanges(vmr *VmRef, client *Client) (bool, error) {
params, err := pendingGuestConfigFromApi(vmr, client)
Expand All @@ -128,6 +185,24 @@ func GuestReboot(vmr *VmRef, client *Client) (err error) {
return
}

// List all features the guest has.
func ListGuestFeatures(vmr *VmRef, client *Client) (features GuestFeatures, err error) {
err = client.CheckVmRef(vmr)
if err != nil {
return
}
features.Clone, err = guestHasFeature(vmr, client, GuestFeature_Clone)
if err != nil {
return
}
features.Copy, err = guestHasFeature(vmr, client, GuestFeature_Copy)
if err != nil {
return
}
features.Snapshot, err = guestHasFeature(vmr, client, GuestFeature_Snapshot)
return
}

// List all guest the user has viewing rights for in the cluster
func ListGuests(client *Client) ([]GuestResource, error) {
list, err := client.GetResourceList("vm")
Expand All @@ -138,11 +213,7 @@ func ListGuests(client *Client) ([]GuestResource, error) {
}

func pendingGuestConfigFromApi(vmr *VmRef, client *Client) ([]interface{}, error) {
err := vmr.nilCheck()
if err != nil {
return nil, err
}
if err = client.CheckVmRef(vmr); err != nil {
if err := client.CheckVmRef(vmr); err != nil {
return nil, err
}
return client.GetItemConfigInterfaceArray("/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/pending", "Guest", "PENDING CONFIG")
Expand Down
59 changes: 59 additions & 0 deletions proxmox/config_guest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,3 +237,62 @@ func Test_GuestResource_mapToStruct(t *testing.T) {
})
}
}

func Test_GuestFeature_mapToStruct(t *testing.T) {
tests := []struct {
name string
input map[string]interface{}
output bool
}{
{name: "false",
input: map[string]interface{}{"hasFeature": float64(0)},
output: false,
},
{name: "not set",
input: map[string]interface{}{},
output: false,
},
{name: "true",
input: map[string]interface{}{"hasFeature": float64(1)},
output: true,
},
}
for _, test := range tests {
t.Run(test.name, func(*testing.T) {
require.Equal(t, test.output, GuestFeature("").mapToStruct(test.input), test.name)
})
}
}

func Test_GuestFeature_Validate(t *testing.T) {
tests := []struct {
name string
input GuestFeature
err error
}{
// Invalid
{name: "Invalid empty",
input: "",
err: GuestFeature("").Error(),
},
{name: "Invalid not enum",
input: "invalid",
err: GuestFeature("").Error(),
},
// Valid
{name: "Valid GuestFeature_Clone",
input: GuestFeature_Clone,
},
{name: "Valid GuestFeature_Copy",
input: GuestFeature_Copy,
},
{name: "Valid GuestFeature_Snapshot",
input: GuestFeature_Snapshot,
},
}
for _, test := range tests {
t.Run(test.name, func(*testing.T) {
require.Equal(t, test.err, test.input.Validate(), test.name)
})
}
}
2 changes: 1 addition & 1 deletion proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ type ConfigQemu struct {
Description string `json:"description,omitempty"`
Disks *QemuStorages `json:"disks,omitempty"`
EFIDisk QemuDevice `json:"efidisk,omitempty"` // TODO should be a struct
RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct
FullClone *int `json:"fullclone,omitempty"` // TODO should probably be a bool
HaGroup string `json:"hagroup,omitempty"`
HaState string `json:"hastate,omitempty"` // TODO should be custom type with enum
Expand Down Expand Up @@ -80,6 +79,7 @@ type ConfigQemu struct {
QemuUsbs QemuDevices `json:"usb,omitempty"` // TODO should be a struct
QemuVcpus int `json:"vcpus,omitempty"` // TODO should be uint
QemuVga QemuDevice `json:"vga,omitempty"` // TODO should be a struct
RNGDrive QemuDevice `json:"rng0,omitempty"` // TODO should be a struct
Scsihw string `json:"scsihw,omitempty"` // TODO should be custom type with enum
Searchdomain string `json:"searchdomain,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum?
Expand Down

0 comments on commit bbb71ca

Please sign in to comment.