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 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) { func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
var err error var err error
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -62,6 +64,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
// Build the steps // Build the steps
steps := []multistep.Step{ 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{}, &stepStartVM{},
&common.StepHTTPServer{ &common.StepHTTPServer{
HTTPDir: b.config.HTTPDir, HTTPDir: b.config.HTTPDir,

View File

@ -23,6 +23,7 @@ import (
type Config struct { type Config struct {
common.PackerConfig `mapstructure:",squash"` common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"` bootcommand.BootConfig `mapstructure:",squash"`
BootKeyInterval time.Duration `mapstructure:"boot_key_interval"` BootKeyInterval time.Duration `mapstructure:"boot_key_interval"`
Comm communicator.Config `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"`
@ -46,6 +47,7 @@ type Config struct {
NICs []nicConfig `mapstructure:"network_adapters"` NICs []nicConfig `mapstructure:"network_adapters"`
Disks []diskConfig `mapstructure:"disks"` Disks []diskConfig `mapstructure:"disks"`
ISOFile string `mapstructure:"iso_file"` ISOFile string `mapstructure:"iso_file"`
ISOStoragePool string `mapstructure:"iso_storage_pool"`
Agent bool `mapstructure:"qemu_agent"` Agent bool `mapstructure:"qemu_agent"`
SCSIController string `mapstructure:"scsi_controller"` SCSIController string `mapstructure:"scsi_controller"`
@ -53,6 +55,8 @@ type Config struct {
TemplateDescription string `mapstructure:"template_description"` TemplateDescription string `mapstructure:"template_description"`
UnmountISO bool `mapstructure:"unmount_iso"` UnmountISO bool `mapstructure:"unmount_iso"`
shouldUploadISO bool
ctx interpolate.Context ctx interpolate.Context
} }
@ -91,6 +95,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
} }
var errs *packer.MultiError var errs *packer.MultiError
warnings := make([]string, 0)
// Defaults // Defaults
if c.ProxmoxURLRaw == "" { 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.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.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 // Required configurations that will display errors if not set
if c.Username == "" { if c.Username == "" {
errs = packer.MultiErrorAppend(errs, errors.New("username must be specified")) 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 { 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))) 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 == "" { if c.Node == "" {
errs = packer.MultiErrorAppend(errs, errors.New("node must be specified")) 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"` HTTPDir *string `mapstructure:"http_directory" cty:"http_directory"`
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min"` HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min"`
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max"` 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"` BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command"`
@ -79,6 +86,7 @@ type FlatConfig struct {
NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters"` NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters"`
Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks"` Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks"`
ISOFile *string `mapstructure:"iso_file" cty:"iso_file"` 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"` Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent"`
SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller"` SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller"`
TemplateName *string `mapstructure:"template_name" cty:"template_name"` 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_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_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}, "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_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_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}, "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())}, "network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*FlatnicConfig)(nil).HCL2Spec())},
"disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*FlatdiskConfig)(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_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}, "qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false},
"scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, 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}, "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 agent = 0
} }
isoFile := state.Get("iso_file").(string)
ui.Say("Creating VM") ui.Say("Creating VM")
config := proxmox.ConfigQemu{ config := proxmox.ConfigQemu{
Name: c.VMName, Name: c.VMName,
@ -37,7 +39,7 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist
QemuCores: c.Cores, QemuCores: c.Cores,
QemuSockets: c.Sockets, QemuSockets: c.Sockets,
QemuOs: c.OS, QemuOs: c.OS,
QemuIso: c.ISOFile, QemuIso: isoFile,
QemuNetworks: generateProxmoxNetworkAdapters(c.NICs), QemuNetworks: generateProxmoxNetworkAdapters(c.NICs),
QemuDisks: generateProxmoxDisks(c.Disks), QemuDisks: generateProxmoxDisks(c.Disks),
Scsihw: c.SCSIController, 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/NaverCloudPlatform/ncloud-sdk-go v0.0.0-20180110055012-c2e73f942591
github.com/PuerkitoBio/goquery v1.5.0 // indirect github.com/PuerkitoBio/goquery v1.5.0 // indirect
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // 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/abdullin/seq v0.0.0-20160510034733-d5467c17e7af // indirect
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190418113227-25233c783f4e github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190418113227-25233c783f4e
github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20170113022742-e6dbea820a9f 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/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 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-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 h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14=
github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= 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.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 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 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= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=

View File

@ -43,6 +43,7 @@ type VmRef struct {
node string node string
pool string pool string
vmType string vmType string
haState string
} }
func (vmr *VmRef) SetNode(node string) { func (vmr *VmRef) SetNode(node string) {
@ -75,6 +76,10 @@ func (vmr *VmRef) Pool() string {
return vmr.pool return vmr.pool
} }
func (vmr *VmRef) HaState() string {
return vmr.haState
}
func NewVmRef(vmId int) (vmr *VmRef) { func NewVmRef(vmId int) (vmr *VmRef) {
vmr = &VmRef{vmId: vmId, node: "", vmType: ""} vmr = &VmRef{vmId: vmId, node: "", vmType: ""}
return return
@ -141,6 +146,9 @@ func (c *Client) GetVmInfo(vmr *VmRef) (vmInfo map[string]interface{}, err error
if vmInfo["pool"] != nil { if vmInfo["pool"] != nil {
vmr.pool = vmInfo["pool"].(string) vmr.pool = vmInfo["pool"].(string)
} }
if vmInfo["hastate"] != nil {
vmr.haState = vmInfo["hastate"].(string)
}
return return
} }
} }
@ -160,6 +168,9 @@ func (c *Client) GetVmRefByName(vmName string) (vmr *VmRef, err error) {
if vm["pool"] != nil { if vm["pool"] != nil {
vmr.pool = vm["pool"].(string) vmr.pool = vm["pool"].(string)
} }
if vm["hastate"] != nil {
vmr.haState = vm["hastate"].(string)
}
return return
} }
} }
@ -413,6 +424,23 @@ func (c *Client) DeleteVm(vmr *VmRef) (exitStatus string, err error) {
if err != nil { if err != nil {
return "", err 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) url := fmt.Sprintf("/nodes/%s/%s/%d", vmr.node, vmr.vmType, vmr.vmId)
var taskResponse map[string]interface{} var taskResponse map[string]interface{}
_, err = c.session.RequestJSON("DELETE", url, nil, nil, nil, &taskResponse) _, 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 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) { func (c *Client) ResizeQemuDisk(vmr *VmRef, disk string, moreSizeGB int) (exitStatus interface{}, err error) {
// PUT // PUT
//disk:virtio0 //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("Content-Type", mimetype)
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")
_, err = c.session.Do(req) resp, err := c.session.Do(req)
if err != nil {
return err 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) { 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 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 // ConfigQemu - Proxmox API QEMU options
type ConfigQemu struct { type ConfigQemu struct {
VmID int `json:"vmid"`
Name string `json:"name"` Name string `json:"name"`
Description string `json:"desc"` Description string `json:"desc"`
Pool string `json:"pool,omitempty"` Pool string `json:"pool,omitempty"`
Bios string `json:"bios"`
Onboot bool `json:"onboot"` Onboot bool `json:"onboot"`
Agent int `json:"agent"` Agent int `json:"agent"`
Memory int `json:"memory"` Memory int `json:"memory"`
Balloon int `json:"balloon"`
QemuOs string `json:"os"` QemuOs string `json:"os"`
QemuCores int `json:"cores"` QemuCores int `json:"cores"`
QemuSockets int `json:"sockets"` QemuSockets int `json:"sockets"`
QemuVcpus int `json:"vcpus"`
QemuCpu string `json:"cpu"` QemuCpu string `json:"cpu"`
QemuNuma bool `json:"numa"` QemuNuma bool `json:"numa"`
Hotplug string `json:"hotplug"` Hotplug string `json:"hotplug"`
@ -41,8 +45,10 @@ type ConfigQemu struct {
BootDisk string `json:"bootdisk,omitempty"` BootDisk string `json:"bootdisk,omitempty"`
Scsihw string `json:"scsihw,omitempty"` Scsihw string `json:"scsihw,omitempty"`
QemuDisks QemuDevices `json:"disk"` QemuDisks QemuDevices `json:"disk"`
QemuVga QemuDevice `json:"vga,omitempty"`
QemuNetworks QemuDevices `json:"network"` QemuNetworks QemuDevices `json:"network"`
QemuSerials QemuDevices `json:"serial,omitempty"` QemuSerials QemuDevices `json:"serial,omitempty"`
HaState string `json:"hastate,omitempty"`
// Deprecated single disk. // Deprecated single disk.
DiskSize float64 `json:"diskGB"` DiskSize float64 `json:"diskGB"`
@ -93,6 +99,19 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
"boot": config.Boot, "boot": config.Boot,
"description": config.Description, "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 != "" { if vmr.pool != "" {
params["pool"] = vmr.pool params["pool"] = vmr.pool
} }
@ -108,6 +127,13 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
// Create disks config. // Create disks config.
config.CreateQemuDisksParams(vmr.vmId, params, false) 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. // Create networks config.
config.CreateQemuNetworksParams(vmr.vmId, params) config.CreateQemuNetworksParams(vmr.vmId, params)
@ -118,6 +144,9 @@ func (config ConfigQemu) CreateVm(vmr *VmRef, client *Client) (err error) {
if err != nil { if err != nil {
return fmt.Errorf("Error creating VM: %v, error status: %s (params: %v)", err, exitStatus, params) return fmt.Errorf("Error creating VM: %v, error status: %s (params: %v)", err, exitStatus, params)
} }
client.UpdateVMHA(vmr, config.HaState)
return return
} }
@ -175,6 +204,7 @@ func (config ConfigQemu) CloneVm(sourceVmr *VmRef, vmr *VmRef, client *Client) (
if err != nil { if err != nil {
return return
} }
return config.UpdateConfig(vmr, client) return config.UpdateConfig(vmr, client)
} }
@ -193,6 +223,25 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
"boot": config.Boot, "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 != "" { if config.BootDisk != "" {
configParams["bootdisk"] = config.BootDisk configParams["bootdisk"] = config.BootDisk
} }
@ -202,11 +251,31 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
} }
// Create disks config. // 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. // Create networks config.
config.CreateQemuNetworksParams(vmr.vmId, configParams) 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 // Create serial interfaces
config.CreateQemuSerialsParams(vmr.vmId, configParams) config.CreateQemuSerialsParams(vmr.vmId, configParams)
@ -242,12 +311,19 @@ func (config ConfigQemu) UpdateConfig(vmr *VmRef, client *Client) (err error) {
if config.Ipconfig2 != "" { if config.Ipconfig2 != "" {
configParams["ipconfig2"] = config.Ipconfig2 configParams["ipconfig2"] = config.Ipconfig2
} }
if len(deleteParams) > 0 {
configParams["delete"] = strings.Join(deleteParams, ", ")
}
_, err = client.SetVmConfig(vmr, configParams) _, err = client.SetVmConfig(vmr, configParams)
if err != nil { if err != nil {
log.Fatal(err) log.Print(err)
return err return err
} }
client.UpdateVMHA(vmr, config.HaState)
_, err = client.UpdateVMPool(vmr, config.Pool) _, err = client.UpdateVMPool(vmr, config.Pool)
return err return err
@ -312,6 +388,10 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
if _, isSet := vmConfig["description"]; isSet { if _, isSet := vmConfig["description"]; isSet {
description = vmConfig["description"].(string) description = vmConfig["description"].(string)
} }
bios := "seabios"
if _, isSet := vmConfig["bios"]; isSet {
bios = vmConfig["bios"].(string)
}
onboot := true onboot := true
if _, isSet := vmConfig["onboot"]; isSet { if _, isSet := vmConfig["onboot"]; isSet {
onboot = Itob(int(vmConfig["onboot"].(float64))) 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 { if _, isSet := vmConfig["memory"]; isSet {
memory = vmConfig["memory"].(float64) memory = vmConfig["memory"].(float64)
} }
balloon := 0.0
if _, isSet := vmConfig["balloon"]; isSet {
balloon = vmConfig["balloon"].(float64)
}
cores := 1.0 cores := 1.0
if _, isSet := vmConfig["cores"]; isSet { if _, isSet := vmConfig["cores"]; isSet {
cores = vmConfig["cores"].(float64) cores = vmConfig["cores"].(float64)
} }
vcpus := 0.0
if _, isSet := vmConfig["vcpus"]; isSet {
vcpus = vmConfig["vcpus"].(float64)
}
sockets := 1.0 sockets := 1.0
if _, isSet := vmConfig["sockets"]; isSet { if _, isSet := vmConfig["sockets"]; isSet {
sockets = vmConfig["sockets"].(float64) sockets = vmConfig["sockets"].(float64)
@ -369,9 +457,15 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
if _, isSet := vmConfig["scsihw"]; isSet { if _, isSet := vmConfig["scsihw"]; isSet {
scsihw = vmConfig["scsihw"].(string) scsihw = vmConfig["scsihw"].(string)
} }
hastate := ""
if _, isSet := vmConfig["hastate"]; isSet {
hastate = vmConfig["hastate"].(string)
}
config = &ConfigQemu{ config = &ConfigQemu{
Name: name, Name: name,
Description: strings.TrimSpace(description), Description: strings.TrimSpace(description),
Bios: bios,
Onboot: onboot, Onboot: onboot,
Agent: agent, Agent: agent,
QemuOs: ostype, QemuOs: ostype,
@ -385,11 +479,20 @@ func NewConfigQemuFromApi(vmr *VmRef, client *Client) (config *ConfigQemu, err e
Boot: boot, Boot: boot,
BootDisk: bootdisk, BootDisk: bootdisk,
Scsihw: scsihw, Scsihw: scsihw,
HaState: hastate,
QemuDisks: QemuDevices{}, QemuDisks: QemuDevices{},
QemuVga: QemuDevice{},
QemuNetworks: QemuDevices{}, QemuNetworks: QemuDevices{},
QemuSerials: QemuDevices{}, QemuSerials: QemuDevices{},
} }
if balloon >= 1 {
config.Balloon = int(balloon);
}
if vcpus >= 1 {
config.QemuVcpus = int(vcpus);
}
if vmConfig["ide2"] != nil { if vmConfig["ide2"] != nil {
isoMatch := rxIso.FindStringSubmatch(vmConfig["ide2"].(string)) isoMatch := rxIso.FindStringSubmatch(vmConfig["ide2"].(string))
config.QemuIso = isoMatch[1] 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. // Add networks.
nicNames := []string{} nicNames := []string{}
@ -776,9 +889,9 @@ func (c ConfigQemu) CreateQemuDisksParams(
// Disk name. // Disk name.
var diskFile string 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. // 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) storageType := diskConfMap["storage_type"].(string)
if matched, _ := regexp.MatchString(rxStorageTypes, storageType); matched { if matched, _ := regexp.MatchString(rxStorageTypes, storageType); matched {
diskFile = fmt.Sprintf("file=%v:vm-%v-disk-%v", diskConfMap["storage"], vmID, diskID) diskFile = fmt.Sprintf("file=%v:vm-%v-disk-%v", diskConfMap["storage"], vmID, diskID)
@ -865,3 +978,30 @@ func (c ConfigQemu) CreateQemuSerialsParams(
return nil 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/PuerkitoBio/goquery
# github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d # github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d
github.com/StackExchange/wmi 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/Telmate/proxmox-api-go/proxmox
# github.com/agext/levenshtein v1.2.1 # github.com/agext/levenshtein v1.2.1
github.com/agext/levenshtein 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 - `iso_file` (string) - Path to the ISO file to boot from, expressed as a
proxmox datastore path, for example 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: ### Optional:
- `insecure_skip_tls_verify` (bool) - Skip validating the certificate. - `insecure_skip_tls_verify` (bool) - Skip validating the certificate.