Merge pull request #8624 from carlpett/proxmox-upload-iso
Proxmox upload iso
This commit is contained in:
commit
db275ade8d
|
@ -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,
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Binary file not shown.
2
go.mod
2
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
|
||||
|
|
3
go.sum
3
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue