Proxmox upload ISO
This commit is contained in:
parent
d70d1e8bf7
commit
8e4c165173
|
@ -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.
|
@ -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