2020-07-29 03:09:58 -04:00
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type CustomizeConfig,LinuxOptions,NetworkInterfaces,NetworkInterface,GlobalDnsSettings,GlobalRoutingSettings
package clone
import (
"context"
"fmt"
"io/ioutil"
"net"
"github.com/hashicorp/packer/builder/vsphere/driver"
2020-11-17 19:31:03 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
2020-11-19 14:54:31 -05:00
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
2020-11-18 13:34:59 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
2020-07-29 03:09:58 -04:00
"github.com/vmware/govmomi/vim25/types"
)
// A cloned virtual machine can be [customized](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-58E346FF-83AE-42B8-BE58-253641D257BC.html)
// to configure host, network, or licensing settings.
//
// To perform virtual machine customization as a part of the clone process, specify the customize block with the
// respective customization options. Windows guests are customized using Sysprep, which will result in the machine SID being reset.
// Before using customization, check that your source VM meets the [requirements](https://docs.vmware.com/en/VMware-vSphere/7.0/com.vmware.vsphere.vm_admin.doc/GUID-E63B6FAA-8D35-428D-B40C-744769845906.html)
// for guest OS customization on vSphere.
// See the [customization example](#customization-example) for a usage synopsis.
//
// The settings for customize are as follows:
type CustomizeConfig struct {
// Settings to Linux guest OS customization. See [Linux customization settings](#linux-customization-settings).
LinuxOptions * LinuxOptions ` mapstructure:"linux_options" `
// Supply your own sysprep.xml file to allow full control of the customization process out-of-band of vSphere.
WindowsSysPrepFile string ` mapstructure:"windows_sysprep_file" `
// Configure network interfaces on a per-interface basis that should matched up to the network adapters present in the VM.
// To use DHCP, declare an empty network_interface for each adapter being configured. This field is required.
// See [Network interface settings](#network-interface-settings).
NetworkInterfaces NetworkInterfaces ` mapstructure:"network_interface" `
GlobalRoutingSettings ` mapstructure:",squash" `
GlobalDnsSettings ` mapstructure:",squash" `
}
type LinuxOptions struct {
// The domain name for this machine. This, along with [host_name](#host_name), make up the FQDN of this virtual machine.
Domain string ` mapstructure:"domain" `
// The host name for this machine. This, along with [domain](#domain), make up the FQDN of this virtual machine.
Hostname string ` mapstructure:"host_name" `
// Tells the operating system that the hardware clock is set to UTC. Default: true.
HWClockUTC config . Trilean ` mapstructure:"hw_clock_utc" `
// Sets the time zone. The default is UTC.
Timezone string ` mapstructure:"time_zone" `
}
type NetworkInterface struct {
// Network interface-specific DNS server settings for Windows operating systems.
// Ignored on Linux and possibly other operating systems - for those systems, please see the [global DNS settings](#global-dns-settings) section.
DnsServerList [ ] string ` mapstructure:"dns_server_list" `
// Network interface-specific DNS search domain for Windows operating systems.
// Ignored on Linux and possibly other operating systems - for those systems, please see the [global DNS settings](#global-dns-settings) section.
DnsDomain string ` mapstructure:"dns_domain" `
// The IPv4 address assigned to this network adapter. If left blank or not included, DHCP is used.
Ipv4Address string ` mapstructure:"ipv4_address" `
// The IPv4 subnet mask, in bits (example: 24 for 255.255.255.0).
Ipv4NetMask int ` mapstructure:"ipv4_netmask" `
// The IPv6 address assigned to this network adapter. If left blank or not included, auto-configuration is used.
Ipv6Address string ` mapstructure:"ipv6_address" `
// The IPv6 subnet mask, in bits (example: 32).
Ipv6NetMask int ` mapstructure:"ipv6_netmask" `
}
type NetworkInterfaces [ ] NetworkInterface
// The settings here must match the IP/mask of at least one network_interface supplied to customization.
type GlobalRoutingSettings struct {
// The IPv4 default gateway when using network_interface customization on the virtual machine.
Ipv4Gateway string ` mapstructure:"ipv4_gateway" `
// The IPv6 default gateway when using network_interface customization on the virtual machine.
Ipv6Gateway string ` mapstructure:"ipv6_gateway" `
}
// The following settings configure DNS globally, generally for Linux systems. For Windows systems,
// this is done per-interface, see [network interface](#network_interface) settings.
type GlobalDnsSettings struct {
// The list of DNS servers to configure on a virtual machine.
DnsServerList [ ] string ` mapstructure:"dns_server_list" `
// A list of DNS search domains to add to the DNS configuration on the virtual machine.
DnsSuffixList [ ] string ` mapstructure:"dns_suffix_list" `
}
type StepCustomize struct {
Config * CustomizeConfig
}
func ( c * CustomizeConfig ) Prepare ( ) [ ] error {
var errs [ ] error
if c . LinuxOptions == nil && c . WindowsSysPrepFile == "" {
errs = append ( errs , fmt . Errorf ( "customize is empty" ) )
}
if c . LinuxOptions != nil && c . WindowsSysPrepFile != "" {
errs = append ( errs , fmt . Errorf ( "`linux_options` and `windows_sysprep_text` both set - one must not be included if the other is specified" ) )
}
if c . LinuxOptions != nil {
if c . LinuxOptions . Hostname == "" {
errs = append ( errs , fmt . Errorf ( "linux options `host_name` is empty" ) )
}
if c . LinuxOptions . Domain == "" {
errs = append ( errs , fmt . Errorf ( "linux options `domain` is empty" ) )
}
if c . LinuxOptions . HWClockUTC == config . TriUnset {
c . LinuxOptions . HWClockUTC = config . TriTrue
}
if c . LinuxOptions . Timezone == "" {
c . LinuxOptions . Timezone = "UTC"
}
}
if len ( c . NetworkInterfaces ) == 0 {
errs = append ( errs , fmt . Errorf ( "one or more `network_interface` must be provided" ) )
}
return errs
}
func ( s * StepCustomize ) Run ( _ context . Context , state multistep . StateBag ) multistep . StepAction {
2020-08-31 04:34:41 -04:00
vm := state . Get ( "vm" ) . ( * driver . VirtualMachineDriver )
2020-11-19 14:54:31 -05:00
ui := state . Get ( "ui" ) . ( packersdk . Ui )
2020-07-29 03:09:58 -04:00
identity , err := s . identitySettings ( )
if err != nil {
state . Put ( "error" , err )
return multistep . ActionHalt
}
nicSettingsMap := s . nicSettingsMap ( )
globalIpSettings := s . globalIpSettings ( )
spec := types . CustomizationSpec {
Identity : identity ,
NicSettingMap : nicSettingsMap ,
GlobalIPSettings : globalIpSettings ,
}
ui . Say ( "Customizing VM..." )
err = vm . Customize ( spec )
if err != nil {
state . Put ( "error" , err )
return multistep . ActionHalt
}
return multistep . ActionContinue
}
func ( s * StepCustomize ) identitySettings ( ) ( types . BaseCustomizationIdentitySettings , error ) {
if s . Config . LinuxOptions != nil {
return & types . CustomizationLinuxPrep {
HostName : & types . CustomizationFixedName {
Name : s . Config . LinuxOptions . Hostname ,
} ,
Domain : s . Config . LinuxOptions . Domain ,
TimeZone : s . Config . LinuxOptions . Timezone ,
HwClockUTC : s . Config . LinuxOptions . HWClockUTC . ToBoolPointer ( ) ,
} , nil
}
if s . Config . WindowsSysPrepFile != "" {
sysPrep , err := ioutil . ReadFile ( s . Config . WindowsSysPrepFile )
if err != nil {
return nil , fmt . Errorf ( "error on reading %s: %s" , s . Config . WindowsSysPrepFile , err )
}
return & types . CustomizationSysprepText {
Value : string ( sysPrep ) ,
} , nil
}
return nil , fmt . Errorf ( "no customization identity found" )
}
func ( s * StepCustomize ) nicSettingsMap ( ) [ ] types . CustomizationAdapterMapping {
result := make ( [ ] types . CustomizationAdapterMapping , len ( s . Config . NetworkInterfaces ) )
var ipv4gwFound , ipv6gwFound bool
for i := range s . Config . NetworkInterfaces {
var adapter types . CustomizationIPSettings
adapter , ipv4gwFound , ipv6gwFound = s . ipSettings ( i , ! ipv4gwFound , ! ipv6gwFound )
obj := types . CustomizationAdapterMapping {
Adapter : adapter ,
}
result [ i ] = obj
}
return result
}
func ( s * StepCustomize ) ipSettings ( n int , ipv4gwAdd bool , ipv6gwAdd bool ) ( types . CustomizationIPSettings , bool , bool ) {
var v4gwFound , v6gwFound bool
var obj types . CustomizationIPSettings
ipv4Address := s . Config . NetworkInterfaces [ n ] . Ipv4Address
if ipv4Address != "" {
ipv4mask := s . Config . NetworkInterfaces [ n ] . Ipv4NetMask
ipv4Gateway := s . Config . Ipv4Gateway
obj . Ip = & types . CustomizationFixedIp {
IpAddress : ipv4Address ,
}
obj . SubnetMask = v4CIDRMaskToDotted ( ipv4mask )
// Check for the gateway
if ipv4gwAdd && ipv4Gateway != "" && matchGateway ( ipv4Address , ipv4mask , ipv4Gateway ) {
obj . Gateway = [ ] string { ipv4Gateway }
v4gwFound = true
}
} else {
obj . Ip = & types . CustomizationDhcpIpGenerator { }
}
obj . DnsServerList = s . Config . NetworkInterfaces [ n ] . DnsServerList
obj . DnsDomain = s . Config . NetworkInterfaces [ n ] . DnsDomain
obj . IpV6Spec , v6gwFound = s . IPSettingsIPV6Address ( n , ipv6gwAdd )
return obj , v4gwFound , v6gwFound
}
func v4CIDRMaskToDotted ( mask int ) string {
m := net . CIDRMask ( mask , 32 )
a := int ( m [ 0 ] )
b := int ( m [ 1 ] )
c := int ( m [ 2 ] )
d := int ( m [ 3 ] )
return fmt . Sprintf ( "%d.%d.%d.%d" , a , b , c , d )
}
func ( s * StepCustomize ) IPSettingsIPV6Address ( n int , gwAdd bool ) ( * types . CustomizationIPSettingsIpV6AddressSpec , bool ) {
addr := s . Config . NetworkInterfaces [ n ] . Ipv6Address
var gwFound bool
if addr == "" {
return nil , gwFound
}
mask := s . Config . NetworkInterfaces [ n ] . Ipv6NetMask
gw := s . Config . Ipv6Gateway
obj := & types . CustomizationIPSettingsIpV6AddressSpec {
Ip : [ ] types . BaseCustomizationIpV6Generator {
& types . CustomizationFixedIpV6 {
IpAddress : addr ,
SubnetMask : int32 ( mask ) ,
} ,
} ,
}
if gwAdd && gw != "" && matchGateway ( addr , mask , gw ) {
obj . Gateway = [ ] string { gw }
gwFound = true
}
return obj , gwFound
}
// matchGateway take an IP, mask, and gateway, and checks to see if the gateway
// is reachable from the IP address.
func matchGateway ( a string , m int , g string ) bool {
ip := net . ParseIP ( a )
gw := net . ParseIP ( g )
var mask net . IPMask
if ip . To4 ( ) != nil {
mask = net . CIDRMask ( m , 32 )
} else {
mask = net . CIDRMask ( m , 128 )
}
if ip . Mask ( mask ) . Equal ( gw . Mask ( mask ) ) {
return true
}
return false
}
func ( s * StepCustomize ) globalIpSettings ( ) types . CustomizationGlobalIPSettings {
return types . CustomizationGlobalIPSettings {
DnsServerList : s . Config . DnsServerList ,
DnsSuffixList : s . Config . DnsSuffixList ,
}
}
func ( s * StepCustomize ) Cleanup ( _ multistep . StateBag ) { }