277 lines
10 KiB
Go
277 lines
10 KiB
Go
//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"
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
|
|
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
|
|
"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
|
|
"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 {
|
|
vm := state.Get("vm").(*driver.VirtualMachineDriver)
|
|
ui := state.Get("ui").(packersdk.Ui)
|
|
|
|
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) {}
|