Proxmox upload ISO

This commit is contained in:
Calle Pettersson 2020-01-04 22:50:35 +01:00
parent d70d1e8bf7
commit 8e4c165173
8 changed files with 274 additions and 5 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.

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.