diff --git a/builder/proxmox/builder.go b/builder/proxmox/builder.go index b4af212c2..6d99fc464 100644 --- a/builder/proxmox/builder.go +++ b/builder/proxmox/builder.go @@ -1,191 +1,5 @@ package proxmox -import ( - "context" - "crypto/tls" - "errors" - "fmt" +import proxmoxiso "github.com/hashicorp/packer/builder/proxmox/iso" - "github.com/Telmate/proxmox-api-go/proxmox" - "github.com/hashicorp/hcl/v2/hcldec" - "github.com/hashicorp/packer/common" - "github.com/hashicorp/packer/helper/communicator" - "github.com/hashicorp/packer/helper/multistep" - "github.com/hashicorp/packer/packer" -) - -// The unique id for the builder -const BuilderId = "proxmox.builder" - -type Builder struct { - config Config - runner multistep.Runner - proxmoxClient *proxmox.Client -} - -// Builder implements packer.Builder -var _ packer.Builder = &Builder{} - -var pluginVersion = "1.0.0" - -func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } - -func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { - warnings, errs := b.config.Prepare(raws...) - if errs != nil { - return nil, warnings, errs - } - 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{ - InsecureSkipVerify: b.config.SkipCertValidation, - } - b.proxmoxClient, err = proxmox.NewClient(b.config.proxmoxURL.String(), nil, tlsConfig) - if err != nil { - return nil, err - } - - err = b.proxmoxClient.Login(b.config.Username, b.config.Password, "") - if err != nil { - return nil, err - } - - // Set up the state - state := new(multistep.BasicStateBag) - state.Put("config", &b.config) - state.Put("proxmoxClient", b.proxmoxClient) - state.Put("hook", hook) - state.Put("ui", ui) - - // Build the steps - steps := []multistep.Step{ - &common.StepDownload{ - Checksum: b.config.ISOChecksum, - Description: "ISO", - Extension: b.config.TargetExtension, - ResultKey: downloadPathKey, - TargetPath: b.config.TargetPath, - Url: b.config.ISOUrls, - }} - - for idx := range b.config.AdditionalISOFiles { - steps = append(steps, &common.StepDownload{ - Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum, - Description: "additional ISO", - Extension: b.config.AdditionalISOFiles[idx].TargetExtension, - ResultKey: b.config.AdditionalISOFiles[idx].downloadPathKey, - TargetPath: b.config.AdditionalISOFiles[idx].downloadPathKey, - Url: b.config.AdditionalISOFiles[idx].ISOUrls, - }) - } - steps = append(steps, - &stepUploadISO{}, - &stepUploadAdditionalISOs{}, - &stepStartVM{}, - &common.StepHTTPServer{ - HTTPDir: b.config.HTTPDir, - HTTPPortMin: b.config.HTTPPortMin, - HTTPPortMax: b.config.HTTPPortMax, - HTTPAddress: b.config.HTTPAddress, - }, - &stepTypeBootCommand{ - BootConfig: b.config.BootConfig, - Ctx: b.config.ctx, - }, - &communicator.StepConnect{ - Config: &b.config.Comm, - Host: commHost(b.config.Comm.Host()), - SSHConfig: b.config.Comm.SSHConfigFunc(), - }, - &common.StepProvision{}, - &common.StepCleanupTempKeys{ - Comm: &b.config.Comm, - }, - &stepConvertToTemplate{}, - &stepFinalizeTemplateConfig{}, - &stepSuccess{}, - ) - - // Run the steps - b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) - b.runner.Run(ctx, state) - // If there was an error, return that - if rawErr, ok := state.GetOk("error"); ok { - return nil, rawErr.(error) - } - // If we were interrupted or cancelled, then just exit. - if _, ok := state.GetOk(multistep.StateCancelled); ok { - return nil, errors.New("build was cancelled") - } - - // Verify that the template_id was set properly, otherwise we didn't progress through the last step - tplID, ok := state.Get("template_id").(int) - if !ok { - return nil, fmt.Errorf("template ID could not be determined") - } - - artifact := &Artifact{ - templateID: tplID, - proxmoxClient: b.proxmoxClient, - StateData: map[string]interface{}{"generated_data": state.Get("generated_data")}, - } - - return artifact, nil -} - -// Returns ssh_host or winrm_host (see communicator.Config.Host) config -// parameter when set, otherwise gets the host IP from running VM -func commHost(host string) func(state multistep.StateBag) (string, error) { - if host != "" { - return func(state multistep.StateBag) (string, error) { - return host, nil - } - } - return getVMIP -} - -// Reads the first non-loopback interface's IP address from the VM. -// qemu-guest-agent package must be installed on the VM -func getVMIP(state multistep.StateBag) (string, error) { - client := state.Get("proxmoxClient").(*proxmox.Client) - config := state.Get("config").(*Config) - vmRef := state.Get("vmRef").(*proxmox.VmRef) - - ifs, err := client.GetVmAgentNetworkInterfaces(vmRef) - if err != nil { - return "", err - } - - if config.VMInterface != "" { - for _, iface := range ifs { - if config.VMInterface != iface.Name { - continue - } - - for _, addr := range iface.IPAddresses { - if addr.IsLoopback() { - continue - } - return addr.String(), nil - } - return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface) - } - return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface) - } - - for _, iface := range ifs { - for _, addr := range iface.IPAddresses { - if addr.IsLoopback() { - continue - } - return addr.String(), nil - } - } - - return "", fmt.Errorf("Found no IP addresses on VM") -} +type Builder = proxmoxiso.Builder diff --git a/builder/proxmox/clone/builder.go b/builder/proxmox/clone/builder.go new file mode 100644 index 000000000..178f73587 --- /dev/null +++ b/builder/proxmox/clone/builder.go @@ -0,0 +1,74 @@ +package proxmoxclone + +import ( + proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/hcl/v2/hcldec" + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + + "context" + "fmt" +) + +// The unique id for the builder +const BuilderID = "proxmox.clone" + +type Builder struct { + config Config +} + +// Builder implements packer.Builder +var _ packer.Builder = &Builder{} + +func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } + +func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { + return b.config.Prepare(raws...) +} + +func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { + state := new(multistep.BasicStateBag) + state.Put("clone-config", &b.config) + + preSteps := []multistep.Step{ + &StepSshKeyPair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName), + }, + } + postSteps := []multistep.Step{} + + sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &cloneVMCreator{}) + return sb.Run(ctx, ui, hook, state) +} + +type cloneVMCreator struct{} + +func (*cloneVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error { + client := state.Get("proxmoxClient").(*proxmoxapi.Client) + c := state.Get("clone-config").(*Config) + comm := state.Get("config").(*proxmox.Config).Comm + + fullClone := 1 + if c.FullClone.False() { + fullClone = 0 + } + + config.FullClone = &fullClone + config.CIuser = comm.SSHUsername + config.Sshkeys = string(comm.SSHPublicKey) + sourceVmr, err := client.GetVmRefByName(c.CloneVM) + if err != nil { + return err + } + err = config.CloneVm(sourceVmr, vmRef, client) + if err != nil { + return err + } + err = config.UpdateConfig(vmRef, client) + if err != nil { + return err + } + return nil +} diff --git a/builder/proxmox/clone/config.go b/builder/proxmox/clone/config.go new file mode 100644 index 000000000..e9a8721d6 --- /dev/null +++ b/builder/proxmox/clone/config.go @@ -0,0 +1,29 @@ +//go:generate mapstructure-to-hcl2 -type Config + +package proxmoxclone + +import ( + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/hashicorp/packer/helper/config" + "github.com/hashicorp/packer/packer" +) + +type Config struct { + proxmox.Config `mapstructure:",squash"` + + CloneVM string `mapstructure:"clone_vm"` + FullClone config.Trilean `mapstructure:"full_clone" required:"false"` +} + +func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) { + var errs *packer.MultiError + _, warnings, merrs := c.Config.Prepare(c, raws...) + if merrs != nil { + errs = packer.MultiErrorAppend(errs, merrs) + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, warnings, errs + } + return nil, warnings, nil +} diff --git a/builder/proxmox/clone/config.hcl2spec.go b/builder/proxmox/clone/config.hcl2spec.go new file mode 100644 index 000000000..879549542 --- /dev/null +++ b/builder/proxmox/clone/config.hcl2spec.go @@ -0,0 +1,211 @@ +// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT. +package proxmoxclone + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"` + HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"` + HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"` + HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"` + HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"` + BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` + BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` + BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` + BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"` + Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` + PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` + SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"` + SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"` + SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"` + SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"` + SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"` + SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"` + SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"` + SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"` + SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"` + SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"` + SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"` + SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"` + SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"` + SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"` + SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"` + SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"` + SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"` + SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"` + SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"` + SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"` + SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"` + SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"` + SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"` + SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"` + SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"` + SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"` + SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"` + SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"` + SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"` + SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"` + SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"` + SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"` + SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"` + SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"` + SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"` + SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"` + WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"` + WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"` + WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"` + WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"` + WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"` + WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"` + WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"` + WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"` + WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"` + ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"` + SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` + Username *string `mapstructure:"username" cty:"username" hcl:"username"` + Password *string `mapstructure:"password" cty:"password" hcl:"password"` + Node *string `mapstructure:"node" cty:"node" hcl:"node"` + Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"` + VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"` + VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"` + Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"` + Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"` + CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"` + Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"` + OS *string `mapstructure:"os" cty:"os" hcl:"os"` + VGA *proxmox.FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"` + NICs []proxmox.FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"` + Disks []proxmox.FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"` + Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"` + SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"` + Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"` + DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"` + TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"` + TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"` + CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"` + CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"` + AdditionalISOFiles []proxmox.FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"` + VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"` + CloneVM *string `mapstructure:"clone_vm" cty:"clone_vm" hcl:"clone_vm"` + FullClone *bool `mapstructure:"full_clone" required:"false" cty:"full_clone" hcl:"full_clone"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(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_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false}, + "http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false}, + "http_interface": &hcldec.AttrSpec{Name: "http_interface", 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}, + "boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false}, + "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, + "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, + "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, + "ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false}, + "ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false}, + "ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false}, + "ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false}, + "temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false}, + "ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false}, + "ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false}, + "ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false}, + "ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false}, + "ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false}, + "ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false}, + "ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false}, + "ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false}, + "ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false}, + "ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false}, + "ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false}, + "ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false}, + "ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false}, + "ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false}, + "ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false}, + "ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false}, + "ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false}, + "ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false}, + "ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false}, + "ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false}, + "ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false}, + "ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false}, + "ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false}, + "ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false}, + "ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false}, + "ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false}, + "ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false}, + "ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false}, + "winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false}, + "winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false}, + "winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false}, + "winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false}, + "winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false}, + "winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false}, + "winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false}, + "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, + "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, + "proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false}, + "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, + "username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false}, + "password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false}, + "node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false}, + "pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false}, + "vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false}, + "vm_id": &hcldec.AttrSpec{Name: "vm_id", Type: cty.Number, Required: false}, + "memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false}, + "cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false}, + "cpu_type": &hcldec.AttrSpec{Name: "cpu_type", Type: cty.String, Required: false}, + "sockets": &hcldec.AttrSpec{Name: "sockets", Type: cty.Number, Required: false}, + "os": &hcldec.AttrSpec{Name: "os", Type: cty.String, Required: false}, + "vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*proxmox.FlatvgaConfig)(nil).HCL2Spec())}, + "network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*proxmox.FlatnicConfig)(nil).HCL2Spec())}, + "disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*proxmox.FlatdiskConfig)(nil).HCL2Spec())}, + "qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false}, + "scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false}, + "onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false}, + "disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false}, + "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, + "template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false}, + "cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false}, + "cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false}, + "additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*proxmox.FlatstorageConfig)(nil).HCL2Spec())}, + "vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false}, + "clone_vm": &hcldec.AttrSpec{Name: "clone_vm", Type: cty.String, Required: false}, + "full_clone": &hcldec.AttrSpec{Name: "full_clone", Type: cty.Bool, Required: false}, + } + return s +} diff --git a/builder/proxmox/clone/step_ssh_key_pair.go b/builder/proxmox/clone/step_ssh_key_pair.go new file mode 100644 index 000000000..2aad603bb --- /dev/null +++ b/builder/proxmox/clone/step_ssh_key_pair.go @@ -0,0 +1,107 @@ +package proxmoxclone + +import ( + "context" + "fmt" + "os" + + common "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/helper/ssh" + "github.com/hashicorp/packer/packer" +) + +// StepSshKeyPair executes the business logic for setting the SSH key pair in +// the specified communicator.Config. +type StepSshKeyPair struct { + Debug bool + DebugKeyPath string +} + +func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + c := state.Get("config").(*common.Config) + + if c.Comm.SSHPassword != "" { + return multistep.ActionContinue + } + + if c.Comm.SSHPrivateKeyFile != "" { + ui.Say("Using existing SSH private key for the communicator...") + privateKeyBytes, err := c.Comm.ReadSSHPrivateKeyFile() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{ + RawPrivateKeyPemBlock: privateKeyBytes, + Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), + }) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + c.Comm.SSHPrivateKey = privateKeyBytes + c.Comm.SSHKeyPairName = kp.Comment + c.Comm.SSHTemporaryKeyPairName = kp.Comment + c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + + return multistep.ActionContinue + } + + if c.Comm.SSHAgentAuth { + ui.Say("Using local SSH Agent to authenticate connections for the communicator...") + return multistep.ActionContinue + } + + ui.Say("Creating ephemeral key pair for SSH communicator...") + + kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{ + Comment: fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()), + }) + if err != nil { + state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) + return multistep.ActionHalt + } + + c.Comm.SSHKeyPairName = kp.Comment + c.Comm.SSHTemporaryKeyPairName = kp.Comment + c.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock + c.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + c.Comm.SSHClearAuthorizedKeys = true + + ui.Say("Created ephemeral SSH key pair for communicator") + + // If we're in debug mode, output the private key to the working + // directory. + if s.Debug { + ui.Message(fmt.Sprintf("Saving communicator private key for debug purposes: %s", s.DebugKeyPath)) + f, err := os.OpenFile(s.DebugKeyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + defer f.Close() + + // Write the key out + if _, err := f.Write(kp.PrivateKeyPemBlock); err != nil { + state.Put("error", fmt.Errorf("Error saving debug key: %s", err)) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *StepSshKeyPair) Cleanup(state multistep.StateBag) { + if s.Debug { + if err := os.Remove(s.DebugKeyPath); err != nil { + ui := state.Get("ui").(packer.Ui) + ui.Error(fmt.Sprintf( + "Error removing debug key '%s': %s", s.DebugKeyPath, err)) + } + } +} diff --git a/builder/proxmox/artifact.go b/builder/proxmox/common/artifact.go similarity index 91% rename from builder/proxmox/artifact.go rename to builder/proxmox/common/artifact.go index 8b11e266d..059d8ed81 100644 --- a/builder/proxmox/artifact.go +++ b/builder/proxmox/common/artifact.go @@ -10,6 +10,7 @@ import ( ) type Artifact struct { + builderID string templateID int proxmoxClient *proxmox.Client @@ -21,8 +22,8 @@ type Artifact struct { // Artifact implements packer.Artifact var _ packer.Artifact = &Artifact{} -func (*Artifact) BuilderId() string { - return BuilderId +func (a *Artifact) BuilderId() string { + return a.builderID } func (*Artifact) Files() []string { diff --git a/builder/proxmox/bootcommand_driver.go b/builder/proxmox/common/bootcommand_driver.go similarity index 100% rename from builder/proxmox/bootcommand_driver.go rename to builder/proxmox/common/bootcommand_driver.go diff --git a/builder/proxmox/common/builder.go b/builder/proxmox/common/builder.go new file mode 100644 index 000000000..a4b407520 --- /dev/null +++ b/builder/proxmox/common/builder.go @@ -0,0 +1,167 @@ +package proxmox + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +func NewSharedBuilder(id string, config Config, preSteps []multistep.Step, postSteps []multistep.Step, vmCreator ProxmoxVMCreator) *Builder { + return &Builder{ + id: id, + config: config, + preSteps: preSteps, + postSteps: postSteps, + vmCreator: vmCreator, + } +} + +type Builder struct { + id string + config Config + preSteps []multistep.Step + postSteps []multistep.Step + runner multistep.Runner + proxmoxClient *proxmox.Client + vmCreator ProxmoxVMCreator +} + +func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook, state multistep.StateBag) (packer.Artifact, error) { + var err error + tlsConfig := &tls.Config{ + InsecureSkipVerify: b.config.SkipCertValidation, + } + b.proxmoxClient, err = proxmox.NewClient(b.config.proxmoxURL.String(), nil, tlsConfig) + if err != nil { + return nil, err + } + + err = b.proxmoxClient.Login(b.config.Username, b.config.Password, "") + if err != nil { + return nil, err + } + + // Set up the state + state.Put("config", &b.config) + state.Put("proxmoxClient", b.proxmoxClient) + state.Put("hook", hook) + state.Put("ui", ui) + + comm := &b.config.Comm + + // Build the steps + coreSteps := []multistep.Step{ + &stepStartVM{ + vmCreator: b.vmCreator, + }, + &common.StepHTTPServer{ + HTTPDir: b.config.HTTPDir, + HTTPPortMin: b.config.HTTPPortMin, + HTTPPortMax: b.config.HTTPPortMax, + HTTPAddress: b.config.HTTPAddress, + }, + &stepTypeBootCommand{ + BootConfig: b.config.BootConfig, + Ctx: b.config.Ctx, + }, + &communicator.StepConnect{ + Config: comm, + Host: commHost((*comm).Host()), + SSHConfig: (*comm).SSHConfigFunc(), + }, + &common.StepProvision{}, + &common.StepCleanupTempKeys{ + Comm: &b.config.Comm, + }, + &stepConvertToTemplate{}, + &stepFinalizeTemplateConfig{}, + &stepSuccess{}, + } + steps := append(b.preSteps, coreSteps...) + steps = append(steps, b.postSteps...) + // Run the steps + b.runner = common.NewRunner(steps, b.config.PackerConfig, ui) + b.runner.Run(ctx, state) + // If there was an error, return that + if rawErr, ok := state.GetOk("error"); ok { + return nil, rawErr.(error) + } + // If we were interrupted or cancelled, then just exit. + if _, ok := state.GetOk(multistep.StateCancelled); ok { + return nil, errors.New("build was cancelled") + } + + // Verify that the template_id was set properly, otherwise we didn't progress through the last step + tplID, ok := state.Get("template_id").(int) + if !ok { + return nil, fmt.Errorf("template ID could not be determined") + } + + artifact := &Artifact{ + builderID: b.id, + templateID: tplID, + proxmoxClient: b.proxmoxClient, + StateData: map[string]interface{}{"generated_data": state.Get("generated_data")}, + } + + return artifact, nil +} + +// Returns ssh_host or winrm_host (see communicator.Config.Host) config +// parameter when set, otherwise gets the host IP from running VM +func commHost(host string) func(state multistep.StateBag) (string, error) { + if host != "" { + return func(state multistep.StateBag) (string, error) { + return host, nil + } + } + return getVMIP +} + +// Reads the first non-loopback interface's IP address from the VM. +// qemu-guest-agent package must be installed on the VM +func getVMIP(state multistep.StateBag) (string, error) { + client := state.Get("proxmoxClient").(*proxmox.Client) + config := state.Get("config").(*Config) + vmRef := state.Get("vmRef").(*proxmox.VmRef) + + ifs, err := client.GetVmAgentNetworkInterfaces(vmRef) + if err != nil { + return "", err + } + + if config.VMInterface != "" { + for _, iface := range ifs { + if config.VMInterface != iface.Name { + continue + } + + for _, addr := range iface.IPAddresses { + if addr.IsLoopback() { + continue + } + return addr.String(), nil + } + return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface) + } + return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface) + } + + for _, iface := range ifs { + for _, addr := range iface.IPAddresses { + if addr.IsLoopback() { + continue + } + return addr.String(), nil + } + } + + return "", fmt.Errorf("Found no IP addresses on VM") +} diff --git a/builder/proxmox/config.go b/builder/proxmox/common/config.go similarity index 65% rename from builder/proxmox/config.go rename to builder/proxmox/common/config.go index 07512ca41..1428345f9 100644 --- a/builder/proxmox/config.go +++ b/builder/proxmox/common/config.go @@ -8,7 +8,6 @@ import ( "log" "net/url" "os" - "strconv" "strings" "time" @@ -25,7 +24,6 @@ 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"` @@ -49,8 +47,6 @@ type Config struct { VGA vgaConfig `mapstructure:"vga"` 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"` Onboot bool `mapstructure:"onboot"` @@ -58,17 +54,24 @@ type Config struct { TemplateName string `mapstructure:"template_name"` TemplateDescription string `mapstructure:"template_description"` - UnmountISO bool `mapstructure:"unmount_iso"` CloudInit bool `mapstructure:"cloud_init"` CloudInitStoragePool string `mapstructure:"cloud_init_storage_pool"` - shouldUploadISO bool - AdditionalISOFiles []storageConfig `mapstructure:"additional_iso_files"` VMInterface string `mapstructure:"vm_interface"` - ctx interpolate.Context + Ctx interpolate.Context `mapstructure-to-hcl2:",skip"` +} + +type storageConfig struct { + common.ISOConfig `mapstructure:",squash"` + Device string `mapstructure:"device"` + ISOFile string `mapstructure:"iso_file"` + ISOStoragePool string `mapstructure:"iso_storage_pool"` + Unmount bool `mapstructure:"unmount"` + ShouldUploadISO bool + DownloadPathKey string } type nicConfig struct { @@ -92,27 +95,18 @@ type vgaConfig struct { Type string `mapstructure:"type"` Memory int `mapstructure:"memory"` } -type storageConfig struct { - common.ISOConfig `mapstructure:",squash"` - Device string `mapstructure:"device"` - ISOFile string `mapstructure:"iso_file"` - ISOStoragePool string `mapstructure:"iso_storage_pool"` - Unmount bool `mapstructure:"unmount"` - shouldUploadISO bool - downloadPathKey string -} -func (c *Config) Prepare(raws ...interface{}) ([]string, error) { +func (c *Config) Prepare(upper interface{}, raws ...interface{}) ([]string, []string, error) { // Agent defaults to true c.Agent = true // Do not add a cloud-init cdrom by default c.CloudInit = false var md mapstructure.Metadata - err := config.Decode(c, &config.DecodeOpts{ + err := config.Decode(upper, &config.DecodeOpts{ Metadata: &md, Interpolate: true, - InterpolateContext: &c.ctx, + InterpolateContext: &c.Ctx, InterpolateFilter: &interpolate.RenderFilter{ Exclude: []string{ "boot_command", @@ -120,11 +114,13 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { }, }, raws...) if err != nil { - return nil, err + return nil, nil, err } var errs *packer.MultiError - warnings := make([]string, 0) + var warnings []string + + packer.LogSecretFilter.Set(c.Password) // Defaults if c.ProxmoxURLRaw == "" { @@ -208,89 +204,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { errs = packer.MultiErrorAppend(errs, fmt.Errorf("disk format must be specified for pool type %q", c.Disks[idx].StoragePoolType)) } } - for idx := range c.AdditionalISOFiles { - // Check AdditionalISO 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.AdditionalISOFiles[idx].ISOFile != "" { - c.AdditionalISOFiles[idx].shouldUploadISO = false - } else { - c.AdditionalISOFiles[idx].downloadPathKey = "downloaded_additional_iso_path_" + strconv.Itoa(idx) - isoWarnings, isoErrors := c.AdditionalISOFiles[idx].ISOConfig.Prepare(&c.ctx) - errs = packer.MultiErrorAppend(errs, isoErrors...) - warnings = append(warnings, isoWarnings...) - c.AdditionalISOFiles[idx].shouldUploadISO = true - } - if c.AdditionalISOFiles[idx].Device == "" { - log.Printf("AdditionalISOFile %d Device not set, using default 'ide3'", idx) - c.AdditionalISOFiles[idx].Device = "ide3" - } - if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "ide") { - busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[3:]) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[3:])) - } - if busnumber == 2 { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus 2 is used by boot ISO")) - } - if busnumber > 3 { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus index can't be higher than 3")) - } - } - if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "sata") { - busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:]) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:])) - } - if busnumber > 5 { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("SATA bus index can't be higher than 5")) - } - } - if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "scsi") { - busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:]) - if err != nil { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:])) - } - if busnumber > 30 { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("SCSI bus index can't be higher than 30")) - } - } - if (c.AdditionalISOFiles[idx].ISOFile == "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) == 0) || (c.AdditionalISOFiles[idx].ISOFile != "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) != 0) { - errs = packer.MultiErrorAppend(errs, fmt.Errorf("either iso_file or iso_url, but not both, must be specified for AdditionalISO file %s", c.AdditionalISOFiles[idx].Device)) - } - 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")) - } - } if c.SCSIController == "" { log.Printf("SCSI controller not set, using default 'lsi'") c.SCSIController = "lsi" } - errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...) - 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")) - } + errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.Ctx)...) + errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.Ctx)...) + errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.Ctx)...) // Required configurations that will display errors if not set if c.Username == "" { @@ -329,11 +250,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { } if errs != nil && len(errs.Errors) > 0 { - return nil, errs + return nil, warnings, errs } - - packer.LogSecretFilter.Set(c.Password) - return nil, nil + return nil, warnings, nil } func contains(haystack []string, needle string) bool { diff --git a/builder/proxmox/config.hcl2spec.go b/builder/proxmox/common/config.hcl2spec.go similarity index 94% rename from builder/proxmox/config.hcl2spec.go rename to builder/proxmox/common/config.hcl2spec.go index 53055378b..2cc292dbd 100644 --- a/builder/proxmox/config.hcl2spec.go +++ b/builder/proxmox/common/config.hcl2spec.go @@ -21,11 +21,6 @@ type FlatConfig struct { HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"` HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"` HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"` - ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"` - RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"` - ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"` - TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"` - TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"` BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` @@ -93,15 +88,12 @@ type FlatConfig struct { VGA *FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"` NICs []FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"` Disks []FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"` - ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"` - ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"` Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"` SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"` Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"` DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"` TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"` TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"` - UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"` CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"` CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"` AdditionalISOFiles []FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"` @@ -132,11 +124,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false}, "http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false}, "http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false}, - "iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", 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}, @@ -204,15 +191,12 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*FlatvgaConfig)(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())}, - "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}, "onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false}, "disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false}, "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, "template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false}, - "unmount_iso": &hcldec.AttrSpec{Name: "unmount_iso", Type: cty.Bool, Required: false}, "cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false}, "cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false}, "additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*FlatstorageConfig)(nil).HCL2Spec())}, @@ -301,6 +285,8 @@ type FlatstorageConfig struct { ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"` ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"` Unmount *bool `mapstructure:"unmount" cty:"unmount" hcl:"unmount"` + ShouldUploadISO *bool `cty:"should_upload_iso" hcl:"should_upload_iso"` + DownloadPathKey *string `cty:"download_path_key" hcl:"download_path_key"` } // FlatMapstructure returns a new FlatstorageConfig. @@ -324,6 +310,8 @@ func (*FlatstorageConfig) HCL2Spec() map[string]hcldec.Spec { "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}, "unmount": &hcldec.AttrSpec{Name: "unmount", Type: cty.Bool, Required: false}, + "should_upload_iso": &hcldec.AttrSpec{Name: "should_upload_iso", Type: cty.Bool, Required: false}, + "download_path_key": &hcldec.AttrSpec{Name: "download_path_key", Type: cty.String, Required: false}, } return s } diff --git a/builder/proxmox/common/config_test.go b/builder/proxmox/common/config_test.go new file mode 100644 index 000000000..b68c6fcc8 --- /dev/null +++ b/builder/proxmox/common/config_test.go @@ -0,0 +1,104 @@ +package proxmox + +import ( + "strings" + "testing" + + "github.com/hashicorp/packer/packer" +) + +func mandatoryConfig(t *testing.T) map[string]interface{} { + return map[string]interface{}{ + "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", + "username": "apiuser@pve", + "password": "supersecret", + "node": "my-proxmox", + "ssh_username": "root", + } +} + +func TestRequiredParameters(t *testing.T) { + var c Config + _, _, err := c.Prepare(&c, make(map[string]interface{})) + if err == nil { + t.Fatal("Expected empty configuration to fail") + } + errs, ok := err.(*packer.MultiError) + if !ok { + t.Fatal("Expected errors to be packer.MultiError") + } + + required := []string{"username", "password", "proxmox_url", "node", "ssh_username"} + for _, param := range required { + found := false + for _, err := range errs.Errors { + if strings.Contains(err.Error(), param) { + found = true + break + } + } + if !found { + t.Errorf("Expected error about missing parameter %q", required) + } + } +} + +func TestAgentSetToFalse(t *testing.T) { + cfg := mandatoryConfig(t) + cfg["qemu_agent"] = false + + var c Config + _, _, err := c.Prepare(&c, cfg) + if err != nil { + t.Fatal(err) + } + + if c.Agent != false { + t.Errorf("Expected Agent to be false, got %t", c.Agent) + } +} + +func TestPacketQueueSupportForNetworkAdapters(t *testing.T) { + drivertests := []struct { + expectedToFail bool + model string + }{ + {expectedToFail: false, model: "virtio"}, + {expectedToFail: true, model: "e1000"}, + {expectedToFail: true, model: "e1000-82540em"}, + {expectedToFail: true, model: "e1000-82544gc"}, + {expectedToFail: true, model: "e1000-82545em"}, + {expectedToFail: true, model: "i82551"}, + {expectedToFail: true, model: "i82557b"}, + {expectedToFail: true, model: "i82559er"}, + {expectedToFail: true, model: "ne2k_isa"}, + {expectedToFail: true, model: "ne2k_pci"}, + {expectedToFail: true, model: "pcnet"}, + {expectedToFail: true, model: "rtl8139"}, + {expectedToFail: true, model: "vmxnet3"}, + } + + for _, tt := range drivertests { + device := make(map[string]interface{}) + device["bridge"] = "vmbr0" + device["model"] = tt.model + device["packet_queues"] = 2 + + devices := make([]map[string]interface{}, 0) + devices = append(devices, device) + + cfg := mandatoryConfig(t) + cfg["network_adapters"] = devices + + var c Config + _, _, err := c.Prepare(&c, cfg) + + if tt.expectedToFail == true && err == nil { + t.Error("expected config preparation to fail, but no error occured") + } + + if tt.expectedToFail == false && err != nil { + t.Errorf("expected config preparation to succeed, but %s", err.Error()) + } + } +} diff --git a/builder/proxmox/step_convert_to_template.go b/builder/proxmox/common/step_convert_to_template.go similarity index 100% rename from builder/proxmox/step_convert_to_template.go rename to builder/proxmox/common/step_convert_to_template.go diff --git a/builder/proxmox/step_convert_to_template_test.go b/builder/proxmox/common/step_convert_to_template_test.go similarity index 100% rename from builder/proxmox/step_convert_to_template_test.go rename to builder/proxmox/common/step_convert_to_template_test.go diff --git a/builder/proxmox/step_finalize_template_config.go b/builder/proxmox/common/step_finalize_template_config.go similarity index 66% rename from builder/proxmox/step_finalize_template_config.go rename to builder/proxmox/common/step_finalize_template_config.go index 570c77b0b..0aebb9aca 100644 --- a/builder/proxmox/step_finalize_template_config.go +++ b/builder/proxmox/common/step_finalize_template_config.go @@ -38,24 +38,6 @@ func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.St // set, we need to clear it changes["description"] = c.TemplateDescription - if c.UnmountISO { - vmParams, err := client.GetVmConfig(vmRef) - if err != nil { - err := fmt.Errorf("Error fetching template config: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - - if vmParams["ide2"] == nil || !strings.HasSuffix(vmParams["ide2"].(string), "media=cdrom") { - err := fmt.Errorf("Cannot eject ISO from cdrom drive, ide2 is not present, or not a cdrom media") - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - changes["ide2"] = "none,media=cdrom" - } - if c.CloudInit { vmParams, err := client.GetVmConfig(vmRef) if err != nil { @@ -93,29 +75,6 @@ func (s *stepFinalizeTemplateConfig) Run(ctx context.Context, state multistep.St } } - if len(c.AdditionalISOFiles) > 0 { - vmParams, err := client.GetVmConfig(vmRef) - if err != nil { - err := fmt.Errorf("Error fetching template config: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - for idx := range c.AdditionalISOFiles { - cdrom := c.AdditionalISOFiles[idx].Device - if c.AdditionalISOFiles[idx].Unmount { - if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") { - err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - changes[cdrom] = "none,media=cdrom" - } else { - changes[cdrom] = c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom" - } - } - } if len(changes) > 0 { _, err := client.SetVmConfig(vmRef, changes) if err != nil { diff --git a/builder/proxmox/step_finalize_template_config_test.go b/builder/proxmox/common/step_finalize_template_config_test.go similarity index 84% rename from builder/proxmox/step_finalize_template_config_test.go rename to builder/proxmox/common/step_finalize_template_config_test.go index e045cb0ce..646e6f73d 100644 --- a/builder/proxmox/step_finalize_template_config_test.go +++ b/builder/proxmox/common/step_finalize_template_config_test.go @@ -56,18 +56,15 @@ func TestTemplateFinalize(t *testing.T) { builderConfig: &Config{ TemplateName: "my-template", TemplateDescription: "some-description", - UnmountISO: true, }, initialVMConfig: map[string]interface{}{ "name": "dummy", "description": "Packer ephemeral build VM", - "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", }, expectCallSetConfig: true, expectedVMConfig: map[string]interface{}{ "name": "my-template", "description": "some-description", - "ide2": "none,media=cdrom", }, expectedAction: multistep.ActionContinue, }, @@ -76,13 +73,11 @@ func TestTemplateFinalize(t *testing.T) { builderConfig: &Config{ TemplateName: "my-template", TemplateDescription: "some-description", - UnmountISO: true, CloudInit: true, }, initialVMConfig: map[string]interface{}{ "name": "dummy", "description": "Packer ephemeral build VM", - "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", "bootdisk": "virtio0", "virtio0": "ceph01:base-223-disk-0,cache=unsafe,media=disk,size=32G", }, @@ -90,7 +85,6 @@ func TestTemplateFinalize(t *testing.T) { expectedVMConfig: map[string]interface{}{ "name": "my-template", "description": "some-description", - "ide2": "none,media=cdrom", "ide3": "ceph01:cloudinit", }, expectedAction: multistep.ActionContinue, @@ -100,7 +94,6 @@ func TestTemplateFinalize(t *testing.T) { builderConfig: &Config{ TemplateName: "my-template", TemplateDescription: "some-description", - UnmountISO: false, CloudInit: true, }, initialVMConfig: map[string]interface{}{ @@ -116,27 +109,12 @@ func TestTemplateFinalize(t *testing.T) { expectCallSetConfig: false, expectedAction: multistep.ActionHalt, }, - { - name: "no cd-drive with unmount=true should returns halt", - builderConfig: &Config{ - TemplateName: "my-template", - TemplateDescription: "some-description", - UnmountISO: true, - }, - initialVMConfig: map[string]interface{}{ - "name": "dummy", - "description": "Packer ephemeral build VM", - "ide1": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", - }, - expectCallSetConfig: false, - expectedAction: multistep.ActionHalt, - }, { name: "GetVmConfig error should return halt", builderConfig: &Config{ TemplateName: "my-template", TemplateDescription: "some-description", - UnmountISO: true, + CloudInit: true, }, getConfigErr: fmt.Errorf("some error"), expectCallSetConfig: false, @@ -147,12 +125,10 @@ func TestTemplateFinalize(t *testing.T) { builderConfig: &Config{ TemplateName: "my-template", TemplateDescription: "some-description", - UnmountISO: true, }, initialVMConfig: map[string]interface{}{ "name": "dummy", "description": "Packer ephemeral build VM", - "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", }, expectCallSetConfig: true, setConfigErr: fmt.Errorf("some error"), diff --git a/builder/proxmox/step_start_vm.go b/builder/proxmox/common/step_start_vm.go similarity index 91% rename from builder/proxmox/step_start_vm.go rename to builder/proxmox/common/step_start_vm.go index 23ccfbf80..9b8a2b0f9 100644 --- a/builder/proxmox/step_start_vm.go +++ b/builder/proxmox/common/step_start_vm.go @@ -15,7 +15,13 @@ import ( // // It sets the vmRef state which is used throughout the later steps to reference the VM // in API calls. -type stepStartVM struct{} +type stepStartVM struct { + vmCreator ProxmoxVMCreator +} + +type ProxmoxVMCreator interface { + Create(*proxmox.VmRef, proxmox.ConfigQemu, multistep.StateBag) error +} func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { ui := state.Get("ui").(packer.Ui) @@ -32,8 +38,6 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist kvm = false } - isoFile := state.Get("iso_file").(string) - ui.Say("Creating VM") config := proxmox.ConfigQemu{ Name: c.VMName, @@ -47,7 +51,6 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist QemuSockets: c.Sockets, QemuOs: c.OS, QemuVga: generateProxmoxVga(c.VGA), - QemuIso: isoFile, QemuNetworks: generateProxmoxNetworkAdapters(c.NICs), QemuDisks: generateProxmoxDisks(c.Disks), Scsihw: c.SCSIController, @@ -78,8 +81,9 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist vmRef.SetPool(c.Pool) } - err := config.CreateVm(vmRef, client) + err := s.vmCreator.Create(vmRef, config, state) if err != nil { + err := fmt.Errorf("Error creating VM: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt @@ -91,19 +95,6 @@ func (s *stepStartVM) Run(ctx context.Context, state multistep.StateBag) multist // instance id inside of the provisioners, used in step_provision. state.Put("instance_id", vmRef) - for idx := range c.AdditionalISOFiles { - params := map[string]interface{}{ - c.AdditionalISOFiles[idx].Device: c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom", - } - _, err = client.SetVmConfig(vmRef, params) - if err != nil { - err := fmt.Errorf("Error configuring VM: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - } - ui.Say("Starting VM") _, err = client.StartVm(vmRef) if err != nil { diff --git a/builder/proxmox/step_start_vm_test.go b/builder/proxmox/common/step_start_vm_test.go similarity index 100% rename from builder/proxmox/step_start_vm_test.go rename to builder/proxmox/common/step_start_vm_test.go diff --git a/builder/proxmox/step_success.go b/builder/proxmox/common/step_success.go similarity index 100% rename from builder/proxmox/step_success.go rename to builder/proxmox/common/step_success.go diff --git a/builder/proxmox/step_type_boot_command.go b/builder/proxmox/common/step_type_boot_command.go similarity index 100% rename from builder/proxmox/step_type_boot_command.go rename to builder/proxmox/common/step_type_boot_command.go diff --git a/builder/proxmox/step_type_boot_command_test.go b/builder/proxmox/common/step_type_boot_command_test.go similarity index 99% rename from builder/proxmox/step_type_boot_command_test.go rename to builder/proxmox/common/step_type_boot_command_test.go index 99a759e80..c713e9cca 100644 --- a/builder/proxmox/step_type_boot_command_test.go +++ b/builder/proxmox/common/step_type_boot_command_test.go @@ -136,7 +136,7 @@ func TestTypeBootCommand(t *testing.T) { step := stepTypeBootCommand{ c.builderConfig.BootConfig, - c.builderConfig.ctx, + c.builderConfig.Ctx, } action := step.Run(context.TODO(), state) step.Cleanup(state) diff --git a/builder/proxmox/iso/builder.go b/builder/proxmox/iso/builder.go new file mode 100644 index 000000000..3c7fb960b --- /dev/null +++ b/builder/proxmox/iso/builder.go @@ -0,0 +1,76 @@ +package proxmoxiso + +import ( + "context" + + proxmoxapi "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/hcl/v2/hcldec" + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// The unique id for the builder +const BuilderID = "proxmox.iso" + +type Builder struct { + config Config +} + +// Builder implements packer.Builder +var _ packer.Builder = &Builder{} + +func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() } + +func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) { + return b.config.Prepare(raws...) +} + +const downloadPathKey = "downloaded_iso_path" + +func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) { + state := new(multistep.BasicStateBag) + state.Put("iso-config", &b.config) + + preSteps := []multistep.Step{ + &common.StepDownload{ + Checksum: b.config.ISOChecksum, + Description: "ISO", + Extension: b.config.TargetExtension, + ResultKey: downloadPathKey, + TargetPath: b.config.TargetPath, + Url: b.config.ISOUrls, + }, + } + for idx := range b.config.AdditionalISOFiles { + preSteps = append(preSteps, &common.StepDownload{ + Checksum: b.config.AdditionalISOFiles[idx].ISOChecksum, + Description: "additional ISO", + Extension: b.config.AdditionalISOFiles[idx].TargetExtension, + ResultKey: b.config.AdditionalISOFiles[idx].DownloadPathKey, + TargetPath: b.config.AdditionalISOFiles[idx].DownloadPathKey, + Url: b.config.AdditionalISOFiles[idx].ISOUrls, + }) + } + preSteps = append(preSteps, + &stepUploadISO{}, + &stepUploadAdditionalISOs{}, + ) + postSteps := []multistep.Step{ + &stepFinalizeISOTemplate{}, + } + + sb := proxmox.NewSharedBuilder(BuilderID, b.config.Config, preSteps, postSteps, &isoVMCreator{}) + return sb.Run(ctx, ui, hook, state) +} + +type isoVMCreator struct{} + +func (*isoVMCreator) Create(vmRef *proxmoxapi.VmRef, config proxmoxapi.ConfigQemu, state multistep.StateBag) error { + isoFile := state.Get("iso_file").(string) + config.QemuIso = isoFile + + client := state.Get("proxmoxClient").(*proxmoxapi.Client) + return config.CreateVm(vmRef, client) +} diff --git a/builder/proxmox/iso/config.go b/builder/proxmox/iso/config.go new file mode 100644 index 000000000..a04c41c05 --- /dev/null +++ b/builder/proxmox/iso/config.go @@ -0,0 +1,114 @@ +//go:generate mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig + +package proxmoxiso + +import ( + "errors" + "fmt" + "log" + "strconv" + "strings" + + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/hashicorp/packer/common" + "github.com/hashicorp/packer/packer" +) + +type Config struct { + proxmox.Config `mapstructure:",squash"` + + common.ISOConfig `mapstructure:",squash"` + ISOFile string `mapstructure:"iso_file"` + ISOStoragePool string `mapstructure:"iso_storage_pool"` + UnmountISO bool `mapstructure:"unmount_iso"` + shouldUploadISO bool +} + +func (c *Config) Prepare(raws ...interface{}) ([]string, []string, error) { + var errs *packer.MultiError + _, warnings, merrs := c.Config.Prepare(c, raws...) + if merrs != nil { + errs = packer.MultiErrorAppend(errs, merrs) + } + + // 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")) + } + + for idx := range c.AdditionalISOFiles { + // Check AdditionalISO 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.AdditionalISOFiles[idx].ISOFile != "" { + c.AdditionalISOFiles[idx].ShouldUploadISO = false + } else { + c.AdditionalISOFiles[idx].DownloadPathKey = "downloaded_additional_iso_path_" + strconv.Itoa(idx) + isoWarnings, isoErrors := c.AdditionalISOFiles[idx].ISOConfig.Prepare(&c.Ctx) + errs = packer.MultiErrorAppend(errs, isoErrors...) + warnings = append(warnings, isoWarnings...) + c.AdditionalISOFiles[idx].ShouldUploadISO = true + } + if c.AdditionalISOFiles[idx].Device == "" { + log.Printf("AdditionalISOFile %d Device not set, using default 'ide3'", idx) + c.AdditionalISOFiles[idx].Device = "ide3" + } + if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "ide") { + busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[3:]) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[3:])) + } + if busnumber == 2 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus 2 is used by boot ISO")) + } + if busnumber > 3 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("IDE bus index can't be higher than 3")) + } + } + if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "sata") { + busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:]) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:])) + } + if busnumber > 5 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("SATA bus index can't be higher than 5")) + } + } + if strings.HasPrefix(c.AdditionalISOFiles[idx].Device, "scsi") { + busnumber, err := strconv.Atoi(c.AdditionalISOFiles[idx].Device[4:]) + if err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("%s is not a valid bus index", c.AdditionalISOFiles[idx].Device[4:])) + } + if busnumber > 30 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("SCSI bus index can't be higher than 30")) + } + } + if (c.AdditionalISOFiles[idx].ISOFile == "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) == 0) || (c.AdditionalISOFiles[idx].ISOFile != "" && len(c.AdditionalISOFiles[idx].ISOConfig.ISOUrls) != 0) { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("either iso_file or iso_url, but not both, must be specified for AdditionalISO file %s", c.AdditionalISOFiles[idx].Device)) + } + 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")) + } + } + + if errs != nil && len(errs.Errors) > 0 { + return nil, warnings, errs + } + return nil, warnings, nil +} diff --git a/builder/proxmox/iso/config.hcl2spec.go b/builder/proxmox/iso/config.hcl2spec.go new file mode 100644 index 000000000..391d0a106 --- /dev/null +++ b/builder/proxmox/iso/config.hcl2spec.go @@ -0,0 +1,223 @@ +// Code generated by "mapstructure-to-hcl2 -type Config,nicConfig,diskConfig,vgaConfig"; DO NOT EDIT. +package proxmoxiso + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + proxmox "github.com/hashicorp/packer/builder/proxmox/common" + "github.com/zclconf/go-cty/cty" +) + +// FlatConfig is an auto-generated flat version of Config. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatConfig struct { + PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` + PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` + PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"` + PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"` + PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"` + PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"` + PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"` + HTTPDir *string `mapstructure:"http_directory" cty:"http_directory" hcl:"http_directory"` + HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"` + HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"` + HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"` + HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"` + BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"` + BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"` + BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"` + BootKeyInterval *string `mapstructure:"boot_key_interval" cty:"boot_key_interval" hcl:"boot_key_interval"` + Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"` + PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"` + SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"` + SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"` + SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"` + SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"` + SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"` + SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"` + SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"` + SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"` + SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"` + SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"` + SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"` + SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"` + SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"` + SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"` + SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"` + SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"` + SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"` + SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"` + SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"` + SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"` + SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"` + SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"` + SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"` + SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"` + SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"` + SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"` + SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"` + SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"` + SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"` + SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"` + SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"` + SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"` + SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"` + SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"` + SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"` + SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"` + WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"` + WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"` + WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"` + WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"` + WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"` + WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"` + WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"` + WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"` + WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"` + ProxmoxURLRaw *string `mapstructure:"proxmox_url" cty:"proxmox_url" hcl:"proxmox_url"` + SkipCertValidation *bool `mapstructure:"insecure_skip_tls_verify" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"` + Username *string `mapstructure:"username" cty:"username" hcl:"username"` + Password *string `mapstructure:"password" cty:"password" hcl:"password"` + Node *string `mapstructure:"node" cty:"node" hcl:"node"` + Pool *string `mapstructure:"pool" cty:"pool" hcl:"pool"` + VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"` + VMID *int `mapstructure:"vm_id" cty:"vm_id" hcl:"vm_id"` + Memory *int `mapstructure:"memory" cty:"memory" hcl:"memory"` + Cores *int `mapstructure:"cores" cty:"cores" hcl:"cores"` + CPUType *string `mapstructure:"cpu_type" cty:"cpu_type" hcl:"cpu_type"` + Sockets *int `mapstructure:"sockets" cty:"sockets" hcl:"sockets"` + OS *string `mapstructure:"os" cty:"os" hcl:"os"` + VGA *proxmox.FlatvgaConfig `mapstructure:"vga" cty:"vga" hcl:"vga"` + NICs []proxmox.FlatnicConfig `mapstructure:"network_adapters" cty:"network_adapters" hcl:"network_adapters"` + Disks []proxmox.FlatdiskConfig `mapstructure:"disks" cty:"disks" hcl:"disks"` + Agent *bool `mapstructure:"qemu_agent" cty:"qemu_agent" hcl:"qemu_agent"` + SCSIController *string `mapstructure:"scsi_controller" cty:"scsi_controller" hcl:"scsi_controller"` + Onboot *bool `mapstructure:"onboot" cty:"onboot" hcl:"onboot"` + DisableKVM *bool `mapstructure:"disable_kvm" cty:"disable_kvm" hcl:"disable_kvm"` + TemplateName *string `mapstructure:"template_name" cty:"template_name" hcl:"template_name"` + TemplateDescription *string `mapstructure:"template_description" cty:"template_description" hcl:"template_description"` + CloudInit *bool `mapstructure:"cloud_init" cty:"cloud_init" hcl:"cloud_init"` + CloudInitStoragePool *string `mapstructure:"cloud_init_storage_pool" cty:"cloud_init_storage_pool" hcl:"cloud_init_storage_pool"` + AdditionalISOFiles []proxmox.FlatstorageConfig `mapstructure:"additional_iso_files" cty:"additional_iso_files" hcl:"additional_iso_files"` + VMInterface *string `mapstructure:"vm_interface" cty:"vm_interface" hcl:"vm_interface"` + ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"` + RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"` + ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"` + TargetPath *string `mapstructure:"iso_target_path" cty:"iso_target_path" hcl:"iso_target_path"` + TargetExtension *string `mapstructure:"iso_target_extension" cty:"iso_target_extension" hcl:"iso_target_extension"` + ISOFile *string `mapstructure:"iso_file" cty:"iso_file" hcl:"iso_file"` + ISOStoragePool *string `mapstructure:"iso_storage_pool" cty:"iso_storage_pool" hcl:"iso_storage_pool"` + UnmountISO *bool `mapstructure:"unmount_iso" cty:"unmount_iso" hcl:"unmount_iso"` +} + +// FlatMapstructure returns a new FlatConfig. +// FlatConfig is an auto-generated flat version of Config. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatConfig) +} + +// HCL2Spec returns the hcl spec of a Config. +// This spec is used by HCL to read the fields of Config. +// The decoded values from this spec will then be applied to a FlatConfig. +func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, + "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, + "packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false}, + "packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false}, + "packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false}, + "packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false}, + "packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(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_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false}, + "http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false}, + "http_interface": &hcldec.AttrSpec{Name: "http_interface", 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}, + "boot_key_interval": &hcldec.AttrSpec{Name: "boot_key_interval", Type: cty.String, Required: false}, + "communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false}, + "pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false}, + "ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false}, + "ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false}, + "ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false}, + "ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false}, + "ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false}, + "temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false}, + "ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false}, + "ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false}, + "ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false}, + "ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false}, + "ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false}, + "ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false}, + "ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false}, + "ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false}, + "ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false}, + "ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false}, + "ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false}, + "ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false}, + "ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false}, + "ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false}, + "ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false}, + "ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false}, + "ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false}, + "ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false}, + "ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false}, + "ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false}, + "ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false}, + "ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false}, + "ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false}, + "ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false}, + "ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false}, + "ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false}, + "ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false}, + "ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false}, + "ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false}, + "winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false}, + "winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false}, + "winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false}, + "winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false}, + "winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false}, + "winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false}, + "winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false}, + "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, + "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, + "proxmox_url": &hcldec.AttrSpec{Name: "proxmox_url", Type: cty.String, Required: false}, + "insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false}, + "username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false}, + "password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false}, + "node": &hcldec.AttrSpec{Name: "node", Type: cty.String, Required: false}, + "pool": &hcldec.AttrSpec{Name: "pool", Type: cty.String, Required: false}, + "vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false}, + "vm_id": &hcldec.AttrSpec{Name: "vm_id", Type: cty.Number, Required: false}, + "memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false}, + "cores": &hcldec.AttrSpec{Name: "cores", Type: cty.Number, Required: false}, + "cpu_type": &hcldec.AttrSpec{Name: "cpu_type", Type: cty.String, Required: false}, + "sockets": &hcldec.AttrSpec{Name: "sockets", Type: cty.Number, Required: false}, + "os": &hcldec.AttrSpec{Name: "os", Type: cty.String, Required: false}, + "vga": &hcldec.BlockSpec{TypeName: "vga", Nested: hcldec.ObjectSpec((*proxmox.FlatvgaConfig)(nil).HCL2Spec())}, + "network_adapters": &hcldec.BlockListSpec{TypeName: "network_adapters", Nested: hcldec.ObjectSpec((*proxmox.FlatnicConfig)(nil).HCL2Spec())}, + "disks": &hcldec.BlockListSpec{TypeName: "disks", Nested: hcldec.ObjectSpec((*proxmox.FlatdiskConfig)(nil).HCL2Spec())}, + "qemu_agent": &hcldec.AttrSpec{Name: "qemu_agent", Type: cty.Bool, Required: false}, + "scsi_controller": &hcldec.AttrSpec{Name: "scsi_controller", Type: cty.String, Required: false}, + "onboot": &hcldec.AttrSpec{Name: "onboot", Type: cty.Bool, Required: false}, + "disable_kvm": &hcldec.AttrSpec{Name: "disable_kvm", Type: cty.Bool, Required: false}, + "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, + "template_description": &hcldec.AttrSpec{Name: "template_description", Type: cty.String, Required: false}, + "cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false}, + "cloud_init_storage_pool": &hcldec.AttrSpec{Name: "cloud_init_storage_pool", Type: cty.String, Required: false}, + "additional_iso_files": &hcldec.BlockListSpec{TypeName: "additional_iso_files", Nested: hcldec.ObjectSpec((*proxmox.FlatstorageConfig)(nil).HCL2Spec())}, + "vm_interface": &hcldec.AttrSpec{Name: "vm_interface", Type: cty.String, Required: false}, + "iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", 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}, + "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}, + "unmount_iso": &hcldec.AttrSpec{Name: "unmount_iso", Type: cty.Bool, Required: false}, + } + return s +} diff --git a/builder/proxmox/config_test.go b/builder/proxmox/iso/config_test.go similarity index 88% rename from builder/proxmox/config_test.go rename to builder/proxmox/iso/config_test.go index 39019b85e..fe519a05b 100644 --- a/builder/proxmox/config_test.go +++ b/builder/proxmox/iso/config_test.go @@ -1,55 +1,17 @@ -package proxmox +package proxmoxiso import ( "strings" "testing" - "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template" ) -func mandatoryConfig(t *testing.T) map[string]interface{} { - return map[string]interface{}{ - "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", - "username": "apiuser@pve", - "password": "supersecret", - "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso", - "node": "my-proxmox", - "ssh_username": "root", - } -} - -func TestRequiredParameters(t *testing.T) { - var c Config - _, err := c.Prepare(make(map[string]interface{})) - if err == nil { - t.Fatal("Expected empty configuration to fail") - } - errs, ok := err.(*packer.MultiError) - if !ok { - t.Fatal("Expected errors to be packer.MultiError") - } - - required := []string{"username", "password", "proxmox_url", "iso_file", "node", "ssh_username"} - for _, param := range required { - found := false - for _, err := range errs.Errors { - if strings.Contains(err.Error(), param) { - found = true - break - } - } - if !found { - t.Errorf("Expected error about missing parameter %q", required) - } - } -} - func TestBasicExampleFromDocsIsValid(t *testing.T) { const config = `{ "builders": [ { - "type": "proxmox", + "type": "proxmox-iso", "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", "insecure_skip_tls_verify": true, "username": "apiuser@pve", @@ -93,9 +55,9 @@ func TestBasicExampleFromDocsIsValid(t *testing.T) { } b := &Builder{} - _, warn, err := b.Prepare(tpl.Builders["proxmox"].Config) + _, _, err = b.Prepare(tpl.Builders["proxmox-iso"].Config) if err != nil { - t.Fatal(err, warn) + t.Fatal(err) } // The example config does not set a number of optional fields. Validate that: @@ -154,7 +116,7 @@ func TestAgentSetToFalse(t *testing.T) { cfg["qemu_agent"] = false var c Config - warn, err := c.Prepare(cfg) + _, warn, err := c.Prepare(cfg) if err != nil { t.Fatal(err, warn) } @@ -197,7 +159,7 @@ func TestPacketQueueSupportForNetworkAdapters(t *testing.T) { cfg["network_adapters"] = devices var c Config - _, err := c.Prepare(cfg) + _, _, err := c.Prepare(cfg) if tt.expectedToFail == true && err == nil { t.Error("expected config preparation to fail, but no error occured") @@ -246,7 +208,7 @@ func TestHardDiskControllerIOThreadSupport(t *testing.T) { cfg["scsi_controller"] = tt.controller var c Config - _, err := c.Prepare(cfg) + _, _, err := c.Prepare(cfg) if tt.expectedToFail == true && err == nil { t.Error("expected config preparation to fail, but no error occured") @@ -257,3 +219,14 @@ func TestHardDiskControllerIOThreadSupport(t *testing.T) { } } } + +func mandatoryConfig(t *testing.T) map[string]interface{} { + return map[string]interface{}{ + "proxmox_url": "https://my-proxmox.my-domain:8006/api2/json", + "username": "apiuser@pve", + "password": "supersecret", + "node": "my-proxmox", + "ssh_username": "root", + "iso_file": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso", + } +} diff --git a/builder/proxmox/iso/step_finalize_iso.go b/builder/proxmox/iso/step_finalize_iso.go new file mode 100644 index 000000000..88ef44bd2 --- /dev/null +++ b/builder/proxmox/iso/step_finalize_iso.go @@ -0,0 +1,87 @@ +package proxmoxiso + +import ( + "context" + "fmt" + "strings" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// stepFinalizeISOTemplate does any ISO-builder specific modifications after +// conversion to a template, and after the non-specific modifications in +// common.stepFinalizeTemplateConfig +type stepFinalizeISOTemplate struct{} + +type templateFinalizer interface { + GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) + SetVmConfig(*proxmox.VmRef, map[string]interface{}) (interface{}, error) +} + +var _ templateFinalizer = &proxmox.Client{} + +func (s *stepFinalizeISOTemplate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + client := state.Get("proxmoxClient").(templateFinalizer) + c := state.Get("iso-config").(*Config) + vmRef := state.Get("vmRef").(*proxmox.VmRef) + + changes := make(map[string]interface{}) + + if c.UnmountISO { + vmParams, err := client.GetVmConfig(vmRef) + if err != nil { + err := fmt.Errorf("Error fetching template config: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + if vmParams["ide2"] == nil || !strings.HasSuffix(vmParams["ide2"].(string), "media=cdrom") { + err := fmt.Errorf("Cannot eject ISO from cdrom drive, ide2 is not present, or not a cdrom media") + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + changes["ide2"] = "none,media=cdrom" + } + if len(c.AdditionalISOFiles) > 0 { + vmParams, err := client.GetVmConfig(vmRef) + if err != nil { + err := fmt.Errorf("Error fetching template config: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + for idx := range c.AdditionalISOFiles { + cdrom := c.AdditionalISOFiles[idx].Device + if c.AdditionalISOFiles[idx].Unmount { + if vmParams[cdrom] == nil || !strings.Contains(vmParams[cdrom].(string), "media=cdrom") { + err := fmt.Errorf("Cannot eject ISO from cdrom drive, %s is not present or not a cdrom media", cdrom) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + changes[cdrom] = "none,media=cdrom" + } else { + changes[cdrom] = c.AdditionalISOFiles[idx].ISOFile + ",media=cdrom" + } + } + } + + if len(changes) > 0 { + _, err := client.SetVmConfig(vmRef, changes) + if err != nil { + err := fmt.Errorf("Error updating template: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + } + + return multistep.ActionContinue +} + +func (s *stepFinalizeISOTemplate) Cleanup(state multistep.StateBag) { +} diff --git a/builder/proxmox/iso/step_finalize_iso_test.go b/builder/proxmox/iso/step_finalize_iso_test.go new file mode 100644 index 000000000..a35a7f5a2 --- /dev/null +++ b/builder/proxmox/iso/step_finalize_iso_test.go @@ -0,0 +1,131 @@ +package proxmoxiso + +import ( + "context" + "fmt" + "testing" + + "github.com/Telmate/proxmox-api-go/proxmox" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +type finalizerMock struct { + getConfig func() (map[string]interface{}, error) + setConfig func(map[string]interface{}) (string, error) +} + +func (m finalizerMock) GetVmConfig(*proxmox.VmRef) (map[string]interface{}, error) { + return m.getConfig() +} +func (m finalizerMock) SetVmConfig(vmref *proxmox.VmRef, c map[string]interface{}) (interface{}, error) { + return m.setConfig(c) +} + +var _ templateFinalizer = finalizerMock{} + +func TestISOTemplateFinalize(t *testing.T) { + cs := []struct { + name string + builderConfig *Config + initialVMConfig map[string]interface{} + getConfigErr error + expectCallSetConfig bool + expectedVMConfig map[string]interface{} + setConfigErr error + expectedAction multistep.StepAction + }{ + { + name: "default config does nothing", + builderConfig: &Config{}, + initialVMConfig: map[string]interface{}{ + "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", + }, + expectCallSetConfig: false, + expectedVMConfig: map[string]interface{}{ + "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", + }, + expectedAction: multistep.ActionContinue, + }, + { + name: "should unmount when configured", + builderConfig: &Config{ + UnmountISO: true, + }, + initialVMConfig: map[string]interface{}{ + "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", + }, + expectCallSetConfig: true, + expectedVMConfig: map[string]interface{}{ + "ide2": "none,media=cdrom", + }, + expectedAction: multistep.ActionContinue, + }, + { + name: "no cd-drive with unmount=true should returns halt", + builderConfig: &Config{ + UnmountISO: true, + }, + initialVMConfig: map[string]interface{}{ + "ide1": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", + }, + expectCallSetConfig: false, + expectedAction: multistep.ActionHalt, + }, + { + name: "GetVmConfig error should return halt", + builderConfig: &Config{ + UnmountISO: true, + }, + getConfigErr: fmt.Errorf("some error"), + expectCallSetConfig: false, + expectedAction: multistep.ActionHalt, + }, + { + name: "SetVmConfig error should return halt", + builderConfig: &Config{ + UnmountISO: true, + }, + initialVMConfig: map[string]interface{}{ + "ide2": "local:iso/Fedora-Server-dvd-x86_64-29-1.2.iso,media=cdrom", + }, + expectCallSetConfig: true, + setConfigErr: fmt.Errorf("some error"), + expectedAction: multistep.ActionHalt, + }, + } + + for _, c := range cs { + t.Run(c.name, func(t *testing.T) { + finalizer := finalizerMock{ + getConfig: func() (map[string]interface{}, error) { + return c.initialVMConfig, c.getConfigErr + }, + setConfig: func(cfg map[string]interface{}) (string, error) { + if !c.expectCallSetConfig { + t.Error("Did not expect SetVmConfig to be called") + } + for key, val := range c.expectedVMConfig { + if cfg[key] != val { + t.Errorf("Expected %q to be %q, got %q", key, val, cfg[key]) + } + } + + return "", c.setConfigErr + }, + } + + state := new(multistep.BasicStateBag) + state.Put("ui", packer.TestUi(t)) + state.Put("iso-config", c.builderConfig) + state.Put("vmRef", proxmox.NewVmRef(1)) + state.Put("proxmoxClient", finalizer) + + step := stepFinalizeISOTemplate{} + action := step.Run(context.TODO(), state) + if action != c.expectedAction { + t.Errorf("Expected action to be %v, got %v", c.expectedAction, action) + } + }) + } +} diff --git a/builder/proxmox/step_upload_additional_isos.go b/builder/proxmox/iso/step_upload_additional_isos.go similarity index 84% rename from builder/proxmox/step_upload_additional_isos.go rename to builder/proxmox/iso/step_upload_additional_isos.go index b867032c0..f23b949be 100644 --- a/builder/proxmox/step_upload_additional_isos.go +++ b/builder/proxmox/iso/step_upload_additional_isos.go @@ -1,9 +1,8 @@ -package proxmox +package proxmoxiso import ( "context" "fmt" - "io" "os" "path/filepath" @@ -16,24 +15,20 @@ import ( // to the VM type stepUploadAdditionalISOs struct{} -type uploader interface { - Upload(node string, storage string, contentType string, filename string, file io.Reader) error -} - var _ uploader = &proxmox.Client{} func (s *stepUploadAdditionalISOs) 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) + c := state.Get("iso-config").(*Config) for idx := range c.AdditionalISOFiles { - if !c.AdditionalISOFiles[idx].shouldUploadISO { + if !c.AdditionalISOFiles[idx].ShouldUploadISO { state.Put("additional_iso_files", c.AdditionalISOFiles) continue } - p := state.Get(c.AdditionalISOFiles[idx].downloadPathKey).(string) + p := state.Get(c.AdditionalISOFiles[idx].DownloadPathKey).(string) if p == "" { err := fmt.Errorf("Path to downloaded ISO was empty") state.Put("erroe", err) diff --git a/builder/proxmox/step_upload_iso.go b/builder/proxmox/iso/step_upload_iso.go similarity index 88% rename from builder/proxmox/step_upload_iso.go rename to builder/proxmox/iso/step_upload_iso.go index de947075c..79096bfe0 100644 --- a/builder/proxmox/step_upload_iso.go +++ b/builder/proxmox/iso/step_upload_iso.go @@ -1,8 +1,9 @@ -package proxmox +package proxmoxiso import ( "context" "fmt" + "io" "os" "path/filepath" @@ -14,12 +15,16 @@ import ( // 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) + c := state.Get("iso-config").(*Config) if !c.shouldUploadISO { state.Put("iso_file", c.ISOFile) @@ -53,6 +58,7 @@ func (s *stepUploadISO) Run(ctx context.Context, state multistep.StateBag) multi isoStoragePath := fmt.Sprintf("%s:iso/%s", c.ISOStoragePool, filename) state.Put("iso_file", isoStoragePath) + return multistep.ActionContinue } diff --git a/builder/proxmox/step_upload_iso_test.go b/builder/proxmox/iso/step_upload_iso_test.go similarity index 98% rename from builder/proxmox/step_upload_iso_test.go rename to builder/proxmox/iso/step_upload_iso_test.go index a009d6686..418005a0c 100644 --- a/builder/proxmox/step_upload_iso_test.go +++ b/builder/proxmox/iso/step_upload_iso_test.go @@ -1,4 +1,4 @@ -package proxmox +package proxmoxiso import ( "context" @@ -106,7 +106,7 @@ func TestUploadISO(t *testing.T) { state := new(multistep.BasicStateBag) state.Put("ui", packer.TestUi(t)) - state.Put("config", c.builderConfig) + state.Put("iso-config", c.builderConfig) state.Put(downloadPathKey, c.downloadPath) state.Put("proxmoxClient", m) diff --git a/builder/proxmox/testdata/test.iso b/builder/proxmox/iso/testdata/test.iso similarity index 100% rename from builder/proxmox/testdata/test.iso rename to builder/proxmox/iso/testdata/test.iso diff --git a/command/plugin.go b/command/plugin.go index 82a4ad016..b3bf09bc7 100644 --- a/command/plugin.go +++ b/command/plugin.go @@ -49,6 +49,8 @@ import ( parallelspvmbuilder "github.com/hashicorp/packer/builder/parallels/pvm" profitbricksbuilder "github.com/hashicorp/packer/builder/profitbricks" proxmoxbuilder "github.com/hashicorp/packer/builder/proxmox" + proxmoxclonebuilder "github.com/hashicorp/packer/builder/proxmox/clone" + proxmoxisobuilder "github.com/hashicorp/packer/builder/proxmox/iso" qemubuilder "github.com/hashicorp/packer/builder/qemu" scalewaybuilder "github.com/hashicorp/packer/builder/scaleway" tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm" @@ -146,6 +148,8 @@ var Builders = map[string]packer.Builder{ "parallels-pvm": new(parallelspvmbuilder.Builder), "profitbricks": new(profitbricksbuilder.Builder), "proxmox": new(proxmoxbuilder.Builder), + "proxmox-clone": new(proxmoxclonebuilder.Builder), + "proxmox-iso": new(proxmoxisobuilder.Builder), "qemu": new(qemubuilder.Builder), "scaleway": new(scalewaybuilder.Builder), "tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder), diff --git a/fix/fixer.go b/fix/fixer.go index cf50d0c30..7634a287c 100644 --- a/fix/fixer.go +++ b/fix/fixer.go @@ -59,6 +59,7 @@ func init() { "iso-checksum-type-and-url": new(FixerISOChecksumTypeAndURL), "qemu-host-port": new(FixerQEMUHostPort), "azure-exclude_from_latest": new(FixerAzureExcludeFromLatest), + "proxmox-type": new(FixerProxmoxType), } FixerOrder = []string{ @@ -95,5 +96,6 @@ func init() { "iso-checksum-type-and-url", "qemu-host-port", "azure-exclude_from_latest", + "proxmox-type", } } diff --git a/fix/fixer_proxmox_type.go b/fix/fixer_proxmox_type.go new file mode 100644 index 000000000..e1faf8bcf --- /dev/null +++ b/fix/fixer_proxmox_type.go @@ -0,0 +1,49 @@ +package fix + +import ( + "github.com/mitchellh/mapstructure" +) + +// FixerProxmoxType updates proxmox builder types to proxmox-iso +type FixerProxmoxType struct{} + +func (FixerProxmoxType) DeprecatedOptions() []string { + return []string{} +} + +func (FixerProxmoxType) Fix(input map[string]interface{}) (map[string]interface{}, error) { + type template struct { + Builders []map[string]interface{} + } + + // Decode the input into our structure, if we can + var tpl template + if err := mapstructure.Decode(input, &tpl); err != nil { + return nil, err + } + + for _, builder := range tpl.Builders { + builderTypeRaw, ok := builder["type"] + if !ok { + continue + } + + builderType, ok := builderTypeRaw.(string) + if !ok { + continue + } + + if builderType != "proxmox" { + continue + } + + builder["type"] = "proxmox-iso" + } + + input["builders"] = tpl.Builders + return input, nil +} + +func (FixerProxmoxType) Synopsis() string { + return `Updates the builder type proxmox to proxmox-iso` +} diff --git a/fix/fixer_proxmox_type_test.go b/fix/fixer_proxmox_type_test.go new file mode 100644 index 000000000..c66ab25bb --- /dev/null +++ b/fix/fixer_proxmox_type_test.go @@ -0,0 +1,73 @@ +package fix + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFixerProxmoxType_Impl(t *testing.T) { + var raw interface{} + raw = new(FixerProxmoxType) + if _, ok := raw.(Fixer); !ok { + t.Fatalf("must be a Fixer") + } +} + +func TestFixerProxmoxType_Fix(t *testing.T) { + + cases := []struct { + Input map[string]interface{} + Expected map[string]interface{} + }{ + + { + Input: map[string]interface{}{ + "type": "proxmox", + }, + + Expected: map[string]interface{}{ + "type": "proxmox-iso", + }, + }, + + { + Input: map[string]interface{}{ + "type": "proxmox-iso", + }, + + Expected: map[string]interface{}{ + "type": "proxmox-iso", + }, + }, + + { + Input: map[string]interface{}{ + "type": "proxmox-clone", + }, + + Expected: map[string]interface{}{ + "type": "proxmox-clone", + }, + }, + } + + for _, tc := range cases { + var f FixerProxmoxType + + input := map[string]interface{}{ + "builders": []map[string]interface{}{tc.Input}, + } + + expected := map[string]interface{}{ + "builders": []map[string]interface{}{tc.Expected}, + } + + output, err := f.Fix(input) + if err != nil { + t.Fatalf("err: %s", err) + } + + assert.Equal(t, expected, output, "Should be equal") + } +} diff --git a/scripts/generate-plugins.go b/scripts/generate-plugins.go index 9b79e3ee2..8fe8feddd 100644 --- a/scripts/generate-plugins.go +++ b/scripts/generate-plugins.go @@ -136,15 +136,17 @@ func listDirectories(path string) ([]string, error) { for _, item := range items { // We only want directories - if item.IsDir() { - currentDir := filepath.Join(path, item.Name()) - names = append(names, currentDir) + if !item.IsDir() || + item.Name() == "common" { + continue + } + currentDir := filepath.Join(path, item.Name()) + names = append(names, currentDir) - // Do some recursion - subNames, err := listDirectories(currentDir) - if err == nil { - names = append(names, subNames...) - } + // Do some recursion + subNames, err := listDirectories(currentDir) + if err == nil { + names = append(names, subNames...) } } diff --git a/website/data/docs-navigation.js b/website/data/docs-navigation.js index b4da5a875..d1ff63882 100644 --- a/website/data/docs-navigation.js +++ b/website/data/docs-navigation.js @@ -217,7 +217,7 @@ export default [ }, { category: 'parallels', content: ['iso', 'pvm'] }, 'profitbricks', - 'proxmox', + { category: 'proxmox', content: ['iso', 'clone'] }, 'qemu', 'scaleway', 'tencentcloud-cvm', diff --git a/website/pages/docs/builders/proxmox/clone.mdx b/website/pages/docs/builders/proxmox/clone.mdx new file mode 100644 index 000000000..df5b3fc14 --- /dev/null +++ b/website/pages/docs/builders/proxmox/clone.mdx @@ -0,0 +1,234 @@ +--- +description: | + The proxmox image Packer builder is able to create new images for use with + Proxmox VE. The builder takes a cloud-init enabled virtual machine + template name, runs any provisioning necessary on the image after + launching it, then creates a virtual machine template. +layout: docs +page_title: Proxmox Clone - Builders +sidebar_title: Clone +--- + +# Proxmox Builder (from an image) + +Type: `proxmox-clone` + +The `proxmox-clone` Packer builder is able to create new images for use with +[Proxmox](https://www.proxmox.com/en/proxmox-ve). The builder takes a virtual +machine template, runs any provisioning necessary on the image after launching it, +then creates a virtual machine template. This template can then be used as to +create new virtual machines within Proxmox. + +The builder does _not_ manage templates. Once it creates a template, it is up +to you to use it or delete it. + +## Configuration Reference + +There are many configuration options available for the builder. They are +segmented below into two categories: required and optional parameters. Within +each category, the available configuration keys are alphabetized. + +In addition to the options listed here, a +[communicator](/docs/templates/communicator) can be configured for this +builder. + +If no communicator is defined, an SSH key is generated for use, and is used +in the image's Cloud-Init settings for provisioning. + +### Required: + +- `proxmox_url` (string) - URL to the Proxmox API, including the full path, + so `https://:/api2/json` for example. + Can also be set via the `PROXMOX_URL` environment variable. + +- `username` (string) - Username when authenticating to Proxmox, including + the realm. For example `user@pve` to use the local Proxmox realm. + Can also be set via the `PROXMOX_USERNAME` environment variable. + +- `password` (string) - Password for the user. + Can also be set via the `PROXMOX_PASSWORD` environment variable. + +- `node` (string) - Which node in the Proxmox cluster to start the virtual + machine on during creation. + +- `clone_vm` (string) - The name of the VM packer should clone and build from. + +### Optional: + +- `insecure_skip_tls_verify` (bool) - Skip validating the certificate. + +- `pool` (string) - Name of resource pool to create virtual machine in. + +- `vm_name` (string) - Name of the virtual machine during creation. If not + given, a random uuid will be used. + +- `vm_id` (int) - The ID used to reference the virtual machine. This will + also be the ID of the final template. If not given, the next free ID on + the node will be used. + +- `memory` (int) - How much memory, in megabytes, to give the virtual + machine. Defaults to `512`. + +- `cores` (int) - How many CPU cores to give the virtual machine. Defaults + to `1`. + +- `sockets` (int) - How many CPU sockets to give the virtual machine. + Defaults to `1` + +- `cpu_type` (string) - The CPU type to emulate. See the Proxmox API + documentation for the complete list of accepted values. For best + performance, set this to `host`. Defaults to `kvm64`. + +- `os` (string) - The operating system. Can be `wxp`, `w2k`, `w2k3`, `w2k8`, + `wvista`, `win7`, `win8`, `win10`, `l24` (Linux 2.4), `l26` (Linux 2.6+), + `solaris` or `other`. Defaults to `other`. + +- `vga` (object) - The graphics adapter to use. Example: + + ```json + { + "type": "vmware", + "memory": 32 + } + ``` + + - `type` (string) - Can be `cirrus`, `none`, `qxl`,`qxl2`, `qxl3`, + `qxl4`, `serial0`, `serial1`, `serial2`, `serial3`, `std`, `virtio`, `vmware`. + Defaults to `std`. + + - `memory` (int) - How much memory to assign. + +- `network_adapters` (array of objects) - Network adapters attached to the + virtual machine. Example: + + ```json + [ + { + "model": "virtio", + "bridge": "vmbr0", + "vlan_tag": "10", + "firewall": true + } + ] + ``` + + - `bridge` (string) - Required. Which Proxmox bridge to attach the + adapter to. + + - `model` (string) - Model of the virtual network adapter. Can be + `rtl8139`, `ne2k_pci`, `e1000`, `pcnet`, `virtio`, `ne2k_isa`, + `i82551`, `i82557b`, `i82559er`, `vmxnet3`, `e1000-82540em`, + `e1000-82544gc` or `e1000-82545em`. Defaults to `e1000`. + + - `mac_address` (string) - Give the adapter a specific MAC address. If + not set, defaults to a random MAC. + + - `vlan_tag` (string) - If the adapter should tag packets. Defaults to + no tagging. + + - `firewall` (bool) - If the interface should be protected by the firewall. + Defaults to `false`. + + - `packet_queues` (int) - Number of packet queues to be used on the device. + Values greater than 1 indicate that the multiqueue feature is activated. + For best performance, set this to the number of cores available to the + virtual machine. CPU load on the host and guest systems will increase as + the traffic increases, so activate this option only when the VM has to + handle a great number of incoming connections, such as when the VM is + operating as a router, reverse proxy or a busy HTTP server. Requires + `virtio` network adapter. Defaults to `0`. + +- `disks` (array of objects) - Disks attached to the virtual machine. + Example: + + ```json + [ + { + "type": "scsi", + "disk_size": "5G", + "storage_pool": "local-lvm", + "storage_pool_type": "lvm" + } + ] + ``` + + - `storage_pool` (string) - Required. Name of the Proxmox storage pool + to store the virtual machine disk on. A `local-lvm` pool is allocated + by the installer, for example. + + - `storage_pool_type` (string) - Required. The type of the pool, can + be `lvm`, `lvm-thin`, `zfspool`, `cephfs`, `rbd` or `directory`. + + - `type` (string) - The type of disk. Can be `scsi`, `sata`, `virtio` or + `ide`. Defaults to `scsi`. + + - `disk_size` (string) - The size of the disk, including a unit suffix, such + as `10G` to indicate 10 gigabytes. + + - `cache_mode` (string) - How to cache operations to the disk. Can be + `none`, `writethrough`, `writeback`, `unsafe` or `directsync`. + Defaults to `none`. + + - `format` (string) - The format of the file backing the disk. Can be + `raw`, `cow`, `qcow`, `qed`, `qcow2`, `vmdk` or `cloop`. Defaults to + `raw`. + +- `template_name` (string) - Name of the template. Defaults to the generated + name used during creation. + +- `template_description` (string) - Description of the template, visible in + the Proxmox interface. + +- `onboot` (boolean) - Specifies whether a VM will be started during system + bootup. Defaults to `false`. + +- `disable_kvm` (boolean) - Disables KVM hardware virtualization. Defaults to `false`. + +- `scsi_controller` (string) - The SCSI controller model to emulate. Can be `lsi`, + `lsi53c810`, `virtio-scsi-pci`, `virtio-scsi-single`, `megasas`, or `pvscsi`. + Defaults to `lsi`. + +- `full_clone` (bool) - Whether to run a full or shallow clone from the base clone_vm. Defaults to `true`. + +## Example: Cloud-Init enabled Debian + +Here is a basic example creating a Debian 10 server image. This assumes +that there exists a Cloud-Init enabled image on the Proxmox server named +`debian-10-4`. + +```json +{ + "variables": { + "proxmox_url": "{{env `PROXMOX_URL`}}", + "proxmox_username": "{{env `PROXMOX_USERNAME`}}", + "proxmox_password": "{{env `PROXMOX_PASSWORD`}}" + }, + + "builders": [ + { + "type": "proxmox-clone", + "proxmox_url": "{{user `proxmox_url`}}", + "username": "{{user `proxmox_username`}}", + "password": "{{user `proxmox_password`}}", + "node": "pve", + "insecure_skip_tls_verify": true, + "clone_vm": "debian-10-4", + "template_name": "debian-scaffolding", + "template_description": "image made from cloud-init image", + + "pool": "api-users", + "os": "l26", + "cores": 1, + "sockets": 1, + "memory": 2048, + "network_adapters": [ + { + "bridge": "vmbr0" + } + ] + } + ], + "description": "A template for building a base" +} + +``` diff --git a/website/pages/docs/builders/proxmox/index.mdx b/website/pages/docs/builders/proxmox/index.mdx new file mode 100644 index 000000000..c5d1731ac --- /dev/null +++ b/website/pages/docs/builders/proxmox/index.mdx @@ -0,0 +1,29 @@ +--- +description: > + The Proxmox Packer builder is able to create Cloud-Init + virtual machine images on a Proxmox server. + +layout: docs +page_title: Proxmox - Builders +sidebar_title: Proxmox +--- + +# Proxmox Builder + +The Proxmox Packer builder is able to create +[Proxmox](https://www.proxmox.com/en/proxmox-ve) virtual +machines and store them as new Proxmox Virutal Machine images. + +Packer is able to target both ISO and existing Cloud-Init images: + +- [proxmox-clone](/docs/builders/proxmox-clone) - The proxmox image + Packer builder is able to create new images for use with + Proxmox VE. The builder takes a cloud-init enabled virtual machine + template name, runs any provisioning necessary on the image after + launching it, then creates a virtual machine template. + +- [proxmox-iso](/docs/builders/proxmox-iso) - The proxmox Packer + builder is able to create new images for use with + Proxmox VE. The builder takes an ISO source, runs any provisioning + necessary on the image after launching it, then creates a virtual machine + template. diff --git a/website/pages/docs/builders/proxmox.mdx b/website/pages/docs/builders/proxmox/iso.mdx similarity index 98% rename from website/pages/docs/builders/proxmox.mdx rename to website/pages/docs/builders/proxmox/iso.mdx index 1e3525bb4..b290b5937 100644 --- a/website/pages/docs/builders/proxmox.mdx +++ b/website/pages/docs/builders/proxmox/iso.mdx @@ -5,15 +5,15 @@ description: | necessary on the image after launching it, then creates a virtual machine template. layout: docs -page_title: Proxmox - Builders -sidebar_title: Proxmox +page_title: Proxmox ISO - Builders +sidebar_title: ISO --- -# Proxmox Builder +# Proxmox Builder (from an ISO) -Type: `proxmox` +Type: `proxmox-iso` -The `proxmox` Packer builder is able to create new images for use with +The `proxmox-iso` Packer builder is able to create new images for use with [Proxmox](https://www.proxmox.com/en/proxmox-ve). The builder takes an ISO image, runs any provisioning necessary on the image after launching it, then creates a virtual machine template. This template can then be used as to