packer-cn/builder/vsphere/clone/step_customize.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-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer/builder/vsphere/driver"
"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) {}