diff --git a/builder/proxmox/builder.go b/builder/proxmox/builder.go index e29645114..4ca1c2d07 100644 --- a/builder/proxmox/builder.go +++ b/builder/proxmox/builder.go @@ -38,6 +38,8 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { return nil, nil, nil } +const downloadPathKey = "downloaded_iso_path" + func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { var err error tlsConfig := &tls.Config{ @@ -62,6 +64,16 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack // Build the steps steps := []multistep.Step{ + &common.StepDownload{ + Checksum: b.config.ISOChecksum, + ChecksumType: b.config.ISOChecksumType, + Description: "ISO", + Extension: b.config.TargetExtension, + ResultKey: downloadPathKey, + TargetPath: b.config.TargetPath, + Url: b.config.ISOUrls, + }, + &stepUploadISO{}, &stepStartVM{}, &common.StepHTTPServer{ HTTPDir: b.config.HTTPDir, diff --git a/builder/proxmox/config.go b/builder/proxmox/config.go index 00f082d07..0dd3cb523 100644 --- a/builder/proxmox/config.go +++ b/builder/proxmox/config.go @@ -23,6 +23,7 @@ import ( type Config struct { common.PackerConfig `mapstructure:",squash"` common.HTTPConfig `mapstructure:",squash"` + common.ISOConfig `mapstructure:",squash"` bootcommand.BootConfig `mapstructure:",squash"` BootKeyInterval time.Duration `mapstructure:"boot_key_interval"` Comm communicator.Config `mapstructure:",squash"` @@ -46,6 +47,7 @@ type Config struct { NICs []nicConfig `mapstructure:"network_adapters"` Disks []diskConfig `mapstructure:"disks"` ISOFile string `mapstructure:"iso_file"` + ISOStoragePool string `mapstructure:"iso_storage_pool"` Agent bool `mapstructure:"qemu_agent"` SCSIController string `mapstructure:"scsi_controller"` @@ -53,6 +55,8 @@ type Config struct { TemplateDescription string `mapstructure:"template_description"` UnmountISO bool `mapstructure:"unmount_iso"` + shouldUploadISO bool + ctx interpolate.Context } @@ -91,6 +95,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { } var errs *packer.MultiError + warnings := make([]string, 0) // Defaults if c.ProxmoxURLRaw == "" { @@ -172,6 +177,26 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...) errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...) + // Check ISO config + // Either a pre-uploaded ISO should be referenced in iso_file, OR a URL + // (possibly to a local file) to an ISO file that will be downloaded and + // then uploaded to Proxmox. + if c.ISOFile != "" { + c.shouldUploadISO = false + } else { + isoWarnings, isoErrors := c.ISOConfig.Prepare(&c.ctx) + errs = packer.MultiErrorAppend(errs, isoErrors...) + warnings = append(warnings, isoWarnings...) + c.shouldUploadISO = true + } + + if (c.ISOFile == "" && len(c.ISOConfig.ISOUrls) == 0) || (c.ISOFile != "" && len(c.ISOConfig.ISOUrls) != 0) { + errs = packer.MultiErrorAppend(errs, errors.New("either iso_file or iso_url, but not both, must be specified")) + } + if len(c.ISOConfig.ISOUrls) != 0 && c.ISOStoragePool == "" { + errs = packer.MultiErrorAppend(errs, errors.New("when specifying iso_url, iso_storage_pool must also be specified")) + } + // Required configurations that will display errors if not set if c.Username == "" { errs = packer.MultiErrorAppend(errs, errors.New("username must be specified")) @@ -185,9 +210,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { if c.proxmoxURL, err = url.Parse(c.ProxmoxURLRaw); err != nil { errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("Could not parse proxmox_url: %s", err))) } - if c.ISOFile == "" { - errs = packer.MultiErrorAppend(errs, errors.New("iso_file must be specified")) - } if c.Node == "" { errs = packer.MultiErrorAppend(errs, errors.New("node must be specified")) } diff --git a/builder/proxmox/config.hcl2spec.go b/builder/proxmox/config.hcl2spec.go index e9c208c12..bdcf7d018 100644 --- a/builder/proxmox/config.hcl2spec.go +++ b/builder/proxmox/config.hcl2spec.go @@ -19,6 +19,13 @@ type FlatConfig struct { HTTPDir *string `mapstructure:"http_directory" cty:"http_directory"` HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min"` HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max"` + ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum"` + ISOChecksumURL *string `mapstructure:"iso_checksum_url" cty:"iso_checksum_url"` + ISOChecksumType *string `mapstructure:"iso_checksum_type" cty:"iso_checksum_type"` + RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url"` + ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls"` + TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path"` + TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension"` BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command"` @@ -79,6 +86,7 @@ type FlatConfig struct { NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters"` Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks"` ISOFile *string `mapstructure:"iso_file" cty:"iso_file"` + ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool"` Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent"` SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller"` TemplateName *string `mapstructure:"template_name" cty:"template_name"` @@ -108,6 +116,13 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "http_directory": &hcldec.AttrSpec{Name: "http_directory", Type: cty.String, Required: false}, "http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false}, "http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false}, + "iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false}, + "iso_checksum_url": &hcldec.AttrSpec{Name: "iso_checksum_url", Type: cty.String, Required: false}, + "iso_checksum_type": &hcldec.AttrSpec{Name: "iso_checksum_type", Type: cty.String, Required: false}, + "iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false}, + "iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false}, + "iso_target_path": &hcldec.AttrSpec{Name: "iso_target_path", Type: cty.String, Required: false}, + "iso_target_extension": &hcldec.AttrSpec{Name: "iso_target_extension", Type: cty.String, Required: false}, "boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false}, "boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false}, "boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false}, @@ -168,6 +183,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*FlatnicConfig)(nil).HCL2Spec())}, "disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*FlatdiskConfig)(nil).HCL2Spec())}, "iso_file": &hcldec.AttrSpec{Name: "iso_file", Type: cty.String, Required: false}, + "iso_storage_pool": &hcldec.AttrSpec{Name: "iso_storage_pool", Type: cty.String, Required: false}, "qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false}, "scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false}, "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, diff --git a/builder/proxmox/step_start_vm.go b/builder/proxmox/step_start_vm.go index 378b689ea..453798917 100644 --- a/builder/proxmox/step_start_vm.go +++ b/builder/proxmox/step_start_vm.go @@ -26,6 +26,8 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist agent = 0 } + isoFile := state.Get("iso_file").(string) + ui.Say("Creating VM") config := proxmox.ConfigQemu{ Name: c.VMName, @@ -37,7 +39,7 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist QemuCores: c.Cores, QemuSockets: c.Sockets, QemuOs: c.OS, - QemuIso: c.ISOFile, + QemuIso: isoFile, QemuNetworks: generateProxmoxNetworkAdapters(c.NICs), QemuDisks: generateProxmoxDisks(c.Disks), Scsihw: c.SCSIController, diff --git a/builder/proxmox/step_upload_iso.go b/builder/proxmox/step_upload_iso.go new file mode 100644 index 000000000..deba1672f --- /dev/null +++ b/builder/proxmox/step_upload_iso.go @@ -0,0 +1,66 @@ +package proxmox + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// stepUploadISO uploads an ISO file to Proxmox so we can boot from it +type stepUploadISO struct{} + +type uploader interface { + Upload(node string, storage string, contentType string, filename string, file io.Reader) error +} + +var _ uploader = &proxmox.Client{} + +func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("proxmoxClient").(uploader) + c := state.Get("config").(*Config) + + if !c.shouldUploadISO { + state.Put("iso_file", c.ISOFile) + return multistep.ActionContinue + } + + p := state.Get(downloadPathKey).(string) + if p == "" { + err := fmt.Errorf("Path to downloaded ISO was empty") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // All failure cases in resolving the symlink are caught anyway in os.Open + isoPath, _ := filepath.EvalSymlinks(p) + r, err := os.Open(isoPath) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + filename := filepath.Base(c.ISOUrls[0]) + err = client.Upload(c.Node, c.ISOStoragePool, "iso", filename, r) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + isoStoragePath := fmt.Sprintf("%s:iso/%s", c.ISOStoragePool, filename) + state.Put("iso_file", isoStoragePath) + + return multistep.ActionContinue +} + +func (s *stepUploadISO) Cleanup(state multistep.StateBag) { +} diff --git a/builder/proxmox/step_upload_iso_test.go b/builder/proxmox/step_upload_iso_test.go new file mode 100644 index 000000000..a009d6686 --- /dev/null +++ b/builder/proxmox/step_upload_iso_test.go @@ -0,0 +1,137 @@ +package proxmox + +import ( + "context" + "fmt" + "io" + "testing" + + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type uploaderMock struct { + fail bool + wasCalled bool +} + +func (m *uploaderMock) Upload(node string, storage string, contentType string, filename string, file io.Reader) error { + m.wasCalled = true + if m.fail { + return fmt.Errorf("Testing induced failure") + } + return nil +} + +var _ uploader = &uploaderMock{} + +func TestUploadISO(t *testing.T) { + cs := []struct { + name string + builderConfig *Config + downloadPath string + failUpload bool + + expectError bool + expectUploadCalled bool + expectedISOPath string + expectedAction multistep.StepAction + }{ + { + name: "should not call upload unless configured to do so", + builderConfig: &Config{shouldUploadISO: false, ISOFile: "local:iso/some-file"}, + + expectUploadCalled: false, + expectedISOPath: "local:iso/some-file", + expectedAction: multistep.ActionContinue, + }, + { + name: "success should continue", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/test.iso", + + expectedISOPath: "local:iso/some-file.iso", + expectUploadCalled: true, + expectedAction: multistep.ActionContinue, + }, + { + name: "failing upload should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/test.iso", + failUpload: true, + + expectError: true, + expectUploadCalled: true, + expectedAction: multistep.ActionHalt, + }, + { + name: "downloader: state misconfiguration should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + + expectError: true, + expectUploadCalled: false, + expectedAction: multistep.ActionHalt, + }, + { + name: "downloader: file unreadable should halt", + builderConfig: &Config{ + shouldUploadISO: true, + ISOStoragePool: "local", + ISOConfig: common.ISOConfig{ISOUrls: []string{"http://server.example/some-file.iso"}}, + }, + downloadPath: "testdata/non-existent.iso", + + expectError: true, + expectUploadCalled: false, + expectedAction: multistep.ActionHalt, + }, + } + + for _, c := range cs { + t.Run(c.name, func(t *testing.T) { + m := &uploaderMock{fail: c.failUpload} + + state := new(multistep.BasicStateBag) + state.Put("ui", packer.TestUi(t)) + state.Put("config", c.builderConfig) + state.Put(downloadPathKey, c.downloadPath) + state.Put("proxmoxClient", m) + + step := stepUploadISO{} + action := step.Run(context.TODO(), state) + step.Cleanup(state) + + if action != c.expectedAction { + t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) + } + if m.wasCalled != c.expectUploadCalled { + t.Errorf("Expected mock to be called: %v, got: %v", c.expectUploadCalled, m.wasCalled) + } + err, gotError := state.GetOk("error") + if gotError != c.expectError { + t.Errorf("Expected error state to be: %v, got: %v", c.expectError, gotError) + } + if err == nil { + if isoPath := state.Get("iso_file"); isoPath != c.expectedISOPath { + if _, ok := isoPath.(string); !ok { + isoPath = "" + } + t.Errorf("Expected state iso_path to be %q, got %q", c.expectedISOPath, isoPath) + } + } + }) + } +} diff --git a/builder/proxmox/testdata/test.iso b/builder/proxmox/testdata/test.iso new file mode 100644 index 000000000..6e28c7af9 Binary files /dev/null and b/builder/proxmox/testdata/test.iso differ diff --git a/website/source/docs/builders/proxmox.html.md b/website/source/docs/builders/proxmox.html.md index 1704fcabb..4d914b480 100644 --- a/website/source/docs/builders/proxmox.html.md +++ b/website/source/docs/builders/proxmox.html.md @@ -50,7 +50,21 @@ builder. - `iso_file` (string) - Path to the ISO file to boot from, expressed as a proxmox datastore path, for example - `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso` + `local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso`. + Either `iso_file` OR `iso_url` must be specifed. + +- `iso_url` (string) - URL to an ISO file to upload to Proxmox, and then + boot from. Either `iso_file` OR `iso_url` must be specifed. + +- `iso_storage_pool` (string) - Proxmox storage pool onto which to upload + the ISO file. + +- `iso_checksum` (string) - Checksum of the ISO file. + +- `iso_checksum_type` (string) - Type of the checksum. Can be md5, sha1, + sha256, sha512 or none. Corruption of large files, such as ISOs, can occur + during transfer from time to time. As such, setting this to none is not + recommended. ### Optional: - `insecure_skip_tls_verify` (bool) - Skip validating the certificate.