diff --git a/builder/vsphere/clone/builder.go b/builder/vsphere/clone/builder.go index 346926051..ea7118113 100644 --- a/builder/vsphere/clone/builder.go +++ b/builder/vsphere/clone/builder.go @@ -54,6 +54,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack }, ) + if b.config.CustomizeConfig != nil { + steps = append(steps, &StepCustomize{ + Config: b.config.CustomizeConfig, + }) + } + if b.config.Comm.Type != "none" { steps = append(steps, &common.StepHTTPIPDiscover{ diff --git a/builder/vsphere/clone/config.go b/builder/vsphere/clone/config.go index f787112c7..f000f54b6 100644 --- a/builder/vsphere/clone/config.go +++ b/builder/vsphere/clone/config.go @@ -40,6 +40,8 @@ type Config struct { // The VM template will not be imported if no [Content Library Import Configuration](#content-library-import-configuration) is specified. // The import doesn't work if [convert_to_template](#convert_to_template) is set to true. ContentLibraryDestinationConfig *common.ContentLibraryDestinationConfig `mapstructure:"content_library_destination"` + // Customize the cloned VM to configure host, network, or licensing settings. See the [customization options](#customization). + CustomizeConfig *CustomizeConfig `mapstructure:"customize"` ctx interpolate.Context } @@ -75,6 +77,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { if c.ContentLibraryDestinationConfig != nil { errs = packer.MultiErrorAppend(errs, c.ContentLibraryDestinationConfig.Prepare(&c.LocationConfig)...) } + if c.CustomizeConfig != nil { + errs = packer.MultiErrorAppend(errs, c.CustomizeConfig.Prepare()...) + } if len(errs.Errors) > 0 { return nil, errs diff --git a/builder/vsphere/clone/config.hcl2spec.go b/builder/vsphere/clone/config.hcl2spec.go index 4c1dd91b9..06f7c5c60 100644 --- a/builder/vsphere/clone/config.hcl2spec.go +++ b/builder/vsphere/clone/config.hcl2spec.go @@ -118,6 +118,7 @@ type FlatConfig struct { ConvertToTemplate *bool `mapstructure:"convert_to_template" cty:"convert_to_template" hcl:"convert_to_template"` Export *common.FlatExportConfig `mapstructure:"export" cty:"export" hcl:"export"` ContentLibraryDestinationConfig *common.FlatContentLibraryDestinationConfig `mapstructure:"content_library_destination" cty:"content_library_destination" hcl:"content_library_destination"` + CustomizeConfig *FlatCustomizeConfig `mapstructure:"customize" cty:"customize" hcl:"customize"` } // FlatMapstructure returns a new FlatConfig. @@ -240,6 +241,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "convert_to_template": &hcldec.AttrSpec{Name: "convert_to_template", Type: cty.Bool, Required: false}, "export": &hcldec.BlockSpec{TypeName: "export", Nested: hcldec.ObjectSpec((*common.FlatExportConfig)(nil).HCL2Spec())}, "content_library_destination": &hcldec.BlockSpec{TypeName: "content_library_destination", Nested: hcldec.ObjectSpec((*common.FlatContentLibraryDestinationConfig)(nil).HCL2Spec())}, + "customize": &hcldec.BlockSpec{TypeName: "customize", Nested: hcldec.ObjectSpec((*FlatCustomizeConfig)(nil).HCL2Spec())}, } return s } diff --git a/builder/vsphere/clone/step_customize.go b/builder/vsphere/clone/step_customize.go new file mode 100644 index 000000000..0ad5a6aa4 --- /dev/null +++ b/builder/vsphere/clone/step_customize.go @@ -0,0 +1,276 @@ +//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/helper/config" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" + "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.VirtualMachine) + ui := state.Get("ui").(packer.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) {} diff --git a/builder/vsphere/clone/step_customize.hcl2spec.go b/builder/vsphere/clone/step_customize.hcl2spec.go new file mode 100644 index 000000000..2620122b2 --- /dev/null +++ b/builder/vsphere/clone/step_customize.hcl2spec.go @@ -0,0 +1,154 @@ +// Code generated by "mapstructure-to-hcl2 -type CustomizeConfig,LinuxOptions,NetworkInterfaces,NetworkInterface,GlobalDnsSettings,GlobalRoutingSettings"; DO NOT EDIT. +package clone + +import ( + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" +) + +// FlatCustomizeConfig is an auto-generated flat version of CustomizeConfig. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatCustomizeConfig struct { + LinuxOptions *FlatLinuxOptions `mapstructure:"linux_options" cty:"linux_options" hcl:"linux_options"` + WindowsSysPrepFile *string `mapstructure:"windows_sysprep_file" cty:"windows_sysprep_file" hcl:"windows_sysprep_file"` + NetworkInterfaces []FlatNetworkInterface `mapstructure:"network_interface" cty:"network_interface" hcl:"network_interface"` + Ipv4Gateway *string `mapstructure:"ipv4_gateway" cty:"ipv4_gateway" hcl:"ipv4_gateway"` + Ipv6Gateway *string `mapstructure:"ipv6_gateway" cty:"ipv6_gateway" hcl:"ipv6_gateway"` + DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"` + DnsSuffixList []string `mapstructure:"dns_suffix_list" cty:"dns_suffix_list" hcl:"dns_suffix_list"` +} + +// FlatMapstructure returns a new FlatCustomizeConfig. +// FlatCustomizeConfig is an auto-generated flat version of CustomizeConfig. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*CustomizeConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatCustomizeConfig) +} + +// HCL2Spec returns the hcl spec of a CustomizeConfig. +// This spec is used by HCL to read the fields of CustomizeConfig. +// The decoded values from this spec will then be applied to a FlatCustomizeConfig. +func (*FlatCustomizeConfig) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "linux_options": &hcldec.BlockSpec{TypeName: "linux_options", Nested: hcldec.ObjectSpec((*FlatLinuxOptions)(nil).HCL2Spec())}, + "windows_sysprep_file": &hcldec.AttrSpec{Name: "windows_sysprep_file", Type: cty.String, Required: false}, + "network_interface": &hcldec.BlockListSpec{TypeName: "network_interface", Nested: hcldec.ObjectSpec((*FlatNetworkInterface)(nil).HCL2Spec())}, + "ipv4_gateway": &hcldec.AttrSpec{Name: "ipv4_gateway", Type: cty.String, Required: false}, + "ipv6_gateway": &hcldec.AttrSpec{Name: "ipv6_gateway", Type: cty.String, Required: false}, + "dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false}, + "dns_suffix_list": &hcldec.AttrSpec{Name: "dns_suffix_list", Type: cty.List(cty.String), Required: false}, + } + return s +} + +// FlatGlobalDnsSettings is an auto-generated flat version of GlobalDnsSettings. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatGlobalDnsSettings struct { + DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"` + DnsSuffixList []string `mapstructure:"dns_suffix_list" cty:"dns_suffix_list" hcl:"dns_suffix_list"` +} + +// FlatMapstructure returns a new FlatGlobalDnsSettings. +// FlatGlobalDnsSettings is an auto-generated flat version of GlobalDnsSettings. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*GlobalDnsSettings) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatGlobalDnsSettings) +} + +// HCL2Spec returns the hcl spec of a GlobalDnsSettings. +// This spec is used by HCL to read the fields of GlobalDnsSettings. +// The decoded values from this spec will then be applied to a FlatGlobalDnsSettings. +func (*FlatGlobalDnsSettings) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false}, + "dns_suffix_list": &hcldec.AttrSpec{Name: "dns_suffix_list", Type: cty.List(cty.String), Required: false}, + } + return s +} + +// FlatGlobalRoutingSettings is an auto-generated flat version of GlobalRoutingSettings. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatGlobalRoutingSettings struct { + Ipv4Gateway *string `mapstructure:"ipv4_gateway" cty:"ipv4_gateway" hcl:"ipv4_gateway"` + Ipv6Gateway *string `mapstructure:"ipv6_gateway" cty:"ipv6_gateway" hcl:"ipv6_gateway"` +} + +// FlatMapstructure returns a new FlatGlobalRoutingSettings. +// FlatGlobalRoutingSettings is an auto-generated flat version of GlobalRoutingSettings. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*GlobalRoutingSettings) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatGlobalRoutingSettings) +} + +// HCL2Spec returns the hcl spec of a GlobalRoutingSettings. +// This spec is used by HCL to read the fields of GlobalRoutingSettings. +// The decoded values from this spec will then be applied to a FlatGlobalRoutingSettings. +func (*FlatGlobalRoutingSettings) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "ipv4_gateway": &hcldec.AttrSpec{Name: "ipv4_gateway", Type: cty.String, Required: false}, + "ipv6_gateway": &hcldec.AttrSpec{Name: "ipv6_gateway", Type: cty.String, Required: false}, + } + return s +} + +// FlatLinuxOptions is an auto-generated flat version of LinuxOptions. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatLinuxOptions struct { + Domain *string `mapstructure:"domain" cty:"domain" hcl:"domain"` + Hostname *string `mapstructure:"host_name" cty:"host_name" hcl:"host_name"` + HWClockUTC *bool `mapstructure:"hw_clock_utc" cty:"hw_clock_utc" hcl:"hw_clock_utc"` + Timezone *string `mapstructure:"time_zone" cty:"time_zone" hcl:"time_zone"` +} + +// FlatMapstructure returns a new FlatLinuxOptions. +// FlatLinuxOptions is an auto-generated flat version of LinuxOptions. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*LinuxOptions) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatLinuxOptions) +} + +// HCL2Spec returns the hcl spec of a LinuxOptions. +// This spec is used by HCL to read the fields of LinuxOptions. +// The decoded values from this spec will then be applied to a FlatLinuxOptions. +func (*FlatLinuxOptions) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "domain": &hcldec.AttrSpec{Name: "domain", Type: cty.String, Required: false}, + "host_name": &hcldec.AttrSpec{Name: "host_name", Type: cty.String, Required: false}, + "hw_clock_utc": &hcldec.AttrSpec{Name: "hw_clock_utc", Type: cty.Bool, Required: false}, + "time_zone": &hcldec.AttrSpec{Name: "time_zone", Type: cty.String, Required: false}, + } + return s +} + +// FlatNetworkInterface is an auto-generated flat version of NetworkInterface. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatNetworkInterface struct { + DnsServerList []string `mapstructure:"dns_server_list" cty:"dns_server_list" hcl:"dns_server_list"` + DnsDomain *string `mapstructure:"dns_domain" cty:"dns_domain" hcl:"dns_domain"` + Ipv4Address *string `mapstructure:"ipv4_address" cty:"ipv4_address" hcl:"ipv4_address"` + Ipv4NetMask *int `mapstructure:"ipv4_netmask" cty:"ipv4_netmask" hcl:"ipv4_netmask"` + Ipv6Address *string `mapstructure:"ipv6_address" cty:"ipv6_address" hcl:"ipv6_address"` + Ipv6NetMask *int `mapstructure:"ipv6_netmask" cty:"ipv6_netmask" hcl:"ipv6_netmask"` +} + +// FlatMapstructure returns a new FlatNetworkInterface. +// FlatNetworkInterface is an auto-generated flat version of NetworkInterface. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*NetworkInterface) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatNetworkInterface) +} + +// HCL2Spec returns the hcl spec of a NetworkInterface. +// This spec is used by HCL to read the fields of NetworkInterface. +// The decoded values from this spec will then be applied to a FlatNetworkInterface. +func (*FlatNetworkInterface) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "dns_server_list": &hcldec.AttrSpec{Name: "dns_server_list", Type: cty.List(cty.String), Required: false}, + "dns_domain": &hcldec.AttrSpec{Name: "dns_domain", Type: cty.String, Required: false}, + "ipv4_address": &hcldec.AttrSpec{Name: "ipv4_address", Type: cty.String, Required: false}, + "ipv4_netmask": &hcldec.AttrSpec{Name: "ipv4_netmask", Type: cty.Number, Required: false}, + "ipv6_address": &hcldec.AttrSpec{Name: "ipv6_address", Type: cty.String, Required: false}, + "ipv6_netmask": &hcldec.AttrSpec{Name: "ipv6_netmask", Type: cty.Number, Required: false}, + } + return s +} diff --git a/builder/vsphere/driver/vm.go b/builder/vsphere/driver/vm.go index 0437fac44..77190216b 100644 --- a/builder/vsphere/driver/vm.go +++ b/builder/vsphere/driver/vm.go @@ -524,6 +524,14 @@ func (vm *VirtualMachine) Configure(config *HardwareConfig) error { return err } +func (vm *VirtualMachine) Customize(spec types.CustomizationSpec) error { + task, err := vm.vm.Customize(vm.driver.ctx, spec) + if err != nil { + return err + } + return task.Wait(vm.driver.ctx) +} + func (vm *VirtualMachine) ResizeDisk(diskSize int64) error { var confSpec types.VirtualMachineConfigSpec diff --git a/website/pages/docs/builders/vmware/vsphere-clone.mdx b/website/pages/docs/builders/vmware/vsphere-clone.mdx index f47a2fe94..5e2d13514 100644 --- a/website/pages/docs/builders/vmware/vsphere-clone.mdx +++ b/website/pages/docs/builders/vmware/vsphere-clone.mdx @@ -86,6 +86,70 @@ can be done via environment variable: @include 'builder/vsphere/common/ConfigParamsConfig-not-required.mdx' +### Customization + +@include '/builder/vsphere/clone/CustomizeConfig.mdx' + +@include 'builder/vsphere/clone/CustomizeConfig-not-required.mdx' + +#### Network Interface Settings + +@include 'builder/vsphere/clone/NetworkInterface-not-required.mdx' + +#### Global Routing Settings + +@include 'builder/vsphere/clone/GlobalRoutingSettings.mdx' + +@include 'builder/vsphere/clone/GlobalRoutingSettings-not-required.mdx' + +#### Global DNS Settings + +@include 'builder/vsphere/clone/GlobalDnsSettings.mdx' + +@include 'builder/vsphere/clone/GlobalDnsSettings-not-required.mdx' + +#### Linux Customization Settings + +@include 'builder/vsphere/clone/LinuxOptions-not-required.mdx' + +#### Customization Example + + + + +```json + "customize": { + "linux_options": { + "host_name": "packer-test", + "domain": "test.internal" + }, + "network_interface": { + "ipv4_address": "10.0.0.10", + "ipv4_netmask": "24" + } + } +``` + + + + +```hcl + customize { + linux_options { + host_name = "packer-test" + domain = "test.internal" + } + + network_interface { + ipv4_address = "10.0.0.10" + ipv4_netmask = "24" + } + } +``` + + + + ### Boot configuration @include 'common/bootcommand/BootConfig.mdx' diff --git a/website/pages/partials/builder/vsphere/clone/Config-not-required.mdx b/website/pages/partials/builder/vsphere/clone/Config-not-required.mdx index 191568809..a896358d2 100644 --- a/website/pages/partials/builder/vsphere/clone/Config-not-required.mdx +++ b/website/pages/partials/builder/vsphere/clone/Config-not-required.mdx @@ -11,3 +11,5 @@ - `content_library_destination` (\*common.ContentLibraryDestinationConfig) - Configuration for importing the VM template to a Content Library. The VM template will not be imported if no [Content Library Import Configuration](#content-library-import-configuration) is specified. The import doesn't work if [convert_to_template](#convert_to_template) is set to true. + +- `customize` (\*CustomizeConfig) - Customize the cloned VM to configure host, network, or licensing settings. See the [customization options](#customization). diff --git a/website/pages/partials/builder/vsphere/clone/CustomizeConfig-not-required.mdx b/website/pages/partials/builder/vsphere/clone/CustomizeConfig-not-required.mdx new file mode 100644 index 000000000..487975075 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/CustomizeConfig-not-required.mdx @@ -0,0 +1,9 @@ + + +- `linux_options` (\*LinuxOptions) - Settings to Linux guest OS customization. See [Linux customization settings](#linux-customization-settings). + +- `windows_sysprep_file` (string) - Supply your own sysprep.xml file to allow full control of the customization process out-of-band of vSphere. + +- `network_interface` (NetworkInterfaces) - 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). diff --git a/website/pages/partials/builder/vsphere/clone/CustomizeConfig.mdx b/website/pages/partials/builder/vsphere/clone/CustomizeConfig.mdx new file mode 100644 index 000000000..fca10d1e6 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/CustomizeConfig.mdx @@ -0,0 +1,12 @@ + + +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: diff --git a/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings-not-required.mdx b/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings-not-required.mdx new file mode 100644 index 000000000..095d11a8a --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings-not-required.mdx @@ -0,0 +1,5 @@ + + +- `dns_server_list` ([]string) - The list of DNS servers to configure on a virtual machine. + +- `dns_suffix_list` ([]string) - A list of DNS search domains to add to the DNS configuration on the virtual machine. diff --git a/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings.mdx b/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings.mdx new file mode 100644 index 000000000..adbcafe54 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/GlobalDnsSettings.mdx @@ -0,0 +1,4 @@ + + +The following settings configure DNS globally, generally for Linux systems. For Windows systems, +this is done per-interface, see [network interface](#network_interface) settings. diff --git a/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings-not-required.mdx b/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings-not-required.mdx new file mode 100644 index 000000000..e83ad8150 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings-not-required.mdx @@ -0,0 +1,5 @@ + + +- `ipv4_gateway` (string) - The IPv4 default gateway when using network_interface customization on the virtual machine. + +- `ipv6_gateway` (string) - The IPv6 default gateway when using network_interface customization on the virtual machine. diff --git a/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings.mdx b/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings.mdx new file mode 100644 index 000000000..55aa29735 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/GlobalRoutingSettings.mdx @@ -0,0 +1,3 @@ + + +The settings here must match the IP/mask of at least one network_interface supplied to customization. diff --git a/website/pages/partials/builder/vsphere/clone/LinuxOptions-not-required.mdx b/website/pages/partials/builder/vsphere/clone/LinuxOptions-not-required.mdx new file mode 100644 index 000000000..462b7bab8 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/LinuxOptions-not-required.mdx @@ -0,0 +1,9 @@ + + +- `domain` (string) - The domain name for this machine. This, along with [host_name](#host_name), make up the FQDN of this virtual machine. + +- `host_name` (string) - The host name for this machine. This, along with [domain](#domain), make up the FQDN of this virtual machine. + +- `hw_clock_utc` (boolean) - Tells the operating system that the hardware clock is set to UTC. Default: true. + +- `time_zone` (string) - Sets the time zone. The default is UTC. diff --git a/website/pages/partials/builder/vsphere/clone/NetworkInterface-not-required.mdx b/website/pages/partials/builder/vsphere/clone/NetworkInterface-not-required.mdx new file mode 100644 index 000000000..7f9a33468 --- /dev/null +++ b/website/pages/partials/builder/vsphere/clone/NetworkInterface-not-required.mdx @@ -0,0 +1,15 @@ + + +- `dns_server_list` ([]string) - 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. + +- `dns_domain` (string) - 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. + +- `ipv4_address` (string) - The IPv4 address assigned to this network adapter. If left blank or not included, DHCP is used. + +- `ipv4_netmask` (int) - The IPv4 subnet mask, in bits (example: 24 for 255.255.255.0). + +- `ipv6_address` (string) - The IPv6 address assigned to this network adapter. If left blank or not included, auto-configuration is used. + +- `ipv6_netmask` (int) - The IPv6 subnet mask, in bits (example: 32).