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

Overhaul: CloudInit #339

Merged
merged 14 commits into from
Jun 16, 2024
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ module github.com/Telmate/proxmox-api-go
go 1.19

require (
github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.2
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
6 changes: 6 additions & 0 deletions proxmox/config_guest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package proxmox

import (
"errors"
"net/netip"
"strconv"
)

// All code LXC and Qemu have in common should be placed here.

type GuestDNS struct {
NameServers *[]netip.Addr `json:"nameservers,omitempty"`
SearchDomain *string `json:"searchdomain,omitempty"` // we are not validating this field, as validating domain names is a complex topic.
}

type GuestResource struct {
CpuCores uint `json:"cpu_cores"`
CpuUsage float64 `json:"cpu_usage"`
Expand Down
213 changes: 79 additions & 134 deletions proxmox/config_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"log"
"math/rand"
"net"
"net/url"
"regexp"
"strconv"
"strings"
Expand All @@ -35,27 +34,23 @@ type ConfigQemu struct {
Args string `json:"args,omitempty"`
Balloon int `json:"balloon,omitempty"` // TODO should probably be a bool
Bios string `json:"bios,omitempty"`
Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums
BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api
CIcustom string `json:"cicustom,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIpassword string `json:"cipassword,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
CIuser string `json:"ciuser,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Boot string `json:"boot,omitempty"` // TODO should be an array of custom enums
BootDisk string `json:"bootdisk,omitempty"` // TODO discuss deprecation? Only returned as it's deprecated in the proxmox api
CloudInit *CloudInit `json:"cloudinit,omitempty"`
Description string `json:"description,omitempty"`
Disks *QemuStorages `json:"disks,omitempty"`
EFIDisk QemuDevice `json:"efidisk,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
Hookscript string `json:"hookscript,omitempty"`
Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct
Ipconfig IpconfigMap `json:"ipconfig,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso
LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect
Machine string `json:"machine,omitempty"` // TODO should be custom type with enum
Memory int `json:"memory,omitempty"` // TODO should be uint
Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations
Nameserver string `json:"nameserver,omitempty"` // TODO should be part of a cloud-init struct (cloud-init option)
Node string `json:"node,omitempty"` // Only returned setting it has no effect, set node in the VmRef instead
Hotplug string `json:"hotplug,omitempty"` // TODO should be a struct
Iso *IsoFile `json:"iso,omitempty"` // Same as Disks.Ide.Disk_2.CdRom.Iso
LinkedVmId uint `json:"linked_id,omitempty"` // Only returned setting it has no effect
Machine string `json:"machine,omitempty"` // TODO should be custom type with enum
Memory int `json:"memory,omitempty"` // TODO should be uint
Name string `json:"name,omitempty"` // TODO should be custom type as there are character and length limitations
Node string `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 All @@ -69,18 +64,16 @@ type ConfigQemu struct {
QemuOs string `json:"ostype,omitempty"`
QemuPCIDevices QemuDevices `json:"hostpci,omitempty"` // TODO should be a struct
QemuPxe bool `json:"pxe,omitempty"`
QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct
QemuSockets int `json:"sockets,omitempty"` // TODO should be uint
QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a 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?
Sshkeys string `json:"sshkeys,omitempty"` // TODO should be an array of strings
Startup string `json:"startup,omitempty"` // TODO should be a struct?
QemuSerials QemuDevices `json:"serial,omitempty"` // TODO should be a struct
QemuSockets int `json:"sockets,omitempty"` // TODO should be uint
QemuUnusedDisks QemuDevices `json:"unused,omitempty"` // TODO should be a 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
Smbios1 string `json:"smbios1,omitempty"` // TODO should be custom type with enum?
Startup string `json:"startup,omitempty"` // TODO should be a struct?
TPM *TpmState `json:"tpm,omitempty"`
Tablet *bool `json:"tablet,omitempty"`
Tags *[]Tag `json:"tags,omitempty"`
Expand Down Expand Up @@ -119,9 +112,6 @@ func (config *ConfigQemu) defaults() {
if config.Hotplug == "" {
config.Hotplug = "network,disk,usb"
}
if config.Ipconfig == nil {
config.Ipconfig = IpconfigMap{}
}
if config.Protection == nil {
config.Protection = util.Pointer(false)
}
Expand Down Expand Up @@ -193,15 +183,6 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
if config.Boot != "" {
params["boot"] = config.Boot
}
if config.CIcustom != "" {
params["cicustom"] = config.CIcustom
}
if config.CIpassword != "" {
params["cipassword"] = config.CIpassword
}
if config.CIuser != "" {
params["ciuser"] = config.CIuser
}
if config.QemuCores != 0 {
params["cores"] = config.QemuCores
}
Expand Down Expand Up @@ -229,9 +210,6 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
if config.Name != "" {
params["name"] = config.Name
}
if config.Nameserver != "" {
params["nameserver"] = config.Nameserver
}
if config.QemuNuma != nil {
params["numa"] = *config.QemuNuma
}
Expand All @@ -247,15 +225,9 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
if config.Scsihw != "" {
params["scsihw"] = config.Scsihw
}
if config.Searchdomain != "" {
params["searchdomain"] = config.Searchdomain
}
if config.QemuSockets != 0 {
params["sockets"] = config.QemuSockets
}
if config.Sshkeys != "" {
params["sshkeys"] = sshKeyUrlEncode(config.Sshkeys)
}
if config.Startup != "" {
params["startup"] = config.Startup
}
Expand Down Expand Up @@ -307,6 +279,10 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire
}
}

if config.CloudInit != nil {
itemsToDelete += config.CloudInit.mapToAPI(currentConfig.CloudInit, params)
}

// Create EFI disk
config.CreateQemuEfiParams(params)

Expand All @@ -330,13 +306,8 @@ func (config ConfigQemu) mapToApiValues(currentConfig ConfigQemu) (rebootRequire

config.CreateQemuPCIsParams(params)

err = config.CreateIpconfigParams(params)
if err != nil {
log.Printf("[ERROR] %q", err)
}

if itemsToDelete != "" {
params["delete"] = itemsToDelete
params["delete"] = strings.TrimPrefix(itemsToDelete, ",")
}
return
}
Expand All @@ -351,7 +322,9 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
// description:Base image
// cores:2 ostype:l26

config := ConfigQemu{}
config := ConfigQemu{
CloudInit: CloudInit{}.mapToSDK(params),
}

if vmr != nil {
config.Node = vmr.node
Expand Down Expand Up @@ -382,15 +355,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
if _, isSet := params["bios"]; isSet {
config.Bios = params["bios"].(string)
}
if _, isSet := params["cicustom"]; isSet {
config.CIcustom = params["cicustom"].(string)
}
if _, isSet := params["cipassword"]; isSet {
config.CIpassword = params["cipassword"].(string)
}
if _, isSet := params["ciuser"]; isSet {
config.CIuser = params["ciuser"].(string)
}
if _, isSet := params["description"]; isSet {
config.Description = strings.TrimSpace(params["description"].(string))
}
Expand Down Expand Up @@ -419,9 +383,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
if _, isSet := params["name"]; isSet {
config.Name = params["name"].(string)
}
if _, isSet := params["nameserver"]; isSet {
config.Nameserver = params["nameserver"].(string)
}
if _, isSet := params["onboot"]; isSet {
config.Onboot = util.Pointer(Itob(int(params["onboot"].(float64))))
}
Expand Down Expand Up @@ -458,12 +419,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
if _, isSet := params["scsihw"]; isSet {
config.Scsihw = params["scsihw"].(string)
}
if _, isSet := params["searchdomain"]; isSet {
config.Searchdomain = params["searchdomain"].(string)
}
if _, isSet := params["sshkeys"]; isSet {
config.Sshkeys, _ = url.PathUnescape(params["sshkeys"].(string))
}
if _, isSet := params["startup"]; isSet {
config.Startup = params["startup"].(string)
}
Expand All @@ -478,24 +433,6 @@ func (ConfigQemu) mapToStruct(vmr *VmRef, params map[string]interface{}) (*Confi
config.Smbios1 = params["smbios1"].(string)
}

ipconfigNames := []string{}

for k := range params {
if ipconfigName := rxIpconfigName.FindStringSubmatch(k); len(ipconfigName) > 0 {
ipconfigNames = append(ipconfigNames, ipconfigName[0])
}
}

if len(ipconfigNames) > 0 {
config.Ipconfig = IpconfigMap{}
for _, ipconfigName := range ipconfigNames {
ipConfStr := params[ipconfigName]
id := rxDeviceID.FindStringSubmatch(ipconfigName)
ipconfigID, _ := strconv.Atoi(id[0])
config.Ipconfig[ipconfigID] = ipConfStr
}
}

linkedVmId := uint(0)
config.Disks = QemuStorages{}.mapToStruct(params, &linkedVmId)
if linkedVmId != 0 {
Expand Down Expand Up @@ -897,6 +834,11 @@ func (config ConfigQemu) Validate(current *ConfigQemu) (err error) {
return
}
}
if config.CloudInit != nil {
if err = config.CloudInit.Validate(); err != nil {
return
}
}
if config.Disks != nil {
err = config.Disks.Validate()
if err != nil {
Expand Down Expand Up @@ -928,21 +870,6 @@ func (config ConfigQemu) Validate(current *ConfigQemu) (err error) {
return
}

// HasCloudInit - are there cloud-init options?
func (config ConfigQemu) HasCloudInit() bool {
for _, config := range config.Ipconfig {
if config != nil && config != "" {
return true
}
}
return config.CIuser != "" ||
config.CIpassword != "" ||
config.Searchdomain != "" ||
config.Nameserver != "" ||
config.Sshkeys != "" ||
config.CIcustom != ""
}

/*
CloneVm
Example: Request
Expand Down Expand Up @@ -1000,7 +927,6 @@ var (
rxSerialName = regexp.MustCompile(`serial\d+`)
rxUsbName = regexp.MustCompile(`usb\d+`)
rxPCIName = regexp.MustCompile(`hostpci\d+`)
rxIpconfigName = regexp.MustCompile(`ipconfig\d+`)
)

func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err error) {
Expand Down Expand Up @@ -1087,16 +1013,6 @@ func SshForwardUsernet(vmr *VmRef, client *Client) (sshPort string, err error) {
return
}

// URL encodes the ssh keys
func sshKeyUrlEncode(keys string) (encodedKeys string) {
encodedKeys = url.PathEscape(keys + "\n")
encodedKeys = strings.Replace(encodedKeys, "+", "%2B", -1)
encodedKeys = strings.Replace(encodedKeys, "@", "%40", -1)
encodedKeys = strings.Replace(encodedKeys, "=", "%3D", -1)
encodedKeys = strings.Replace(encodedKeys, ":", "%3A", -1)
return
}

// device_del net1
// netdev_del net1
func RemoveSshForwardUsernet(vmr *VmRef, client *Client) (err error) {
Expand Down Expand Up @@ -1328,23 +1244,6 @@ func (c ConfigQemu) CreateQemuNetworksParams(params map[string]interface{}) {
}
}

// Create parameters for each Cloud-Init ipconfig entry.
func (c ConfigQemu) CreateIpconfigParams(params map[string]interface{}) error {

for ID, config := range c.Ipconfig {
if ID > 15 {
return fmt.Errorf("only up to 16 Cloud-Init network configurations supported (ipconfig[0-15]), skipping ipconfig%d", ID)
}

if config != "" {
ipconfigName := "ipconfig" + strconv.Itoa(ID)
params[ipconfigName] = config
}
}

return nil
}

// Create RNG parameter.
func (c ConfigQemu) CreateQemuRngParams(params map[string]interface{}) {
rngParam := QemuDeviceParam{}
Expand Down Expand Up @@ -1501,3 +1400,49 @@ func (c ConfigQemu) String() string {
jsConf, _ := json.Marshal(c)
return string(jsConf)
}

type QemuNetworkInterfaceID uint8

const (
QemuNetworkInterfaceID_Error_Invalid string = "network interface ID must be in the range 0-31"

QemuNetworkInterfaceID0 QemuNetworkInterfaceID = 0
QemuNetworkInterfaceID1 QemuNetworkInterfaceID = 1
QemuNetworkInterfaceID2 QemuNetworkInterfaceID = 2
QemuNetworkInterfaceID3 QemuNetworkInterfaceID = 3
QemuNetworkInterfaceID4 QemuNetworkInterfaceID = 4
QemuNetworkInterfaceID5 QemuNetworkInterfaceID = 5
QemuNetworkInterfaceID6 QemuNetworkInterfaceID = 6
QemuNetworkInterfaceID7 QemuNetworkInterfaceID = 7
QemuNetworkInterfaceID8 QemuNetworkInterfaceID = 8
QemuNetworkInterfaceID9 QemuNetworkInterfaceID = 9
QemuNetworkInterfaceID10 QemuNetworkInterfaceID = 10
QemuNetworkInterfaceID11 QemuNetworkInterfaceID = 11
QemuNetworkInterfaceID12 QemuNetworkInterfaceID = 12
QemuNetworkInterfaceID13 QemuNetworkInterfaceID = 13
QemuNetworkInterfaceID14 QemuNetworkInterfaceID = 14
QemuNetworkInterfaceID15 QemuNetworkInterfaceID = 15
QemuNetworkInterfaceID16 QemuNetworkInterfaceID = 16
QemuNetworkInterfaceID17 QemuNetworkInterfaceID = 17
QemuNetworkInterfaceID18 QemuNetworkInterfaceID = 18
QemuNetworkInterfaceID19 QemuNetworkInterfaceID = 19
QemuNetworkInterfaceID20 QemuNetworkInterfaceID = 20
QemuNetworkInterfaceID21 QemuNetworkInterfaceID = 21
QemuNetworkInterfaceID22 QemuNetworkInterfaceID = 22
QemuNetworkInterfaceID23 QemuNetworkInterfaceID = 23
QemuNetworkInterfaceID24 QemuNetworkInterfaceID = 24
QemuNetworkInterfaceID25 QemuNetworkInterfaceID = 25
QemuNetworkInterfaceID26 QemuNetworkInterfaceID = 26
QemuNetworkInterfaceID27 QemuNetworkInterfaceID = 27
QemuNetworkInterfaceID28 QemuNetworkInterfaceID = 28
QemuNetworkInterfaceID29 QemuNetworkInterfaceID = 29
QemuNetworkInterfaceID30 QemuNetworkInterfaceID = 30
QemuNetworkInterfaceID31 QemuNetworkInterfaceID = 31
)

func (id QemuNetworkInterfaceID) Validate() error {
if id > 31 {
return fmt.Errorf(QemuNetworkInterfaceID_Error_Invalid)
}
return nil
}
Loading
Loading