diff --git a/builder/vsphere/clone/builder.go b/builder/vsphere/clone/builder.go index 0f68d33c3..1e5b1c4a0 100644 --- a/builder/vsphere/clone/builder.go +++ b/builder/vsphere/clone/builder.go @@ -2,6 +2,7 @@ package clone import ( "context" + "fmt" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/packer/builder/vsphere/common" @@ -65,6 +66,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack HTTPPortMax: b.config.HTTPPortMax, HTTPAddress: b.config.HTTPAddress, }, + &common.StepSshKeyPair{ + Debug: b.config.PackerDebug, + DebugKeyPath: fmt.Sprintf("%s.pem", b.config.PackerBuildName), + Comm: &b.config.Comm, + }, &common.StepRun{ Config: &b.config.RunConfig, SetOrder: false, diff --git a/builder/vsphere/clone/config.hcl2spec.go b/builder/vsphere/clone/config.hcl2spec.go index e769cd496..267b232df 100644 --- a/builder/vsphere/clone/config.hcl2spec.go +++ b/builder/vsphere/clone/config.hcl2spec.go @@ -31,6 +31,7 @@ type FlatConfig struct { LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"` Network *string `mapstructure:"network" cty:"network" hcl:"network"` Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"` + VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"` VMName *string `mapstructure:"vm_name" cty:"vm_name" hcl:"vm_name"` Folder *string `mapstructure:"folder" cty:"folder" hcl:"folder"` Cluster *string `mapstructure:"cluster" cty:"cluster" hcl:"cluster"` @@ -147,6 +148,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false}, "network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false}, "notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false}, + "vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())}, "vm_name": &hcldec.AttrSpec{Name: "vm_name", Type: cty.String, Required: false}, "folder": &hcldec.AttrSpec{Name: "folder", Type: cty.String, Required: false}, "cluster": &hcldec.AttrSpec{Name: "cluster", Type: cty.String, Required: false}, diff --git a/builder/vsphere/clone/step_clone.go b/builder/vsphere/clone/step_clone.go index 9f5cd5f62..de6448ff7 100644 --- a/builder/vsphere/clone/step_clone.go +++ b/builder/vsphere/clone/step_clone.go @@ -1,5 +1,5 @@ //go:generate struct-markdown -//go:generate mapstructure-to-hcl2 -type CloneConfig +//go:generate mapstructure-to-hcl2 -type CloneConfig,vAppConfig package clone @@ -14,6 +14,17 @@ import ( "github.com/hashicorp/packer/packer" ) +type vAppConfig struct { + // Set values for the available vApp Properties to supply configuration parameters to a virtual machine cloned from + // a template that came from an imported OVF or OVA file. + // + // -> **Note:** The only supported usage path for vApp properties is for existing user-configurable keys. + // These generally come from an existing template that was created from an imported OVF or OVA file. + // You cannot set values for vApp properties on virtual machines created from scratch, + // virtual machines lacking a vApp configuration, or on property keys that do not exist. + Properties map[string]string `mapstructure:"properties"` +} + type CloneConfig struct { // Name of source VM. Path is optional. Template string `mapstructure:"template"` @@ -26,6 +37,10 @@ type CloneConfig struct { Network string `mapstructure:"network"` // VM notes. Notes string `mapstructure:"notes"` + // Set the vApp Options to a virtual machine. + // See the [vApp Options Configuration](/docs/builders/vmware/vsphere-clone#vapp-options-configuration) + // to know the available options and how to use it. + VAppConfig vAppConfig `mapstructure:"vapp"` } func (c *CloneConfig) Prepare() []error { @@ -67,15 +82,16 @@ func (s *StepCloneVM) Run(ctx context.Context, state multistep.StateBag) multist } vm, err := template.Clone(ctx, &driver.CloneConfig{ - Name: s.Location.VMName, - Folder: s.Location.Folder, - Cluster: s.Location.Cluster, - Host: s.Location.Host, - ResourcePool: s.Location.ResourcePool, - Datastore: s.Location.Datastore, - LinkedClone: s.Config.LinkedClone, - Network: s.Config.Network, - Annotation: s.Config.Notes, + Name: s.Location.VMName, + Folder: s.Location.Folder, + Cluster: s.Location.Cluster, + Host: s.Location.Host, + ResourcePool: s.Location.ResourcePool, + Datastore: s.Location.Datastore, + LinkedClone: s.Config.LinkedClone, + Network: s.Config.Network, + Annotation: s.Config.Notes, + VAppProperties: s.Config.VAppConfig.Properties, }) if err != nil { state.Put("error", err) diff --git a/builder/vsphere/clone/step_clone.hcl2spec.go b/builder/vsphere/clone/step_clone.hcl2spec.go index d7bd87301..a9f670f94 100644 --- a/builder/vsphere/clone/step_clone.hcl2spec.go +++ b/builder/vsphere/clone/step_clone.hcl2spec.go @@ -1,4 +1,4 @@ -// Code generated by "mapstructure-to-hcl2 -type CloneConfig"; DO NOT EDIT. +// Code generated by "mapstructure-to-hcl2 -type CloneConfig,vAppConfig"; DO NOT EDIT. package clone import ( @@ -9,11 +9,12 @@ import ( // FlatCloneConfig is an auto-generated flat version of CloneConfig. // Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. type FlatCloneConfig struct { - Template *string `mapstructure:"template" cty:"template" hcl:"template"` - DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"` - LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"` - Network *string `mapstructure:"network" cty:"network" hcl:"network"` - Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"` + Template *string `mapstructure:"template" cty:"template" hcl:"template"` + DiskSize *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"` + LinkedClone *bool `mapstructure:"linked_clone" cty:"linked_clone" hcl:"linked_clone"` + Network *string `mapstructure:"network" cty:"network" hcl:"network"` + Notes *string `mapstructure:"notes" cty:"notes" hcl:"notes"` + VAppConfig *FlatvAppConfig `mapstructure:"vapp" cty:"vapp" hcl:"vapp"` } // FlatMapstructure returns a new FlatCloneConfig. @@ -33,6 +34,30 @@ func (*FlatCloneConfig) HCL2Spec() map[string]hcldec.Spec { "linked_clone": &hcldec.AttrSpec{Name: "linked_clone", Type: cty.Bool, Required: false}, "network": &hcldec.AttrSpec{Name: "network", Type: cty.String, Required: false}, "notes": &hcldec.AttrSpec{Name: "notes", Type: cty.String, Required: false}, + "vapp": &hcldec.BlockSpec{TypeName: "vapp", Nested: hcldec.ObjectSpec((*FlatvAppConfig)(nil).HCL2Spec())}, + } + return s +} + +// FlatvAppConfig is an auto-generated flat version of vAppConfig. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatvAppConfig struct { + Properties map[string]string `mapstructure:"properties" cty:"properties" hcl:"properties"` +} + +// FlatMapstructure returns a new FlatvAppConfig. +// FlatvAppConfig is an auto-generated flat version of vAppConfig. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*vAppConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatvAppConfig) +} + +// HCL2Spec returns the hcl spec of a vAppConfig. +// This spec is used by HCL to read the fields of vAppConfig. +// The decoded values from this spec will then be applied to a FlatvAppConfig. +func (*FlatvAppConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "properties": &hcldec.AttrSpec{Name: "properties", Type: cty.Map(cty.String), Required: false}, } return s } diff --git a/builder/vsphere/common/step_ssh_key_pair.go b/builder/vsphere/common/step_ssh_key_pair.go new file mode 100644 index 000000000..d094a0cb5 --- /dev/null +++ b/builder/vsphere/common/step_ssh_key_pair.go @@ -0,0 +1,115 @@ +package common + +import ( + "context" + "fmt" + "io/ioutil" + "os" + + "github.com/hashicorp/packer/builder/vsphere/driver" + "github.com/hashicorp/packer/common/uuid" + "github.com/hashicorp/packer/helper/communicator" + "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 + Comm *communicator.Config +} + +func (s *StepSshKeyPair) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + if s.Comm.Type != "ssh" || s.Comm.SSHPassword != "" { + return multistep.ActionContinue + } + + ui := state.Get("ui").(packer.Ui) + + comment := fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID()) + if s.Comm.SSHPrivateKeyFile != "" { + ui.Say("Using existing SSH private key for the communicator...") + privateKeyBytes, err := s.Comm.ReadSSHPrivateKeyFile() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + kp, err := ssh.KeyPairFromPrivateKey(ssh.FromPrivateKeyConfig{ + RawPrivateKeyPemBlock: privateKeyBytes, + Comment: comment, + }) + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + + s.Comm.SSHPrivateKey = privateKeyBytes + s.Comm.SSHKeyPairName = kp.Comment + s.Comm.SSHTemporaryKeyPairName = kp.Comment + s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + + return multistep.ActionContinue + } + + if s.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...") + + if s.Comm.SSHTemporaryKeyPairName != "" { + comment = s.Comm.SSHTemporaryKeyPairName + } + + kp, err := ssh.NewKeyPair(ssh.CreateKeyPairConfig{ + Comment: comment, + Type: ssh.Rsa, + }) + if err != nil { + state.Put("error", fmt.Errorf("Error creating temporary keypair: %s", err)) + return multistep.ActionHalt + } + + s.Comm.SSHKeyPairName = kp.Comment + s.Comm.SSHTemporaryKeyPairName = kp.Comment + s.Comm.SSHPrivateKey = kp.PrivateKeyPemBlock + s.Comm.SSHPublicKey = kp.PublicKeyAuthorizedKeysLine + s.Comm.SSHClearAuthorizedKeys = true + + vm := state.Get("vm").(*driver.VirtualMachine) + err = vm.AddPublicKeys(ctx, string(s.Comm.SSHPublicKey)) + if err != nil { + state.Put("error", fmt.Errorf("error saving temporary keypair in the vm: %s", err)) + return multistep.ActionHalt + } + + 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)) + // Write the key out + if err := ioutil.WriteFile(s.DebugKeyPath, kp.PrivateKeyPemBlock, 0600); 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/vsphere/driver/vm.go b/builder/vsphere/driver/vm.go index 8d1372282..cae885e1e 100644 --- a/builder/vsphere/driver/vm.go +++ b/builder/vsphere/driver/vm.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net" + "reflect" "strings" "time" @@ -26,15 +27,16 @@ type VirtualMachine struct { } type CloneConfig struct { - Name string - Folder string - Cluster string - Host string - ResourcePool string - Datastore string - LinkedClone bool - Network string - Annotation string + Name string + Folder string + Cluster string + Host string + ResourcePool string + Datastore string + LinkedClone bool + Network string + Annotation string + VAppProperties map[string]string } type HardwareConfig struct { @@ -315,6 +317,12 @@ func (vm *VirtualMachine) Clone(ctx context.Context, config *CloneConfig) (*Virt configSpec.DeviceChange = append(configSpec.DeviceChange, config) } + vAppConfig, err := vm.updateVAppConfig(ctx, config.VAppProperties) + if err != nil { + return nil, err + } + configSpec.VAppConfig = vAppConfig + task, err := vm.vm.Clone(vm.driver.ctx, folder.folder, config.Name, cloneSpec) if err != nil { return nil, err @@ -339,6 +347,80 @@ func (vm *VirtualMachine) Clone(ctx context.Context, config *CloneConfig) (*Virt return created, nil } +func (vm *VirtualMachine) updateVAppConfig(ctx context.Context, newProps map[string]string) (*types.VmConfigSpec, error) { + if len(newProps) == 0 { + return nil, nil + } + + vProps, _ := vm.Properties(ctx) + if vProps.Config.VAppConfig == nil { + return nil, fmt.Errorf("this VM lacks a vApp configuration and cannot have vApp properties set on it") + } + + allProperties := vProps.Config.VAppConfig.GetVmConfigInfo().Property + + var props []types.VAppPropertySpec + for _, p := range allProperties { + userValue, setByUser := newProps[p.Id] + if !setByUser { + continue + } + + if *p.UserConfigurable == false { + return nil, fmt.Errorf("vApp property with userConfigurable=false specified in vapp.properties: %+v", reflect.ValueOf(newProps).MapKeys()) + } + + prop := types.VAppPropertySpec{ + ArrayUpdateSpec: types.ArrayUpdateSpec{ + Operation: types.ArrayUpdateOperationEdit, + }, + Info: &types.VAppPropertyInfo{ + Key: p.Key, + Id: p.Id, + Value: userValue, + UserConfigurable: p.UserConfigurable, + }, + } + props = append(props, prop) + + delete(newProps, p.Id) + } + + if len(newProps) > 0 { + return nil, fmt.Errorf("unsupported vApp properties in vapp.properties: %+v", reflect.ValueOf(newProps).MapKeys()) + } + + return &types.VmConfigSpec{ + Property: props, + }, nil +} + +func (vm *VirtualMachine) AddPublicKeys(ctx context.Context, publicKeys string) error { + newProps := map[string]string{"public-keys": publicKeys} + config, err := vm.updateVAppConfig(ctx, newProps) + if err != nil { + return fmt.Errorf("not possible to save temporary public key: %s", err.Error()) + } + + confSpec := types.VirtualMachineConfigSpec{VAppConfig: config} + task, err := vm.vm.Reconfigure(vm.driver.ctx, confSpec) + if err != nil { + return err + } + + _, err = task.WaitForResult(vm.driver.ctx, nil) + return err +} + +func (vm *VirtualMachine) Properties(ctx context.Context) (*mo.VirtualMachine, error) { + log.Printf("fetching properties for VM %q", vm.vm.InventoryPath) + var props mo.VirtualMachine + if err := vm.vm.Properties(ctx, vm.vm.Reference(), nil, &props); err != nil { + return nil, err + } + return &props, nil +} + func (vm *VirtualMachine) Destroy() error { task, err := vm.vm.Destroy(vm.driver.ctx) if err != nil { diff --git a/website/pages/docs/builders/vmware/vsphere-clone.mdx b/website/pages/docs/builders/vmware/vsphere-clone.mdx index 361300eb1..e22e7a5d4 100644 --- a/website/pages/docs/builders/vmware/vsphere-clone.mdx +++ b/website/pages/docs/builders/vmware/vsphere-clone.mdx @@ -42,6 +42,43 @@ necessary for this build to succeed and can be found further down the page. @include 'builder/vsphere/clone/CloneConfig-not-required.mdx' +### vApp Options Configuration + +@include 'builder/vsphere/clone/vAppConfig-not-required.mdx' + +Example of usage: + + + + +```json + "vapp": { + "properties": { + "hostname": "{{ user `hostname`}}", + "user-data": "{{ env `USERDATA`}}" + } + } +``` + +A `user-data` field requires the content of a yaml file to be encoded with base64. This +can be done via environment variable: +`export USERDATA=$(gzip -c9 /dev/null || base64; })` + + + + +```hcl + vapp { + properties = { + hostname = var.hostname + user-data = base64encode(var.user_data) + } + } +``` + + + + ### Extra Configuration Parameters @include 'builder/vsphere/common/ConfigParamsConfig-not-required.mdx' @@ -98,6 +135,23 @@ necessary for this build to succeed and can be found further down the page. @include 'helper/communicator/SSH-not-required.mdx' +@include 'helper/communicator/SSH-Temporary-Key-Pair-not-required.mdx' + +@include 'helper/communicator/SSH-Key-Pair-Name-not-required.mdx' + +@include 'helper/communicator/SSH-Private-Key-File-not-required.mdx' + +@include 'helper/communicator/SSH-Agent-Auth-not-required.mdx' + +-> **NOTE:** Packer uses vApp Options to inject ssh public keys to the Virtual Machine. + The [temporary_key_pair_name](/docs/builders/vmware/vsphere-clone#temporary_key_pair_name) will only work + if the template being cloned contains the vApp property `public-keys`. + If using [ssh_private_key_file](/docs/builders/vmware/vsphere-clone#ssh_private_key_file), provide + the public key via [configuration_parameters](/docs/builders/vmware/vsphere-clone#configuration_parameters) or + [vApp Options Configuration](/docs/builders/vmware/vsphere-clone#vapp-options-configuration) whenever the `guestinto.userdata` + is available. See [VMware Guestinfo datasource](https://github.com/vmware/cloud-init-vmware-guestinfo) for more information + about the key. + #### Optional WinRM fields: @include 'helper/communicator/WinRM-not-required.mdx' diff --git a/website/pages/partials/builder/vsphere/clone/CloneConfig-not-required.mdx b/website/pages/partials/builder/vsphere/clone/CloneConfig-not-required.mdx index 8e07b5e05..83a3a3f00 100644 --- a/website/pages/partials/builder/vsphere/clone/CloneConfig-not-required.mdx +++ b/website/pages/partials/builder/vsphere/clone/CloneConfig-not-required.mdx @@ -10,4 +10,8 @@ must be specified to allow Packer to look for the available network. - `notes` (string) - VM notes. + +- `vapp` (vAppConfig) - Set the vApp Options to a virtual machine. + See the [vApp Options Configuration](/docs/builders/vmware/vsphere-clone#vapp-options-configuration) + to know the available options and how to use it. \ No newline at end of file diff --git a/website/pages/partials/builder/vsphere/clone/vAppConfig-not-required.mdx b/website/pages/partials/builder/vsphere/clone/vAppConfig-not-required.mdx new file mode 100644 index 000000000..fb4b3a3b0 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/vAppConfig-not-required.mdx @@ -0,0 +1,10 @@ + + +- `properties` (map[string]string) - Set values for the available vApp Properties to supply configuration parameters to a virtual machine cloned from + a template that came from an imported OVF or OVA file. + + -> **Note:** The only supported usage path for vApp properties is for existing user-configurable keys. + These generally come from an existing template that was created from an imported OVF or OVA file. + You cannot set values for vApp properties on virtual machines created from scratch, + virtual machines lacking a vApp configuration, or on property keys that do not exist. + \ No newline at end of file