Adds vApp properties config and save public ssh key to a vApp property (#9507)

This commit is contained in:
Sylvia Moss 2020-07-08 10:33:45 +02:00 committed by GitHub
parent 673858a63c
commit 268e95364f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 339 additions and 25 deletions

View File

@ -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,

View File

@ -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},

View File

@ -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)

View File

@ -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
}

View File

@ -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))
}
}
}

View File

@ -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 {

View File

@ -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'

View File

@ -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.

View File

@ -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.