From d70d1e8bf71c9c422bb3985bc5849541e38c0036 Mon Sep 17 00:00:00 2001 From: Calle Pettersson Date: Fri, 17 Jan 2020 20:26:44 +0100 Subject: [PATCH 1/2] Bump proxmox-api-go --- go.mod | 2 +- go.sum | 3 + .../Telmate/proxmox-api-go/proxmox/client.go | 129 ++++++++++++++- .../proxmox-api-go/proxmox/config_qemu.go | 148 +++++++++++++++++- vendor/modules.txt | 2 +- 5 files changed, 272 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 8a7289ff5..bc90c868f 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/NaverCloudPlatform/ncloud-sdk-go v0.0.0-20180110055012-c2e73f942591 github.com/PuerkitoBio/goquery v1.5.0 // indirect github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect - github.com/Telmate/proxmox-api-go v0.0.0-20191015171801-b0c2796b9fcf + github.com/Telmate/proxmox-api-go v0.0.0-20200116224409-320525bf3340 github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af // indirect github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190418113227-25233c783f4e github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f diff --git a/go.sum b/go.sum index ba8e78e84..70f5e41d0 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUW github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/Telmate/proxmox-api-go v0.0.0-20191015171801-b0c2796b9fcf h1:rVT2xsBm03Jp0r0yfGm5AMlqp0mZmxTTiNnSrc9S+Hs= github.com/Telmate/proxmox-api-go v0.0.0-20191015171801-b0c2796b9fcf/go.mod h1:OGWyIMJ87/k/GCz8CGiWB2HOXsOVDM6Lpe/nFPkC4IQ= +github.com/Telmate/proxmox-api-go v0.0.0-20200116224409-320525bf3340 h1:bOjy6c07dpipWm11dL92FbtmXGnDywOm2uKzG4CePuY= +github.com/Telmate/proxmox-api-go v0.0.0-20200116224409-320525bf3340/go.mod h1:OGWyIMJ87/k/GCz8CGiWB2HOXsOVDM6Lpe/nFPkC4IQ= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= @@ -693,4 +695,5 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go index 6afc03942..787e7497f 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/client.go @@ -39,10 +39,11 @@ type Client struct { // 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 { - vmId int - node string - pool string - vmType string + vmId int + node string + pool string + vmType string + haState string } func (vmr *VmRef) SetNode(node string) { @@ -75,6 +76,10 @@ func (vmr *VmRef) Pool() string { return vmr.pool } +func (vmr *VmRef) HaState() string { + return vmr.haState +} + func NewVmRef(vmId int) (vmr *VmRef) { vmr = &VmRef{vmId: vmId, node: "", vmType: ""} return @@ -141,6 +146,9 @@ func (c *Client) GetVmInfo(vmr *VmRef) (vmInfo map[string]interface{}, err error if vmInfo["pool"] != nil { vmr.pool = vmInfo["pool"].(string) } + if vmInfo["hastate"] != nil { + vmr.haState = vmInfo["hastate"].(string) + } return } } @@ -160,6 +168,9 @@ func (c *Client) GetVmRefByName(vmName string) (vmr *VmRef, err error) { if vm["pool"] != nil { vmr.pool = vm["pool"].(string) } + if vm["hastate"] != nil { + vmr.haState = vm["hastate"].(string) + } return } } @@ -413,6 +424,23 @@ func (c *Client) DeleteVm(vmr *VmRef) (exitStatus string, err error) { if err != nil { return "", err } + + //Remove HA if required + if vmr.haState != "" { + url := fmt.Sprintf("/cluster/ha/resources/%d", vmr.vmId) + resp, err := c.session.Delete(url, nil, nil) + if err == nil { + taskResponse, err := ResponseJSON(resp) + if err != nil { + return "", err + } + exitStatus, err = c.WaitForCompletion(taskResponse) + if err != nil { + return "", err + } + } + } + url := fmt.Sprintf("/nodes/%s/%s/%d", vmr.node, vmr.vmType, vmr.vmId) var taskResponse map[string]interface{} _, err = c.session.RequestJSON("DELETE", url, nil, nil, nil, &taskResponse) @@ -536,6 +564,22 @@ func (c *Client) SetLxcConfig(vmr *VmRef, vmParams map[string]interface{}) (exit return } +// MigrateNode - Migrate a VM +func (c *Client) MigrateNode(vmr *VmRef, newTargetNode string, online bool) (exitStatus interface{}, err error) { + reqbody := ParamsToBody(map[string]interface{}{"target": newTargetNode, "online": online}) + url := fmt.Sprintf("/nodes/%s/%s/%d/migrate", vmr.node, vmr.vmType, vmr.vmId) + resp, err := c.session.Post(url, nil, nil, &reqbody) + if err == nil { + taskResponse, err := ResponseJSON(resp) + if err != nil { + return nil, err + } + exitStatus, err = c.WaitForCompletion(taskResponse) + return exitStatus, err + } + return nil, err +} + func (c *Client) ResizeQemuDisk(vmr *VmRef, disk string, moreSizeGB int) (exitStatus interface{}, err error) { // PUT //disk:virtio0 @@ -691,8 +735,23 @@ func (c *Client) Upload(node string, storage string, contentType string, filenam req.Header.Add("Content-Type", mimetype) req.Header.Add("Accept", "application/json") - _, err = c.session.Do(req) - return err + resp, err := c.session.Do(req) + if err != nil { + return err + } + + taskResponse, err := ResponseJSON(resp) + if err != nil { + return err + } + exitStatus, err := c.WaitForCompletion(taskResponse) + if err != nil { + return err + } + if exitStatus != exitStatusSuccess { + return fmt.Errorf("Moving file to destination failed: %v", exitStatus) + } + return nil } func createUploadBody(contentType string, filename string, r io.Reader) (io.Reader, string, error) { @@ -787,3 +846,61 @@ func (c *Client) UpdateVMPool(vmr *VmRef, pool string) (exitStatus interface{}, } return } + +func (c *Client) UpdateVMHA(vmr *VmRef, haState string) (exitStatus interface{}, err error) { + // Same hastate + if vmr.haState == haState { + return + } + + //Remove HA + if haState == "" { + url := fmt.Sprintf("/cluster/ha/resources/%d", vmr.vmId) + resp, err := c.session.Delete(url, nil, nil) + if err == nil { + taskResponse, err := ResponseJSON(resp) + if err != nil { + return nil, err + } + exitStatus, err = c.WaitForCompletion(taskResponse) + } + return nil, err + } + + //Activate HA + if vmr.haState == "" { + paramMap := map[string]interface{}{ + "sid": vmr.vmId, + } + reqbody := ParamsToBody(paramMap) + resp, err := c.session.Post("/cluster/ha/resources", nil, nil, &reqbody) + if err == nil { + taskResponse, err := ResponseJSON(resp) + if err != nil { + return nil, err + } + exitStatus, err = c.WaitForCompletion(taskResponse) + + if err != nil { + return nil, err + } + } + } + + //Set wanted state + paramMap := map[string]interface{}{ + "state": haState, + } + reqbody := ParamsToBody(paramMap) + url := fmt.Sprintf("/cluster/ha/resources/%d", vmr.vmId) + resp, err := c.session.Put(url, nil, nil, &reqbody) + if err == nil { + taskResponse, err := ResponseJSON(resp) + if err != nil { + return nil, err + } + exitStatus, err = c.WaitForCompletion(taskResponse) + } + + return +} diff --git a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go index db7b40ccc..10f23641d 100644 --- a/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go +++ b/vendor/github.com/Telmate/proxmox-api-go/proxmox/config_qemu.go @@ -23,15 +23,19 @@ type ( // ConfigQemu - Proxmox API QEMU options type ConfigQemu struct { + VmID int `json:"vmid"` Name string `json:"name"` Description string `json:"desc"` Pool string `json:"pool,omitempty"` + Bios string `json:"bios"` Onboot bool `json:"onboot"` Agent int `json:"agent"` Memory int `json:"memory"` + Balloon int `json:"balloon"` QemuOs string `json:"os"` QemuCores int `json:"cores"` QemuSockets int `json:"sockets"` + QemuVcpus int `json:"vcpus"` QemuCpu string `json:"cpu"` QemuNuma bool `json:"numa"` Hotplug string `json:"hotplug"` @@ -41,8 +45,10 @@ type ConfigQemu struct { BootDisk string `json:"bootdisk,omitempty"` Scsihw string `json:"scsihw,omitempty"` QemuDisks QemuDevices `json:"disk"` + QemuVga QemuDevice `json:"vga,omitempty"` QemuNetworks QemuDevices `json:"network"` QemuSerials QemuDevices `json:"serial,omitempty"` + HaState string `json:"hastate,omitempty"` // Deprecated single disk. DiskSize float64 `json:"diskGB"` @@ -93,6 +99,19 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) { "boot": config.Boot, "description": config.Description, } + + if config.Bios != "" { + params["bios"] = config.Bios + } + + if config.Balloon >= 1 { + params["balloon"] = config.Balloon + } + + if config.QemuVcpus >= 1 { + params["vcpus"] = config.QemuVcpus + } + if vmr.pool != "" { params["pool"] = vmr.pool } @@ -108,6 +127,13 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) { // Create disks config. config.CreateQemuDisksParams(vmr.vmId, params, false) + // Create vga config. + vgaParam := QemuDeviceParam{} + vgaParam = vgaParam.createDeviceParam(config.QemuVga, nil) + if len(vgaParam) > 0 { + params["vga"] = strings.Join(vgaParam, ",") + } + // Create networks config. config.CreateQemuNetworksParams(vmr.vmId, params) @@ -118,6 +144,9 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) { if err != nil { return fmt.Errorf("Error creating VM: %v, error status: %s (params: %v)", err, exitStatus, params) } + + client.UpdateVMHA(vmr, config.HaState) + return } @@ -175,6 +204,7 @@ func (config ConfigQemu) CloneVm(sourceVmr *VmRef, vmr *VmRef, client *Client) ( if err != nil { return } + return config.UpdateConfig(vmr, client) } @@ -193,6 +223,25 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) { "boot": config.Boot, } + //Array to list deleted parameters + deleteParams := []string{} + + if config.Bios != "" { + configParams["bios"] = config.Bios + } + + if config.Balloon >= 1 { + configParams["balloon"] = config.Balloon + } else { + deleteParams = append(deleteParams, "balloon") + } + + if config.QemuVcpus >= 1 { + configParams["vcpus"] = config.QemuVcpus + } else { + deleteParams = append(deleteParams, "vcpus") + } + if config.BootDisk != "" { configParams["bootdisk"] = config.BootDisk } @@ -202,11 +251,31 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) { } // Create disks config. - config.CreateQemuDisksParams(vmr.vmId, configParams, true) + configParamsDisk := map[string]interface{} { + "vmid": vmr.vmId, + } + config.CreateQemuDisksParams(vmr.vmId, configParamsDisk, false) + client.createVMDisks(vmr.node, configParamsDisk) + //Copy the disks to the global configParams + for key, value := range configParamsDisk { + //vmid is only required in createVMDisks + if key != "vmid" { + configParams[key] = value + } + } // Create networks config. config.CreateQemuNetworksParams(vmr.vmId, configParams) + // Create vga config. + vgaParam := QemuDeviceParam{} + vgaParam = vgaParam.createDeviceParam(config.QemuVga, nil) + if len(vgaParam) > 0 { + configParams["vga"] = strings.Join(vgaParam, ",") + } else { + deleteParams = append(deleteParams, "vga") + } + // Create serial interfaces config.CreateQemuSerialsParams(vmr.vmId, configParams) @@ -242,12 +311,19 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) { if config.Ipconfig2 != "" { configParams["ipconfig2"] = config.Ipconfig2 } + + if len(deleteParams) > 0 { + configParams["delete"] = strings.Join(deleteParams, ", ") + } + _, err = client.SetVmConfig(vmr, configParams) if err != nil { - log.Fatal(err) + log.Print(err) return err } + client.UpdateVMHA(vmr, config.HaState) + _, err = client.UpdateVMPool(vmr, config.Pool) return err @@ -312,6 +388,10 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e if _, isSet := vmConfig["description"]; isSet { description = vmConfig["description"].(string) } + bios := "seabios" + if _, isSet := vmConfig["bios"]; isSet { + bios = vmConfig["bios"].(string) + } onboot := true if _, isSet := vmConfig["onboot"]; isSet { onboot = Itob(int(vmConfig["onboot"].(float64))) @@ -335,10 +415,18 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e if _, isSet := vmConfig["memory"]; isSet { memory = vmConfig["memory"].(float64) } + balloon := 0.0 + if _, isSet := vmConfig["balloon"]; isSet { + balloon = vmConfig["balloon"].(float64) + } cores := 1.0 if _, isSet := vmConfig["cores"]; isSet { cores = vmConfig["cores"].(float64) } + vcpus := 0.0 + if _, isSet := vmConfig["vcpus"]; isSet { + vcpus = vmConfig["vcpus"].(float64) + } sockets := 1.0 if _, isSet := vmConfig["sockets"]; isSet { sockets = vmConfig["sockets"].(float64) @@ -369,9 +457,15 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e if _, isSet := vmConfig["scsihw"]; isSet { scsihw = vmConfig["scsihw"].(string) } + hastate := "" + if _, isSet := vmConfig["hastate"]; isSet { + hastate = vmConfig["hastate"].(string) + } + config = &ConfigQemu{ Name: name, Description: strings.TrimSpace(description), + Bios: bios, Onboot: onboot, Agent: agent, QemuOs: ostype, @@ -385,10 +479,19 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e Boot: boot, BootDisk: bootdisk, Scsihw: scsihw, + HaState: hastate, QemuDisks: QemuDevices{}, + QemuVga: QemuDevice{}, QemuNetworks: QemuDevices{}, QemuSerials: QemuDevices{}, } + + if balloon >= 1 { + config.Balloon = int(balloon); + } + if vcpus >= 1 { + config.QemuVcpus = int(vcpus); + } if vmConfig["ide2"] != nil { isoMatch := rxIso.FindStringSubmatch(vmConfig["ide2"].(string)) @@ -459,6 +562,16 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e } } + //Display + if vga, isSet := vmConfig["vga"]; isSet { + vgaList := strings.Split(vga.(string), ",") + vgaMap := QemuDevice{} + vgaMap.readDeviceConfig(vgaList) + if len(vgaMap) > 0 { + config.QemuVga = vgaMap + } + } + // Add networks. nicNames := []string{} @@ -776,9 +889,9 @@ func (c ConfigQemu) CreateQemuDisksParams( // Disk name. var diskFile string - // Currently ZFS local, LVM, Ceph RBD, and Directory are considered. + // Currently ZFS local, LVM, Ceph RBD, CephFS and Directory are considered. // Other formats are not verified, but could be added if they're needed. - rxStorageTypes := `(zfspool|lvm|rbd)` + rxStorageTypes := `(zfspool|lvm|rbd|cephfs)` storageType := diskConfMap["storage_type"].(string) if matched, _ := regexp.MatchString(rxStorageTypes, storageType); matched { diskFile = fmt.Sprintf("file=%v:vm-%v-disk-%v", diskConfMap["storage"], vmID, diskID) @@ -865,3 +978,30 @@ func (c ConfigQemu) CreateQemuSerialsParams( return nil } + +// NextId - Get next free VMID +func (c *Client) NextId() (id int, err error) { + var data map[string]interface{} + _, err = c.session.GetJSON("/cluster/nextid", nil, nil, &data) + if err != nil { + return -1, err + } + if data["data"] == nil || data["errors"] != nil { + return -1, fmt.Errorf(data["errors"].(string)) + } + + i, err := strconv.Atoi(data["data"].(string)) + if err != nil { + return -1, err + } + return i, nil +} + +// VMIdExists - If you pass an VMID that exists it will raise an error otherwise it will return the vmID +func (c *Client) VMIdExists(vmID int) (id int, err error) { + _, err = c.session.Get(fmt.Sprintf("/cluster/nextid?vmid=%d", vmID), nil, nil) + if err != nil { + return -1, err + } + return vmID, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 8167e4b34..7d4a24d16 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -63,7 +63,7 @@ github.com/NaverCloudPlatform/ncloud-sdk-go/sdk github.com/PuerkitoBio/goquery # github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d github.com/StackExchange/wmi -# github.com/Telmate/proxmox-api-go v0.0.0-20191015171801-b0c2796b9fcf +# github.com/Telmate/proxmox-api-go v0.0.0-20200116224409-320525bf3340 github.com/Telmate/proxmox-api-go/proxmox # github.com/agext/levenshtein v1.2.1 github.com/agext/levenshtein From 8e4c165173d8596a37a5950f76b70b1b1b938836 Mon Sep 17 00:00:00 2001 From: Calle Pettersson Date: Sat, 4 Jan 2020 22:50:35 +0100 Subject: [PATCH 2/2] Proxmox upload ISO --- builder/proxmox/builder.go | 12 ++ builder/proxmox/config.go | 28 +++- builder/proxmox/config.hcl2spec.go | 16 +++ builder/proxmox/step_start_vm.go | 4 +- builder/proxmox/step_upload_iso.go | 66 +++++++++ builder/proxmox/step_upload_iso_test.go | 137 +++++++++++++++++++ builder/proxmox/testdata/test.iso | Bin 0 -> 356352 bytes website/source/docs/builders/proxmox.html.md | 16 ++- 8 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 builder/proxmox/step_upload_iso.go create mode 100644 builder/proxmox/step_upload_iso_test.go create mode 100644 builder/proxmox/testdata/test.iso diff --git a/builder/proxmox/builder.go b/builder/proxmox/builder.go index e29645114..4ca1c2d07 100644 --- a/builder/proxmox/builder.go +++ b/builder/proxmox/builder.go @@ -38,6 +38,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { return nil, nil, nil } +const downloadPathKey = "downloaded_iso_path" + func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { var err error tlsConfig := &tls.Config{ @@ -62,6 +64,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack // Build the steps steps := []multistep.Step{ + &common.StepDownload{ + Checksum: b.config.ISOChecksum, + ChecksumType: b.config.ISOChecksumType, + Description: "ISO", + Extension: b.config.TargetExtension, + ResultKey: downloadPathKey, + TargetPath: b.config.TargetPath, + Url: b.config.ISOUrls, + }, + &stepUploadISO{}, &stepStartVM{}, &common.StepHTTPServer{ HTTPDir: b.config.HTTPDir, diff --git a/builder/proxmox/config.go b/builder/proxmox/config.go index 00f082d07..0dd3cb523 100644 --- a/builder/proxmox/config.go +++ b/builder/proxmox/config.go @@ -23,6 +23,7 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"` + common.ISOConfig `mapstructure:",squash"` bootcommand.BootConfig `mapstructure:",squash"` BootKeyInterval time.Duration `mapstructure:"boot_key_interval"` Comm communicator.Config `mapstructure:",squash"` @@ -46,6 +47,7 @@ type Config struct { NICs []nicConfig `mapstructure:"network_adapters"` Disks []diskConfig `mapstructure:"disks"` ISOFile string `mapstructure:"iso_file"` + ISOStoragePool string `mapstructure:"iso_storage_pool"` Agent bool `mapstructure:"qemu_agent"` SCSIController string `mapstructure:"scsi_controller"` @@ -53,6 +55,8 @@ type Config struct { TemplateDescription string `mapstructure:"template_description"` UnmountISO bool `mapstructure:"unmount_iso"` + shouldUploadISO bool + ctx interpolate.Context } @@ -91,6 +95,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { } var errs *packer.MultiError + warnings := make([]string, 0) // Defaults if c.ProxmoxURLRaw == "" { @@ -172,6 +177,26 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) + // Check ISO config + // Either a pre-uploaded ISO should be referenced in iso_file, OR a URL + // (possibly to a local file) to an ISO file that will be downloaded and + // then uploaded to Proxmox. + if c.ISOFile != "" { + c.shouldUploadISO = false + } else { + isoWarnings, isoErrors := c.ISOConfig.Prepare(&c.ctx) + errs = packer.MultiErrorAppend(errs, isoErrors...) + warnings = append(warnings, isoWarnings...) + c.shouldUploadISO = true + } + + if (c.ISOFile == "" && len(c.ISOConfig.ISOUrls) == 0) || (c.ISOFile != "" && len(c.ISOConfig.ISOUrls) != 0) { + errs = packer.MultiErrorAppend(errs, errors.New("either iso_file or iso_url, but not both, must be specified")) + } + if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" { + errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified")) + } + // Required configurations that will display errors if not set if c.Username == "" { errs = packer.MultiErrorAppend(errs, errors.New("username must be specified")) @@ -185,9 +210,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { if c.proxmoxURL, err = url.Parse(c.ProxmoxURLRaw); err != nil { errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("Could not parse proxmox_url: %s", err))) } - if c.ISOFile == "" { - errs = packer.MultiErrorAppend(errs, errors.New("iso_file must be specified")) - } if c.Node == "" { errs = packer.MultiErrorAppend(errs, errors.New("node must be specified")) } diff --git a/builder/proxmox/config.hcl2spec.go b/builder/proxmox/config.hcl2spec.go index e9c208c12..bdcf7d018 100644 --- a/builder/proxmox/config.hcl2spec.go +++ b/builder/proxmox/config.hcl2spec.go @@ -19,6 +19,13 @@ type FlatConfig struct { HTTPDir *string `mapstructure:"http_directory" cty:"http_directory"` HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min"` HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max"` + ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum"` + ISOChecksumURL *string `mapstructure:"iso_checksum_url" cty:"iso_checksum_url"` + ISOChecksumType *string `mapstructure:"iso_checksum_type" cty:"iso_checksum_type"` + RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url"` + ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls"` + TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path"` + TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension"` BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command"` @@ -79,6 +86,7 @@ type FlatConfig struct { NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters"` Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks"` ISOFile *string `mapstructure:"iso_file" cty:"iso_file"` + ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool"` Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent"` SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller"` TemplateName *string `mapstructure:"template_name" cty:"template_name"` @@ -108,6 +116,13 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false}, "http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false}, "http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false}, + "iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false}, + "iso_checksum_url": &hcldec.AttrSpec{Name: "iso_checksum_url", Type: cty.String, Required: false}, + "iso_checksum_type": &hcldec.AttrSpec{Name: "iso_checksum_type", Type: cty.String, Required: false}, + "iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false}, + "iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false}, + "iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false}, + "iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false}, "boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false}, "boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false}, "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, @@ -168,6 +183,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*FlatnicConfig)(nil).HCL2Spec())}, "disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*FlatdiskConfig)(nil).HCL2Spec())}, "iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false}, + "iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false}, "qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false}, "scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false}, "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, diff --git a/builder/proxmox/step_start_vm.go b/builder/proxmox/step_start_vm.go index 378b689ea..453798917 100644 --- a/builder/proxmox/step_start_vm.go +++ b/builder/proxmox/step_start_vm.go @@ -26,6 +26,8 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist agent = 0 } + isoFile := state.Get("iso_file").(string) + ui.Say("Creating VM") config := proxmox.ConfigQemu{ Name: c.VMName, @@ -37,7 +39,7 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist QemuCores: c.Cores, QemuSockets: c.Sockets, QemuOs: c.OS, - QemuIso: c.ISOFile, + QemuIso: isoFile, QemuNetworks: generateProxmoxNetworkAdapters(c.NICs), QemuDisks: generateProxmoxDisks(c.Disks), Scsihw: c.SCSIController, diff --git a/builder/proxmox/step_upload_iso.go b/builder/proxmox/step_upload_iso.go new file mode 100644 index 000000000..deba1672f --- /dev/null +++ b/builder/proxmox/step_upload_iso.go @@ -0,0 +1,66 @@ +package proxmox + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// stepUploadISO uploads an ISO file to Proxmox so we can boot from it +type stepUploadISO struct{} + +type uploader interface { + Upload(node string, storage string, contentType string, filename string, file io.Reader) error +} + +var _ uploader = &proxmox.Client{} + +func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("proxmoxClient").(uploader) + c := state.Get("config").(*Config) + + if !c.shouldUploadISO { + state.Put("iso_file", c.ISOFile) + return multistep.ActionContinue + } + + p := state.Get(downloadPathKey).(string) + if p == "" { + err := fmt.Errorf("Path to downloaded ISO was empty") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // All failure cases in resolving the symlink are caught anyway in os.Open + isoPath, _ := filepath.EvalSymlinks(p) + r, err := os.Open(isoPath) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + filename := filepath.Base(c.ISOUrls[0]) + err = client.Upload(c.Node, c.ISOStoragePool, "iso", filename, r) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + isoStoragePath := fmt.Sprintf("%s:iso/%s", c.ISOStoragePool, filename) + state.Put("iso_file", isoStoragePath) + + return multistep.ActionContinue +} + +func (s *stepUploadISO) Cleanup(state multistep.StateBag) { +} diff --git a/builder/proxmox/step_upload_iso_test.go b/builder/proxmox/step_upload_iso_test.go new file mode 100644 index 000000000..a009d6686 --- /dev/null +++ b/builder/proxmox/step_upload_iso_test.go @@ -0,0 +1,137 @@ +package proxmox + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type uploaderMock struct { + fail bool + wasCalled bool +} + +func (m *uploaderMock) Upload(node string, storage string, contentType string, filename string, file io.Reader) error { + m.wasCalled = true + if m.fail { + return fmt.Errorf("Testing induced failure") + } + return nil +} + +var _ uploader = &uploaderMock{} + +func TestUploadISO(t *testing.T) { + cs := []struct { + name string + builderConfig *Config + downloadPath string + failUpload bool + + expectError bool + expectUploadCalled bool + expectedISOPath string + expectedAction multistep.StepAction + }{ + { + name: "should not call upload unless configured to do so", + builderConfig: &Config{shouldUploadISO: false, ISOFile: "local:iso/some-file"}, + + expectUploadCalled: false, + expectedISOPath: "local:iso/some-file", + expectedAction: multistep.ActionContinue, + }, + { + name: "success should continue", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/test.iso", + + expectedISOPath: "local:iso/some-file.iso", + expectUploadCalled: true, + expectedAction: multistep.ActionContinue, + }, + { + name: "failing upload should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/test.iso", + failUpload: true, + + expectError: true, + expectUploadCalled: true, + expectedAction: multistep.ActionHalt, + }, + { + name: "downloader: state misconfiguration should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + + expectError: true, + expectUploadCalled: false, + expectedAction: multistep.ActionHalt, + }, + { + name: "downloader: file unreadable should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/non-existent.iso", + + expectError: true, + expectUploadCalled: false, + expectedAction: multistep.ActionHalt, + }, + } + + for _, c := range cs { + t.Run(c.name, func(t *testing.T) { + m := &uploaderMock{fail: c.failUpload} + + state := new(multistep.BasicStateBag) + state.Put("ui", packer.TestUi(t)) + state.Put("config", c.builderConfig) + state.Put(downloadPathKey, c.downloadPath) + state.Put("proxmoxClient", m) + + step := stepUploadISO{} + action := step.Run(context.TODO(), state) + step.Cleanup(state) + + if action != c.expectedAction { + t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) + } + if m.wasCalled != c.expectUploadCalled { + t.Errorf("Expected mock to be called: %v, got: %v", c.expectUploadCalled, m.wasCalled) + } + err, gotError := state.GetOk("error") + if gotError != c.expectError { + t.Errorf("Expected error state to be: %v, got: %v", c.expectError, gotError) + } + if err == nil { + if isoPath := state.Get("iso_file"); isoPath != c.expectedISOPath { + if _, ok := isoPath.(string); !ok { + isoPath = "" + } + t.Errorf("Expected state iso_path to be %q, got %q", c.expectedISOPath, isoPath) + } + } + }) + } +} diff --git a/builder/proxmox/testdata/test.iso b/builder/proxmox/testdata/test.iso new file mode 100644 index 0000000000000000000000000000000000000000..6e28c7af9ce82aa71073c50abb59b2d72be2f93b GIT binary patch literal 356352 zcmeI(-D(;^6ae7aVyT2)l$K&IhhC+Hnl%lkk}FwPv+1hERcM~1eX71Vm)`SkwbxRd;~>1>-_cE3$RNSZ6O+u>w0eDdb5>}}J1A3B>% zC$yVRJB*WNQtMeWv6&ymg9=RYSy9c4>2Q+Ax)TTeetLF2uHv{T^Xj%*=F^xh^5Js6 zh^N_y*c%K!$Gm$xU(F_?VVUoem!Hnlv>$J}-}2$2n$ON|x>a^vl=ZHhtZSO#rsgu% z8}VzgjLUrZukCmkmGiV-LNC4OeM!@cy4%`Iq<=-&t4P9O*5+NGiooVG%`1UNAn<$p z{r?D-VhIEY5FkK+009C72oNAZfWUzSlJzt8`f0i#K!5-N0t5&UAV7cs0RjY$R$%?h zztjAkP7okKfB*pk1PBlyK!5-N0>>(_f5z@-(s~^}-kq`gZ$)w%-feicO$a|;Ca<1f zwd!nhPVdERRhHeW-mx0GF9HMz5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF R5FkK+009C72oN~Dz#kgOz7qfd literal 0 HcmV?d00001 diff --git a/website/source/docs/builders/proxmox.html.md b/website/source/docs/builders/proxmox.html.md index 1704fcabb..4d914b480 100644 --- a/website/source/docs/builders/proxmox.html.md +++ b/website/source/docs/builders/proxmox.html.md @@ -50,7 +50,21 @@ builder. - `iso_file` (string) - Path to the ISO file to boot from, expressed as a proxmox datastore path, for example - `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso` + `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso`. + Either `iso_file` OR `iso_url` must be specifed. + +- `iso_url` (string) - URL to an ISO file to upload to Proxmox, and then + boot from. Either `iso_file` OR `iso_url` must be specifed. + +- `iso_storage_pool` (string) - Proxmox storage pool onto which to upload + the ISO file. + +- `iso_checksum` (string) - Checksum of the ISO file. + +- `iso_checksum_type` (string) - Type of the checksum. Can be md5, sha1, + sha256, sha512 or none. Corruption of large files, such as ISOs, can occur + during transfer from time to time. As such, setting this to none is not + recommended. ### Optional: - `insecure_skip_tls_verify` (bool) - Skip validating the certificate.