Adds vApp properties config and save public ssh key to a vApp property (#9507)
This commit is contained in:
parent
673858a63c
commit
268e95364f
|
@ -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,
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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:
|
||||
|
||||
<Tabs>
|
||||
<Tab heading="JSON">
|
||||
|
||||
```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 <userdata.yaml | { base64 -w0 2>/dev/null || base64; })`
|
||||
|
||||
</Tab>
|
||||
<Tab heading="HCL2">
|
||||
|
||||
```hcl
|
||||
vapp {
|
||||
properties = {
|
||||
hostname = var.hostname
|
||||
user-data = base64encode(var.user_data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### 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'
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<!-- Code generated from the comments of the vAppConfig struct in builder/vsphere/clone/step_clone.go; DO NOT EDIT MANUALLY -->
|
||||
|
||||
- `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.
|
||||
|
Loading…
Reference in New Issue