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

feat: type NodeName #392

Merged
merged 1 commit into from
Dec 28, 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
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ func main() {
fmt.Printf("Missing target node\n")
os.Exit(1)
}
_, err := c.MigrateNode(ctx, vmr, args[2], true)
_, err := c.MigrateNode(ctx, vmr, proxmox.NodeName(args[2]), true)

if err != nil {
log.Printf("Error to move %+v\n", err)
Expand Down
30 changes: 15 additions & 15 deletions proxmox/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ const (
// 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 {
vmId int
node string
node NodeName
pool string
vmType string
haState string
haGroup string
}

func (vmr *VmRef) SetNode(node string) {
vmr.node = node
vmr.node = NodeName(node)
}

func (vmr *VmRef) SetPool(pool string) {
Expand All @@ -78,7 +78,7 @@ func (vmr *VmRef) VmId() int {
return vmr.vmId
}

func (vmr *VmRef) Node() string {
func (vmr *VmRef) Node() NodeName {
return vmr.node
}

Expand Down Expand Up @@ -235,7 +235,7 @@ func (c *Client) GetVmInfo(ctx context.Context, vmr *VmRef) (vmInfo map[string]i
vm := vms[vmii].(map[string]interface{})
if int(vm["vmid"].(float64)) == vmr.vmId {
vmInfo = vm
vmr.node = vmInfo["node"].(string)
vmr.node = NodeName(vmInfo["node"].(string))
vmr.vmType = vmInfo["type"].(string)
vmr.pool = ""
if vmInfo["pool"] != nil {
Expand Down Expand Up @@ -268,7 +268,7 @@ func (c *Client) GetVmRefsByName(ctx context.Context, vmName string) (vmrs []*Vm
vm := vms[vmii].(map[string]interface{})
if vm["name"] != nil && vm["name"].(string) == vmName {
vmr := NewVmRef(int(vm["vmid"].(float64)))
vmr.node = vm["node"].(string)
vmr.node = NodeName(vm["node"].(string))
vmr.vmType = vm["type"].(string)
vmr.pool = ""
if vm["pool"] != nil {
Expand Down Expand Up @@ -297,7 +297,7 @@ func (c *Client) GetVmRefById(ctx context.Context, vmId int) (vmr *VmRef, err er
vm := vms[vmii].(map[string]interface{})
if int(vm["vmid"].(float64)) != 0 && int(vm["vmid"].(float64)) == vmId {
vmr = NewVmRef(int(vm["vmid"].(float64)))
vmr.node = vm["node"].(string)
vmr.node = NodeName(vm["node"].(string))
vmr.vmType = vm["type"].(string)
vmr.pool = ""
if vm["pool"] != nil {
Expand All @@ -321,15 +321,15 @@ func (c *Client) GetVmState(ctx context.Context, vmr *VmRef) (vmState map[string
if err != nil {
return nil, err
}
return c.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/status/current", "vm", "STATE")
return c.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/status/current", "vm", "STATE")
}

func (c *Client) GetVmConfig(ctx context.Context, vmr *VmRef) (vmConfig map[string]interface{}, err error) {
err = c.CheckVmRef(ctx, vmr)
if err != nil {
return nil, err
}
return c.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/config", "vm", "CONFIG")
return c.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/config", "vm", "CONFIG")
}

func (c *Client) GetStorageStatus(ctx context.Context, vmr *VmRef, storageName string) (storageStatus map[string]interface{}, err error) {
Expand Down Expand Up @@ -588,7 +588,7 @@ func (c *Client) DeleteVmParams(ctx context.Context, vmr *VmRef, params map[stri
return
}

func (c *Client) CreateQemuVm(ctx context.Context, node string, vmParams map[string]interface{}) (exitStatus string, err error) {
func (c *Client) CreateQemuVm(ctx context.Context, node NodeName, vmParams map[string]interface{}) (exitStatus string, err error) {
// Create VM disks first to ensure disks names.
createdDisks, createdDisksErr := c.createVMDisks(ctx, node, vmParams)
if createdDisksErr != nil {
Expand Down Expand Up @@ -745,7 +745,7 @@ func (c *Client) RollbackQemuVm(vmr *VmRef, snapshot string) (exitStatus string,

// DEPRECATED SetVmConfig - send config options
func (c *Client) SetVmConfig(vmr *VmRef, params map[string]interface{}) (exitStatus interface{}, err error) {
return c.PostWithTask(context.Background(), params, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/config")
return c.PostWithTask(context.Background(), params, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/config")
}

// SetLxcConfig - send config options
Expand All @@ -767,9 +767,9 @@ func (c *Client) SetLxcConfig(ctx context.Context, vmr *VmRef, vmParams map[stri
}

// MigrateNode - Migrate a VM
func (c *Client) MigrateNode(ctx context.Context, vmr *VmRef, newTargetNode string, online bool) (exitStatus interface{}, err error) {
func (c *Client) MigrateNode(ctx context.Context, vmr *VmRef, newTargetNode NodeName, online bool) (exitStatus interface{}, err error) {
reqbody := ParamsToBody(map[string]interface{}{"target": newTargetNode, "online": online, "with-local-disks": true})
url := fmt.Sprintf("/nodes/%s/%s/%d/migrate", vmr.node, vmr.vmType, vmr.vmId)
url := fmt.Sprintf("/nodes/%s/%s/%d/migrate", vmr.node.String(), vmr.vmType, vmr.vmId)
resp, err := c.session.Post(ctx, url, nil, nil, &reqbody)
if err == nil {
taskResponse, err := ResponseJSON(resp)
Expand Down Expand Up @@ -938,7 +938,7 @@ func (c *Client) VMIdExists(ctx context.Context, vmID int) (exists bool, err err
// CreateVMDisk - Create single disk for VM on host node.
func (c *Client) CreateVMDisk(
ctx context.Context,
nodeName string,
nodeName NodeName,
storageName string,
fullDiskName string,
diskParams map[string]interface{},
Expand Down Expand Up @@ -966,7 +966,7 @@ var rxStorageModels = regexp.MustCompile(`(ide|sata|scsi|virtio)\d+`)
// createVMDisks - Make disks parameters and create all VM disks on host node.
func (c *Client) createVMDisks(
ctx context.Context,
node string,
node NodeName,
vmParams map[string]interface{},
) (disks []string, err error) {
var createdDisks []string
Expand Down Expand Up @@ -1020,7 +1020,7 @@ func (c *Client) CreateNewDisk(ctx context.Context, vmr *VmRef, disk string, vol
// so mainly this is used to delete the disks in case VM creation didn't complete.
func (c *Client) DeleteVMDisks(
ctx context.Context,
node string,
node NodeName,
disks []string,
) error {
for _, fullDiskName := range disks {
Expand Down
6 changes: 3 additions & 3 deletions proxmox/config_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func GuestHasFeature(ctx context.Context, vmr *VmRef, client *Client, feature Gu

func guestHasFeature(ctx context.Context, vmr *VmRef, client *Client, feature GuestFeature) (bool, error) {
var params map[string]interface{}
params, err := client.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/feature?feature=snapshot", "guest", "FEATURES")
params, err := client.GetItemConfigMapStringInterface(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/feature?feature=snapshot", "guest", "FEATURES")
if err != nil {
return false, err
}
Expand Down Expand Up @@ -226,7 +226,7 @@ func GuestShutdown(ctx context.Context, vmr *VmRef, client *Client, force bool)
if force {
params = map[string]interface{}{"forceStop": force}
}
_, err = client.PostWithTask(ctx, params, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/status/shutdown")
_, err = client.PostWithTask(ctx, params, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/status/shutdown")
return
}

Expand Down Expand Up @@ -266,5 +266,5 @@ func pendingGuestConfigFromApi(ctx context.Context, vmr *VmRef, client *Client)
if err := client.CheckVmRef(ctx, vmr); err != nil {
return nil, err
}
return client.GetItemConfigInterfaceArray(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/pending", "Guest", "PENDING CONFIG")
return client.GetItemConfigInterfaceArray(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/pending", "Guest", "PENDING CONFIG")
}
2 changes: 1 addition & 1 deletion proxmox/config_lxc.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ func (config ConfigLxc) CreateLxc(ctx context.Context, vmr *VmRef, client *Clien
// amend vmid
paramMap["vmid"] = vmr.vmId

exitStatus, err := client.CreateLxcContainer(ctx, vmr.node, paramMap)
exitStatus, err := client.CreateLxcContainer(ctx, vmr.node.String(), paramMap)
if err != nil {
params, _ := json.Marshal(&paramMap)
return fmt.Errorf("error creating LXC container: %v, error status: %s (params: %v)", err, exitStatus, string(params))
Expand Down
8 changes: 4 additions & 4 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type ConfigQemu struct {
Memory *QemuMemory `json:"memory,omitempty"`
Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations
Networks QemuNetworkInterfaces `json:"networks,omitempty"`
Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead
Node NodeName `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead
Onboot *bool `json:"onboot,omitempty"`
Pool *PoolName `json:"pool,omitempty"`
Protection *bool `json:"protection,omitempty"`
Expand Down Expand Up @@ -480,7 +480,7 @@ func (newConfig ConfigQemu) setAdvanced(ctx context.Context, currentConfig *Conf

if currentConfig != nil { // Update
// TODO implement tmp move and version change
url := "/nodes/" + vmr.node + "/" + vmr.vmType + "/" + strconv.Itoa(vmr.vmId) + "/config"
url := "/nodes/" + vmr.node.String() + "/" + vmr.vmType + "/" + strconv.Itoa(vmr.vmId) + "/config"
var itemsToDeleteBeforeUpdate string // this is for items that should be removed before they can be created again e.g. cloud-init disks. (convert to array when needed)
stopped := false

Expand Down Expand Up @@ -543,13 +543,13 @@ func (newConfig ConfigQemu) setAdvanced(ctx context.Context, currentConfig *Conf
}

if newConfig.Node != currentConfig.Node { // Migrate VM
vmr.SetNode(currentConfig.Node)
vmr.node = newConfig.Node
_, err = client.MigrateNode(ctx, vmr, newConfig.Node, true)
if err != nil {
return
}
// Set node to the node the VM was migrated to
vmr.SetNode(newConfig.Node)
vmr.node = newConfig.Node
}

rebootRequired, params, err = newConfig.mapToAPI(*currentConfig, version)
Expand Down
2 changes: 1 addition & 1 deletion proxmox/data_qemu_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ func (vmr *VmRef) GetAgentInformation(ctx context.Context, c *Client, statistics
}
vmid := strconv.FormatInt(int64(vmr.vmId), 10)
params, err := c.GetItemConfigMapStringInterface(ctx,
"/nodes/"+vmr.node+"/qemu/"+vmid+"/agent/network-get-interfaces", "guest agent", "data",
"/nodes/"+vmr.node.String()+"/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 {
Expand Down
52 changes: 52 additions & 0 deletions proxmox/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,63 @@ package proxmox

import (
"context"
"errors"
"fmt"
"io"
"net/http"
)

// Only the following characters are allowed: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-".
// May not start with a hyphen.
// May not end with a hyphen.
// Must contain at least one alphabetical character.
// Max length 63 characters.
type NodeName string

const (
NodeName_Error_Alphabetical string = "Node name must contain at least one alphabetical character"
NodeName_Error_Empty string = "Node name cannot be empty"
NodeName_Error_HyphenEnd string = "Node name cannot end with a hyphen"
NodeName_Error_HyphenStart string = "Node name cannot start with a hyphen"
NodeName_Error_Illegal string = "Node name may only contain the following characters: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
NodeName_Error_Length string = "Node name must be less than 64 characters"
)

func (name NodeName) Validate() error {
if name == "" {
return errors.New(NodeName_Error_Empty)
}
if len(name) > 63 {
return errors.New(NodeName_Error_Length)
}
if name[0] == '-' {
return errors.New(NodeName_Error_HyphenStart)
}
if name[len(name)-1] == '-' {
return errors.New(NodeName_Error_HyphenEnd)
}
var hasAlpha bool
for i := range name {
if (name[i] >= 'a' && name[i] <= 'z') || (name[i] >= 'A' && name[i] <= 'Z') {
hasAlpha = true
break
}
}
if !hasAlpha {
return errors.New(NodeName_Error_Alphabetical)
}
for i := range name {
if !((name[i] >= 'a' && name[i] <= 'z') || (name[i] >= 'A' && name[i] <= 'Z') || (name[i] >= '0' && name[i] <= '9') || name[i] == '-') {
return errors.New(NodeName_Error_Illegal)
}
}
return nil
}

func (name NodeName) String() string {
return string(name)
}

func (c *Client) nodeStatusCommand(ctx context.Context, node, command string) (exitStatus string, err error) {
nodes, err := c.GetNodeList(ctx)
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions proxmox/node_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package proxmox

import (
"errors"
"testing"

"github.com/Telmate/proxmox-api-go/test/data/test_data_node"
"github.com/stretchr/testify/require"
)

func Test_NodeName_Validate(t *testing.T) {
tests := []struct {
name string
input []string
output error
}{
{name: `Valid NodeName`,
input: test_data_node.NodeName_Legals()},
{name: `Invalid Empty`,
input: []string{""},
output: errors.New(NodeName_Error_Empty)},
{name: `Invalid Length`,
input: []string{test_data_node.NodeName_Max_Illegal()},
output: errors.New(NodeName_Error_Length)},
{name: `Invalid Start Hyphen`,
input: test_data_node.NodeName_StartHyphens(),
output: errors.New(NodeName_Error_HyphenStart)},
{name: `Invalid End Hyphen`,
input: test_data_node.NodeName_EndHyphens(),
output: errors.New(NodeName_Error_HyphenEnd)},
{name: `Invalid Alphabetical`,
input: test_data_node.NodeName_Numeric_Illegal(),
output: errors.New(NodeName_Error_Alphabetical)},
{name: `Invalid Characters`,
input: test_data_node.NodeName_Error_Characters(),
output: errors.New(NodeName_Error_Illegal)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
for _, input := range test.input {
require.Equal(t, test.output, NodeName(input).Validate())
}
})
}
}

func Test_NodeName_String(t *testing.T) {
tests := []struct {
name string
input NodeName
output string
}{
{name: `Empty`,
input: "",
output: ""},
{name: `Valid`,
input: "node1",
output: "node1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, test.output, test.input.String())
})
}
}
10 changes: 5 additions & 5 deletions proxmox/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func (config ConfigSnapshot) Create(ctx context.Context, c *Client, vmr *VmRef)
// Create a snapshot without validating the input, use ConfigSnapshot.Create() to validate the input.
func (config ConfigSnapshot) Create_Unsafe(ctx context.Context, c *Client, vmr *VmRef) error {
params := config.mapToApiValues()
_, err := c.PostWithTask(ctx, params, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/")
_, err := c.PostWithTask(ctx, params, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/")
if err != nil {
params, _ := json.Marshal(&params)
return fmt.Errorf("error creating Snapshot: %v, (params: %v)", err, string(params))
Expand All @@ -62,7 +62,7 @@ func ListSnapshots(ctx context.Context, c *Client, vmr *VmRef) (rawSnapshots, er
if err := c.CheckVmRef(ctx, vmr); err != nil {
return nil, err
}
return c.GetItemConfigInterfaceArray(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/", "Guest", "SNAPSHOTS")
return c.GetItemConfigInterfaceArray(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/", "Guest", "SNAPSHOTS")
}

// Updates the description of the specified snapshot, same as SnapshotName.UpdateDescription()
Expand Down Expand Up @@ -161,7 +161,7 @@ func (snap SnapshotName) Delete(ctx context.Context, c *Client, vmr *VmRef) (exi

// Deletes the specified snapshot without validating the input, use SnapshotName.Delete() to validate the input.
func (snap SnapshotName) Delete_Unsafe(ctx context.Context, c *Client, vmr *VmRef) (exitStatus string, err error) {
return c.DeleteWithTask(ctx, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+string(snap))
return c.DeleteWithTask(ctx, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+string(snap))
}

// Rollback to the specified snapshot, validates the input
Expand All @@ -178,7 +178,7 @@ func (snap SnapshotName) Rollback(ctx context.Context, c *Client, vmr *VmRef) (e

// Rollback to the specified snapshot without validating the input, use SnapshotName.Rollback() to validate the input.
func (snap SnapshotName) Rollback_Unsafe(ctx context.Context, c *Client, vmr *VmRef) (exitStatus string, err error) {
return c.PostWithTask(ctx, nil, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.FormatInt(int64(vmr.vmId), 10)+"/snapshot/"+string(snap)+"/rollback")
return c.PostWithTask(ctx, nil, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.FormatInt(int64(vmr.vmId), 10)+"/snapshot/"+string(snap)+"/rollback")
}

// Updates the description of the specified snapshot, validates the input
Expand All @@ -195,7 +195,7 @@ func (snap SnapshotName) UpdateDescription(ctx context.Context, c *Client, vmr *

// Updates the description of the specified snapshot without validating the input, use SnapshotName.UpdateDescription() to validate the input.
func (snap SnapshotName) UpdateDescription_Unsafe(ctx context.Context, c *Client, vmr *VmRef, description string) error {
return c.Put(ctx, map[string]interface{}{"description": description}, "/nodes/"+vmr.node+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+string(snap)+"/config")
return c.Put(ctx, map[string]interface{}{"description": description}, "/nodes/"+vmr.node.String()+"/"+vmr.vmType+"/"+strconv.Itoa(vmr.vmId)+"/snapshot/"+string(snap)+"/config")
}

func (name SnapshotName) Validate() error {
Expand Down
Loading
Loading