Merge pull request #8624 from carlpett/proxmox-upload-iso

Proxmox upload iso
This commit is contained in:
Megan Marsh 2020-01-17 14:50:16 -08:00 committed by GitHub
commit db275ade8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 546 additions and 17 deletions

View File

@ -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,

View File

@ -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"))
}

View File

@ -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},

View File

@ -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,

View File

@ -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) {
}

View File

@ -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)
}
}
})
}
}

BIN
builder/proxmox/testdata/test.iso vendored Normal file

Binary file not shown.

2
go.mod
View File

@ -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

3
go.sum
View File

@ -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=

View File

@ -43,6 +43,7 @@ type VmRef struct {
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,10 +735,25 @@ 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)
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) {
var buf bytes.Buffer
w := multipart.NewWriter(&buf)
@ -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
}

View File

@ -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,11 +479,20 @@ 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))
config.QemuIso = isoMatch[1]
@ -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
}

2
vendor/modules.txt vendored
View File

@ -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

View File

@ -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.