From f57cff9313529056542e1993f4cf75a5b5382a13 Mon Sep 17 00:00:00 2001 From: Vasil Atanasov Date: Fri, 3 Nov 2023 11:36:59 +0200 Subject: [PATCH] [fix #1604]Add support for applying existing customization specs to r/virtual_machine - added `vsphere_guest_os_customization` data source for existing customization specs - added `vsphere_guest_os_customization` resource for CRUD operations on customization specs - added customization_spec attribute to VirtualMachineCloneSchema to enable `vsphere_guest_os_customization` usage on VM clone - virtual machine customization and guest os customization are using the same code for expanding/flattening specs sent to API - deleted the file containing the common code regarding guest OS customizations shared between VM and customizations - created e2e tests - added documentation for `d/vsphere_guest_os_customization` and `r/vsphere_guest_os_customization` - addressed comments Testing done: - `make build` - Verified that the folowing e2e tests pass: - TestAccResourceVSphereVirtualMachine_cloneWithDifferentHostname - TestAccResourceVSphereVirtualMachine_cloneCustomizeWithNewResourcePool - TestAccResourceVSphereVirtualMachine_cloneWithDifferentTimezone - TestAccResourceVSphereVirtualMachine_cloneCustomizeForceNewWithDatastore - Verified that the newly introduced e2e tests pass Signed-off-by: Vasil Atanasov --- ...a_source_vsphere_guest_os_customization.go | 219 +++++++ ...rce_vsphere_guest_os_customization_test.go | 44 ++ .../customizations_helper.go} | 538 ++++++++++++------ .../virtual_machine_clone_subresource.go | 44 +- vsphere/provider.go | 2 + ...resource_vsphere_guest_os_customization.go | 142 +++++ ...rce_vsphere_guest_os_customization_test.go | 176 ++++++ vsphere/resource_vsphere_virtual_machine.go | 30 +- .../resource_vsphere_virtual_machine_test.go | 89 +++ .../d/guest_os_customization.html.markdown | 37 ++ .../r/guest_os_customization.html.markdown | 48 ++ 11 files changed, 1191 insertions(+), 178 deletions(-) create mode 100644 vsphere/data_source_vsphere_guest_os_customization.go create mode 100644 vsphere/data_source_vsphere_guest_os_customization_test.go rename vsphere/internal/{vmworkflow/virtual_machine_customize_subresource.go => helper/guestoscustomizations/customizations_helper.go} (51%) create mode 100644 vsphere/resource_vsphere_guest_os_customization.go create mode 100644 vsphere/resource_vsphere_guest_os_customization_test.go create mode 100644 website/docs/d/guest_os_customization.html.markdown create mode 100644 website/docs/r/guest_os_customization.html.markdown diff --git a/vsphere/data_source_vsphere_guest_os_customization.go b/vsphere/data_source_vsphere_guest_os_customization.go new file mode 100644 index 000000000..8362fe427 --- /dev/null +++ b/vsphere/data_source_vsphere_guest_os_customization.go @@ -0,0 +1,219 @@ +package vsphere + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" +) + +func dataSourceVSphereGuestOSCustomization() *schema.Resource { + return &schema.Resource{ + Read: dataSourceVSphereGuestCustomizationRead, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the customization specification is the unique identifier per vCenter Server instance.", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "TThe type of customization specification: One among: Windows, Linux.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "The description for the customization specification.", + }, + "last_update_time": { + Type: schema.TypeString, + Computed: true, + Description: "The time of last modification to the customization specification.", + }, + "change_version": { + Type: schema.TypeString, + Computed: true, + Description: "The number of last changed version to the customization specification.", + }, + "spec": { + Type: schema.TypeList, + Computed: true, + Description: "Container object for the guest operating system properties to be customized.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dns_server_list": { + Type: schema.TypeList, + Computed: true, + Description: "A list of DNS servers for a virtual network adapter with a static IP address.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dns_suffix_list": { + Type: schema.TypeList, + Computed: true, + Description: "A list of DNS search domains to add to the DNS configuration on the virtual machine.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "linux_options": { + Type: schema.TypeList, + Computed: true, + Description: "A list of configuration options specific to Linux.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "domain": { + Type: schema.TypeString, + Computed: true, + Description: "The domain name for this virtual machine.", + }, + "host_name": { + Type: schema.TypeString, + Computed: true, + Description: "The hostname for this virtual machine.", + }, + "hw_clock_utc": { + Type: schema.TypeBool, + Computed: true, + Description: "Specifies whether or not the hardware clock should be in UTC or not.", + }, + "script_text": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The customization script to run before and or after guest customization.", + }, + "time_zone": { + Type: schema.TypeString, + Computed: true, + Description: "Set the time zone on the guest operating system. For a list of the acceptable values for Linux customization specifications, see [List of Time Zone Database Zones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) on Wikipedia.", + }, + }, + }, + }, + "windows_options": { + Type: schema.TypeList, + Computed: true, + Description: "A list of configuration options specific to Windows.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "run_once_command_list": { + Type: schema.TypeList, + Computed: true, + Description: "A list of commands to run at first user logon, after guest customization.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + // CustomizationGuiUnattended + "auto_logon": { + Type: schema.TypeBool, + Computed: true, + Description: "Specifies whether or not the VM automatically logs on as Administrator.", + }, + "auto_logon_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Specifies how many times the guest operating system should auto-logon the Administrator account when `auto_logon` is `true`.", + }, + "admin_password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "The new administrator password for this virtual machine.", + }, + "time_zone": { + Type: schema.TypeInt, + Computed: true, + Description: "The new time zone for the virtual machine. This is a sysprep-dictated timezone code.", + }, + + // CustomizationIdentification + "join_domain": { + Type: schema.TypeString, + Computed: true, + Description: "The Active Directory domain for the virtual machine to join.", + }, + "domain_admin_user": { + Type: schema.TypeString, + Computed: true, + Description: "The user account of the domain administrator used to join this virtual machine to the domain.", + }, + "domain_admin_password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "The user account used to join this virtual machine to the Active Directory domain.", + }, + "workgroup": { + Type: schema.TypeString, + Computed: true, + Description: "The workgroup for this virtual machine if not joining an Active Directory domain.", + }, + "computer_name": { + Type: schema.TypeString, + Computed: true, + Description: "The hostname for this virtual machine.", + }, + }, + }, + }, + "windows_sysprep_text": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Use this option to specify use of a Windows Sysprep file.", + }, + "network_interface": { + Type: schema.TypeList, + Computed: true, + Description: "A specification of network interface configuration options.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "dns_server_list": { + Type: schema.TypeList, + Computed: true, + Description: "Network-interface specific DNS settings for Windows operating systems. Ignored on Linux.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "dns_domain": { + Type: schema.TypeString, + Computed: true, + Description: "A DNS search domain to add to the DNS configuration on the virtual machine.", + }, + "ipv4_address": { + Type: schema.TypeString, + Computed: true, + Description: "The IPv4 address assigned to this network adapter. If left blank, DHCP is used.", + }, + "ipv4_netmask": { + Type: schema.TypeInt, + Computed: true, + Description: "The IPv4 CIDR netmask for the supplied IP address. Ignored if DHCP is selected.", + }, + "ipv6_address": { + Type: schema.TypeString, + Computed: true, + Description: "The IPv6 address assigned to this network adapter. If left blank, default auto-configuration is used.", + }, + "ipv6_netmask": { + Type: schema.TypeInt, + Computed: true, + Description: "The IPv6 CIDR netmask for the supplied IP address. Ignored if auto-configuration is selected.", + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceVSphereGuestCustomizationRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + name := d.Get("name").(string) + specItem, err := guestoscustomizations.FromName(client, name) + if err != nil { + return err + } + + d.SetId(name) + + return guestoscustomizations.FlattenGuestOsCustomizationSpec(d, specItem) +} diff --git a/vsphere/data_source_vsphere_guest_os_customization_test.go b/vsphere/data_source_vsphere_guest_os_customization_test.go new file mode 100644 index 000000000..e21deb6bd --- /dev/null +++ b/vsphere/data_source_vsphere_guest_os_customization_test.go @@ -0,0 +1,44 @@ +package vsphere + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "testing" +) + +func TestAccDataSourceVSphereGOSC_basic(t *testing.T) { + goscName := acctest.RandomWithPrefix("lin") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccDataSourceVSphereHostPreCheck(t) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceVSphereGOSCConfig(goscName), + Check: resource.TestCheckResourceAttr("data.vsphere_guest_os_customization.gosc1", "id", goscName), + }, + }, + }) +} + +func testAccDataSourceVSphereGOSCConfig(goscName string) string { + return fmt.Sprintf(` +resource "vsphere_guest_os_customization" "source" { + name = %q + type = "Linux" + spec { + linux_options { + domain = "example.com" + host_name = "linux" + } + } + } +data "vsphere_guest_os_customization" "gosc1" { + name = vsphere_guest_os_customization.source.id +} +`, goscName) +} diff --git a/vsphere/internal/vmworkflow/virtual_machine_customize_subresource.go b/vsphere/internal/helper/guestoscustomizations/customizations_helper.go similarity index 51% rename from vsphere/internal/vmworkflow/virtual_machine_customize_subresource.go rename to vsphere/internal/helper/guestoscustomizations/customizations_helper.go index fed59c251..88659b194 100644 --- a/vsphere/internal/vmworkflow/virtual_machine_customize_subresource.go +++ b/vsphere/internal/helper/guestoscustomizations/customizations_helper.go @@ -1,31 +1,39 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package vmworkflow +package guestoscustomizations import ( + "context" "errors" "fmt" - "net" - "regexp" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/provider" "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/structure" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/object" "github.com/vmware/govmomi/vim25/types" + "net" + "regexp" ) const ( - cKeyPrefix = "clone.0.customize.0" - cLinuxKeyPrefix = "clone.0.customize.0.linux_options.0" - cWindowsKeyPrefix = "clone.0.customize.0.windows_options.0" - cNetifKeyPrefix = "clone.0.customize.0.network_interface" + GuestOsCustomizationTypeWindows = "Windows" + GuestOsCustomizationTypeLinux = "Linux" + // GuestOsCustomizationHostNameFixed user enters a host name + GuestOsCustomizationHostNameFixed = "fixed" + + GuestOsCustomizationHostNamePrefixed = "prefixed" + GuestOsCustomizationHostNameUnknown = "unknown" + + GuestOsCustomizationHostNameVMname = "VMname" + + schemaPrefixVMClone = "clone.0.customize.0." + + schemaPrefixGOSC = "spec.0." ) -// netifKey renders a specific network_interface key for a specific resource -// index. -func netifKey(key string, n int) string { - return fmt.Sprintf("%s.%d.%s", cNetifKeyPrefix, n, key) +func netifKey(key string, n int, prefix string) string { + netifKeyPrefix := prefix + "network_interface" + return fmt.Sprintf("%s.%d.%s", netifKeyPrefix, n, key) } // matchGateway take an IP, mask, and gateway, and checks to see if the gateway @@ -54,8 +62,13 @@ func v4CIDRMaskToDotted(mask int) string { return fmt.Sprintf("%d.%d.%d.%d", a, b, c, d) } -// VirtualMachineCustomizeSchema returns the schema for VM customization. -func VirtualMachineCustomizeSchema() map[string]*schema.Schema { +type HostName struct { + Type string + Value string +} + +func SpecSchema(isVM bool) map[string]*schema.Schema { + prefix := getSchemaPrefix(isVM) return map[string]*schema.Schema{ // CustomizationGlobalIPSettings "dns_server_list": { @@ -76,7 +89,7 @@ func VirtualMachineCustomizeSchema() map[string]*schema.Schema { Type: schema.TypeList, Optional: true, MaxItems: 1, - ConflictsWith: []string{cKeyPrefix + "." + "windows_options", cKeyPrefix + "." + "windows_sysprep_text"}, + ConflictsWith: []string{prefix + "windows_options", prefix + "windows_sysprep_text"}, Description: "A list of configuration options specific to Linux virtual machines.", Elem: &schema.Resource{Schema: map[string]*schema.Schema{ "domain": { @@ -118,7 +131,7 @@ func VirtualMachineCustomizeSchema() map[string]*schema.Schema { Type: schema.TypeList, Optional: true, MaxItems: 1, - ConflictsWith: []string{cKeyPrefix + "." + "linux_options", cKeyPrefix + "." + "windows_sysprep_text"}, + ConflictsWith: []string{prefix + "linux_options", prefix + "windows_sysprep_text"}, Description: "A list of configuration options specific to Windows virtual machines.", Elem: &schema.Resource{Schema: map[string]*schema.Schema{ // CustomizationGuiRunOnce @@ -157,29 +170,29 @@ func VirtualMachineCustomizeSchema() map[string]*schema.Schema { "domain_admin_user": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{cWindowsKeyPrefix + "." + "workgroup"}, + ConflictsWith: []string{prefix + "windows_options.workgroup"}, Description: "The user account of the domain administrator used to join this virtual machine to the domain.", - RequiredWith: []string{cWindowsKeyPrefix + "." + "join_domain"}, + RequiredWith: []string{prefix + "windows_options.join_domain"}, }, "domain_admin_password": { Type: schema.TypeString, Optional: true, Sensitive: true, - ConflictsWith: []string{cWindowsKeyPrefix + "." + "workgroup"}, + ConflictsWith: []string{prefix + "windows_options.workgroup"}, Description: "The password of the domain administrator used to join this virtual machine to the domain.", - RequiredWith: []string{cWindowsKeyPrefix + "." + "join_domain"}, + RequiredWith: []string{prefix + "windows_options.join_domain"}, }, "join_domain": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{cWindowsKeyPrefix + "." + "workgroup"}, + ConflictsWith: []string{prefix + "windows_options.workgroup"}, Description: "The domain that the virtual machine should join.", - RequiredWith: []string{cWindowsKeyPrefix + "." + "domain_admin_user", cWindowsKeyPrefix + "." + "domain_admin_password"}, + RequiredWith: []string{prefix + "windows_options.domain_admin_user", prefix + "windows_options.domain_admin_password"}, }, "workgroup": { Type: schema.TypeString, Optional: true, - ConflictsWith: []string{cWindowsKeyPrefix + "." + "join_domain"}, + ConflictsWith: []string{prefix + "windows_options.join_domain"}, Description: "The workgroup for this virtual machine if not joining a domain.", }, @@ -215,7 +228,7 @@ func VirtualMachineCustomizeSchema() map[string]*schema.Schema { Type: schema.TypeString, Optional: true, Sensitive: true, - ConflictsWith: []string{cKeyPrefix + "." + "linux_options", cKeyPrefix + "." + "windows_options"}, + ConflictsWith: []string{prefix + "linux_options", prefix + "windows_options"}, Description: "Use this option to specify a windows sysprep file directly.", }, @@ -272,45 +285,287 @@ func VirtualMachineCustomizeSchema() map[string]*schema.Schema { Optional: true, Description: "The IPv6 default gateway when using network_interface customization on the virtual machine. This address must be local to a static IPv4 address configured in an interface sub-resource.", }, - "timeout": { - Type: schema.TypeInt, - Optional: true, - Default: 10, - Description: "The amount of time, in minutes, to wait for guest OS customization to complete before returning with an error. Setting this value to 0 or a negative value skips the waiter.", + } +} + +func FromName(client *govmomi.Client, name string) (*types.CustomizationSpecItem, error) { + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + + csm := object.NewCustomizationSpecManager(client.Client) + return csm.GetCustomizationSpec(ctx, name) +} + +func FlattenGuestOsCustomizationSpec(d *schema.ResourceData, specItem *types.CustomizationSpecItem) error { + d.Set("type", specItem.Info.Type) + d.Set("description", specItem.Info.Description) + d.Set("last_update_time", specItem.Info.LastUpdateTime.String()) + d.Set("change_version", specItem.Info.ChangeVersion) + + specData := make(map[string]interface{}) + specData["dns_server_list"] = specItem.Spec.GlobalIPSettings.DnsServerList + specData["dns_suffix_list"] = specItem.Spec.GlobalIPSettings.DnsSuffixList + + if specItem.Info.Type == GuestOsCustomizationTypeLinux { + linuxPrep := specItem.Spec.Identity.(*types.CustomizationLinuxPrep) + linuxOptions, err := flattenLinuxOptions(linuxPrep) + if err != nil { + return err + } + + specData["linux_options"] = linuxOptions + } else if specItem.Info.Type == GuestOsCustomizationTypeWindows { + sysprepText := flattenSysprepText(specItem.Spec.Identity) + if len(sysprepText) > 0 { + specData["windows_sysprep_text"] = sysprepText + } else { + specItemWinOptions := specItem.Spec.Identity.(*types.CustomizationSysprep) + windowsOptions, err := flattenWindowsOptions(specItemWinOptions) + if err != nil { + return err + } + + specData["windows_options"] = windowsOptions + } + + } + + var networkInterfaces []map[string]interface{} + for _, networkAdapterMapping := range specItem.Spec.NicSettingMap { + data := make(map[string]interface{}) + data["dns_server_list"] = networkAdapterMapping.Adapter.DnsServerList + data["dns_domain"] = networkAdapterMapping.Adapter.DnsDomain + data["ipv4_address"] = "" + if ipAddress, ok := networkAdapterMapping.Adapter.Ip.(*types.CustomizationFixedIp); ok { + data["ipv4_address"] = ipAddress.IpAddress + } + if len(networkAdapterMapping.Adapter.SubnetMask) > 0 { + ip := net.ParseIP(networkAdapterMapping.Adapter.SubnetMask) + if ip != nil { + addr := ip.To4() + mask, _ := net.IPv4Mask(addr[0], addr[1], addr[2], addr[3]).Size() + data["ipv4_netmask"] = mask + } + } + + if networkAdapterMapping.Adapter.IpV6Spec != nil { + ipV6IP, ok := networkAdapterMapping.Adapter.IpV6Spec.Ip[0].(*types.CustomizationFixedIpV6) + if ok { + data["ipv6_address"] = ipV6IP.IpAddress + data["ipv6_netmask"] = ipV6IP.SubnetMask + } + } + + networkInterfaces = append(networkInterfaces, data) + } + specData["network_interface"] = networkInterfaces + spec := []map[string]interface{}{specData} + d.Set("spec", spec) + + return nil +} + +func IsSpecOsApplicableToVmOs(vmOsFamily types.VirtualMachineGuestOsFamily, specType string) bool { + if specType == GuestOsCustomizationTypeWindows && vmOsFamily == types.VirtualMachineGuestOsFamilyWindowsGuest { + return true + } + + if specType == GuestOsCustomizationTypeLinux && vmOsFamily == types.VirtualMachineGuestOsFamilyLinuxGuest { + return true + } + + return false +} + +func ExpandGuestOsCustomizationSpec(d *schema.ResourceData) (*types.CustomizationSpecItem, error) { + osType := d.Get("type").(string) + osFamily := types.VirtualMachineGuestOsFamilyLinuxGuest + if osType == GuestOsCustomizationTypeWindows { + osFamily = types.VirtualMachineGuestOsFamilyWindowsGuest + } + + return &types.CustomizationSpecItem{ + Info: types.CustomizationSpecInfo{ + Name: d.Get("name").(string), + Type: osType, + Description: d.Get("description").(string), }, + Spec: ExpandCustomizationSpec(d, string(osFamily), false), + }, nil +} + +// ValidateCustomizationSpec checks the validity of the supplied customization +// spec. It should be called during diff customization to veto invalid configs. +func ValidateCustomizationSpec(d *schema.ResourceDiff, family string, isVM bool) error { + prefix := getSchemaPrefix(isVM) + // Validate that the proper section exists for OS family suboptions. + linuxExists := len(d.Get(prefix+"linux_options").([]interface{})) > 0 || !structure.ValuesAvailable(prefix+"linux_options.", []string{"host_name", "domain"}, d) + windowsExists := len(d.Get(prefix+"windows_options").([]interface{})) > 0 || !structure.ValuesAvailable(prefix+"windows_options.", []string{"computer_name"}, d) + sysprepExists := d.Get(prefix+"windows_sysprep_text").(string) != "" || !structure.ValuesAvailable(prefix, []string{"windows_sysprep_text"}, d) + switch { + case family == string(types.VirtualMachineGuestOsFamilyLinuxGuest) && !linuxExists: + return errors.New("linux_options must exist in VM customization options for Linux operating systems") + case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && !windowsExists && !sysprepExists: + return errors.New("one of windows_options or windows_sysprep_text must exist in VM customization options for Windows operating systems") + } + return nil +} +func flattenWindowsOptions(customizationPrep *types.CustomizationSysprep) ([]map[string]interface{}, error) { + winOptionsData := make(map[string]interface{}) + if customizationPrep.GuiRunOnce != nil { + winOptionsData["run_once_command_list"] = customizationPrep.GuiRunOnce.CommandList } + winOptionsData["auto_logon"] = customizationPrep.GuiUnattended.AutoLogon + winOptionsData["auto_logon_count"] = customizationPrep.GuiUnattended.AutoLogonCount + if customizationPrep.GuiUnattended.Password != nil { + winOptionsData["admin_password"] = customizationPrep.GuiUnattended.Password.Value + } + winOptionsData["time_zone"] = customizationPrep.GuiUnattended.TimeZone + winOptionsData["domain_admin_user"] = customizationPrep.Identification.DomainAdmin + if customizationPrep.Identification.DomainAdminPassword != nil { + winOptionsData["domain_admin_password"] = customizationPrep.Identification.DomainAdminPassword.Value + } + winOptionsData["join_domain"] = customizationPrep.Identification.JoinDomain + winOptionsData["workgroup"] = customizationPrep.Identification.JoinWorkgroup + hostName, err := flattenHostName(customizationPrep.UserData.ComputerName) + if err != nil { + return nil, err + } + winOptionsData["computer_name"] = hostName.Value + winOptionsData["full_name"] = customizationPrep.UserData.FullName + winOptionsData["organization_name"] = customizationPrep.UserData.OrgName + winOptionsData["product_key"] = customizationPrep.UserData.ProductId + + return []map[string]interface{}{winOptionsData}, nil } -// expandCustomizationGlobalIPSettings reads certain ResourceData keys and -// returns a CustomizationGlobalIPSettings. -func expandCustomizationGlobalIPSettings(d *schema.ResourceData) types.CustomizationGlobalIPSettings { - obj := types.CustomizationGlobalIPSettings{ - DnsSuffixList: structure.SliceInterfacesToStrings(d.Get(cKeyPrefix + "." + "dns_suffix_list").([]interface{})), - DnsServerList: structure.SliceInterfacesToStrings(d.Get(cKeyPrefix + "." + "dns_server_list").([]interface{})), +func flattenLinuxOptions(customizationPrep *types.CustomizationLinuxPrep) ([]map[string]interface{}, error) { + linuxOptionsData := make(map[string]interface{}) + linuxOptionsData["domain"] = customizationPrep.Domain + hostName, err := flattenHostName(customizationPrep.HostName) + if err != nil { + return nil, err + } + + linuxOptionsData["host_name"] = hostName.Value + + linuxOptionsData["hw_clock_utc"] = customizationPrep.HwClockUTC + linuxOptionsData["script_text"] = customizationPrep.ScriptText + linuxOptionsData["time_zone"] = customizationPrep.TimeZone + + return []map[string]interface{}{linuxOptionsData}, nil +} + +func flattenSysprepText(identity types.BaseCustomizationIdentitySettings) string { + sysprep, ok := identity.(*types.CustomizationSysprepText) + if ok { + return sysprep.Value + } + return "" +} + +func flattenHostName(hostName types.BaseCustomizationName) (HostName, error) { + if name, ok := hostName.(*types.CustomizationFixedName); ok { + return HostName{ + Type: GuestOsCustomizationHostNameFixed, + Value: name.Name, + }, nil + } + + if name, ok := hostName.(*types.CustomizationPrefixName); ok { + return HostName{ + Type: GuestOsCustomizationHostNamePrefixed, + Value: name.Base, + }, nil + } + + if _, ok := hostName.(*types.CustomizationVirtualMachineName); ok { + return HostName{ + Type: GuestOsCustomizationHostNameVMname, + }, nil + } + + if _, ok := hostName.(*types.CustomizationUnknownName); ok { + return HostName{ + Type: GuestOsCustomizationHostNameUnknown, + }, nil + } + + return HostName{}, errors.New("unknown linux host name type") +} + +// ExpandCustomizationSpec reads certain ResourceData keys and +// returns a CustomizationSpec. +func ExpandCustomizationSpec(d *schema.ResourceData, family string, isVM bool) types.CustomizationSpec { + prefix := getSchemaPrefix(isVM) + obj := types.CustomizationSpec{ + Identity: expandBaseCustomizationIdentitySettings(d, family, prefix), + GlobalIPSettings: expandCustomizationGlobalIPSettings(d, prefix), + NicSettingMap: expandSliceOfCustomizationAdapterMapping(d, prefix), + } + return obj +} + +// expandBaseCustomizationIdentitySettings returns a +// BaseCustomizationIdentitySettings, depending on what is defined. +// +// Only one of the three types of identity settings can be specified: Linux +// settings (from linux_options), Windows settings (from windows_options), and +// the raw Windows sysprep file (via windows_sysprep_text). +func expandBaseCustomizationIdentitySettings(d *schema.ResourceData, family string, prefix string) types.BaseCustomizationIdentitySettings { + var obj types.BaseCustomizationIdentitySettings + windowsExists := len(d.Get(prefix+"windows_options").([]interface{})) > 0 + sysprepExists := len(d.Get(prefix+"windows_sysprep_text").(string)) > 0 + switch { + case family == string(types.VirtualMachineGuestOsFamilyLinuxGuest): + linuxKeyPrefix := prefix + "linux_options.0." + obj = expandCustomizationLinuxPrep(d, linuxKeyPrefix) + case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && windowsExists: + windowsKeyPrefix := prefix + "windows_options.0." + obj = expandCustomizationSysprep(d, windowsKeyPrefix) + case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && sysprepExists: + obj = &types.CustomizationSysprepText{ + Value: d.Get(prefix + "windows_sysprep_text").(string), + } + default: + obj = &types.CustomizationIdentitySettings{} } return obj } // expandCustomizationLinuxPrep reads certain ResourceData keys and // returns a CustomizationLinuxPrep. -func expandCustomizationLinuxPrep(d *schema.ResourceData) *types.CustomizationLinuxPrep { +func expandCustomizationLinuxPrep(d *schema.ResourceData, prefix string) *types.CustomizationLinuxPrep { + obj := &types.CustomizationLinuxPrep{ HostName: &types.CustomizationFixedName{ - Name: d.Get(cLinuxKeyPrefix + "." + "host_name").(string), + Name: d.Get(prefix + "host_name").(string), }, - Domain: d.Get(cLinuxKeyPrefix + "." + "domain").(string), - TimeZone: d.Get(cLinuxKeyPrefix + "." + "time_zone").(string), - ScriptText: d.Get(cLinuxKeyPrefix + "." + "script_text").(string), - HwClockUTC: structure.GetBoolPtr(d, cLinuxKeyPrefix+"."+"hw_clock_utc"), + Domain: d.Get(prefix + "domain").(string), + TimeZone: d.Get(prefix + "time_zone").(string), + ScriptText: d.Get(prefix + "script_text").(string), + HwClockUTC: structure.GetBoolPtr(d, prefix+"hw_clock_utc"), + } + return obj +} + +// expandCustomizationSysprep reads certain ResourceData keys and +// returns a CustomizationSysprep. +func expandCustomizationSysprep(d *schema.ResourceData, prefix string) *types.CustomizationSysprep { + obj := &types.CustomizationSysprep{ + GuiUnattended: expandCustomizationGuiUnattended(d, prefix), + UserData: expandCustomizationUserData(d, prefix), + GuiRunOnce: expandCustomizationGuiRunOnce(d, prefix), + Identification: expandCustomizationIdentification(d, prefix), } return obj } // expandCustomizationGuiRunOnce reads certain ResourceData keys and // returns a CustomizationGuiRunOnce. -func expandCustomizationGuiRunOnce(d *schema.ResourceData) *types.CustomizationGuiRunOnce { +func expandCustomizationGuiRunOnce(d *schema.ResourceData, prefix string) *types.CustomizationGuiRunOnce { obj := &types.CustomizationGuiRunOnce{ - CommandList: structure.SliceInterfacesToStrings(d.Get(cWindowsKeyPrefix + "." + "run_once_command_list").([]interface{})), + CommandList: structure.SliceInterfacesToStrings(d.Get(prefix + "run_once_command_list").([]interface{})), } if len(obj.CommandList) < 1 { return nil @@ -320,13 +575,13 @@ func expandCustomizationGuiRunOnce(d *schema.ResourceData) *types.CustomizationG // expandCustomizationGuiUnattended reads certain ResourceData keys and // returns a CustomizationGuiUnattended. -func expandCustomizationGuiUnattended(d *schema.ResourceData) types.CustomizationGuiUnattended { +func expandCustomizationGuiUnattended(d *schema.ResourceData, prefix string) types.CustomizationGuiUnattended { obj := types.CustomizationGuiUnattended{ - TimeZone: int32(d.Get(cWindowsKeyPrefix + "." + "time_zone").(int)), - AutoLogon: d.Get(cWindowsKeyPrefix + "." + "auto_logon").(bool), - AutoLogonCount: int32(d.Get(cWindowsKeyPrefix + "." + "auto_logon_count").(int)), + TimeZone: int32(d.Get(prefix + "time_zone").(int)), + AutoLogon: d.Get(prefix + "auto_logon").(bool), + AutoLogonCount: int32(d.Get(prefix + "auto_logon_count").(int)), } - if v, ok := d.GetOk(cWindowsKeyPrefix + "." + "admin_password"); ok { + if v, ok := d.GetOk(prefix + "admin_password"); ok { obj.Password = &types.CustomizationPassword{ Value: v.(string), PlainText: true, @@ -338,13 +593,13 @@ func expandCustomizationGuiUnattended(d *schema.ResourceData) types.Customizatio // expandCustomizationIdentification reads certain ResourceData keys and // returns a CustomizationIdentification. -func expandCustomizationIdentification(d *schema.ResourceData) types.CustomizationIdentification { +func expandCustomizationIdentification(d *schema.ResourceData, prefix string) types.CustomizationIdentification { obj := types.CustomizationIdentification{ - JoinWorkgroup: d.Get(cWindowsKeyPrefix + "." + "workgroup").(string), - JoinDomain: d.Get(cWindowsKeyPrefix + "." + "join_domain").(string), - DomainAdmin: d.Get(cWindowsKeyPrefix + "." + "domain_admin_user").(string), + JoinWorkgroup: d.Get(prefix + "workgroup").(string), + JoinDomain: d.Get(prefix + "join_domain").(string), + DomainAdmin: d.Get(prefix + "domain_admin_user").(string), } - if v, ok := d.GetOk(cWindowsKeyPrefix + "." + "domain_admin_password"); ok { + if v, ok := d.GetOk(prefix + "domain_admin_password"); ok { obj.DomainAdminPassword = &types.CustomizationPassword{ Value: v.(string), PlainText: true, @@ -355,73 +610,87 @@ func expandCustomizationIdentification(d *schema.ResourceData) types.Customizati // expandCustomizationUserData reads certain ResourceData keys and // returns a CustomizationUserData. -func expandCustomizationUserData(d *schema.ResourceData) types.CustomizationUserData { +func expandCustomizationUserData(d *schema.ResourceData, prefix string) types.CustomizationUserData { obj := types.CustomizationUserData{ - FullName: d.Get(cWindowsKeyPrefix + "." + "full_name").(string), - OrgName: d.Get(cWindowsKeyPrefix + "." + "organization_name").(string), + FullName: d.Get(prefix + "full_name").(string), + OrgName: d.Get(prefix + "organization_name").(string), ComputerName: &types.CustomizationFixedName{ - Name: d.Get(cWindowsKeyPrefix + "." + "computer_name").(string), + Name: d.Get(prefix + "computer_name").(string), }, - ProductId: d.Get(cWindowsKeyPrefix + "." + "product_key").(string), + ProductId: d.Get(prefix + "product_key").(string), } return obj } -// expandCustomizationSysprep reads certain ResourceData keys and -// returns a CustomizationSysprep. -func expandCustomizationSysprep(d *schema.ResourceData) *types.CustomizationSysprep { - obj := &types.CustomizationSysprep{ - GuiUnattended: expandCustomizationGuiUnattended(d), - UserData: expandCustomizationUserData(d), - GuiRunOnce: expandCustomizationGuiRunOnce(d), - Identification: expandCustomizationIdentification(d), +// expandCustomizationGlobalIPSettings reads certain ResourceData keys and +// returns a CustomizationGlobalIPSettings. +func expandCustomizationGlobalIPSettings(d *schema.ResourceData, prefix string) types.CustomizationGlobalIPSettings { + obj := types.CustomizationGlobalIPSettings{ + DnsSuffixList: structure.SliceInterfacesToStrings(d.Get(prefix + "dns_suffix_list").([]interface{})), + DnsServerList: structure.SliceInterfacesToStrings(d.Get(prefix + "dns_server_list").([]interface{})), } return obj } -// expandCustomizationSysprepText reads certain ResourceData keys and -// returns a CustomizationSysprepText. -func expandCustomizationSysprepText(d *schema.ResourceData) *types.CustomizationSysprepText { - obj := &types.CustomizationSysprepText{ - Value: d.Get(cKeyPrefix + "." + "windows_sysprep_text").(string), +// expandSliceOfCustomizationAdapterMapping reads certain ResourceData keys and +// returns a CustomizationAdapterMapping slice. +func expandSliceOfCustomizationAdapterMapping(d *schema.ResourceData, prefix string) []types.CustomizationAdapterMapping { + s := d.Get(prefix + "network_interface").([]interface{}) + if len(s) < 1 { + return nil } - return obj + result := make([]types.CustomizationAdapterMapping, len(s)) + var v4gwFound, v6gwFound bool + for i := range s { + var adapter types.CustomizationIPSettings + adapter, v4gwFound, v6gwFound = expandCustomizationIPSettings(d, i, !v4gwFound, !v6gwFound, prefix) + obj := types.CustomizationAdapterMapping{ + Adapter: adapter, + } + result[i] = obj + } + return result } -// expandBaseCustomizationIdentitySettings returns a -// BaseCustomizationIdentitySettings, depending on what is defined. -// -// Only one of the three types of identity settings can be specified: Linux -// settings (from linux_options), Windows settings (from windows_options), and -// the raw Windows sysprep file (via windows_sysprep_text). -func expandBaseCustomizationIdentitySettings(d *schema.ResourceData, family string) types.BaseCustomizationIdentitySettings { - var obj types.BaseCustomizationIdentitySettings - _, windowsExists := d.GetOkExists(cKeyPrefix + "." + "windows_options") - _, sysprepExists := d.GetOkExists(cKeyPrefix + "." + "windows_sysprep_text") +// expandCustomizationIPSettings reads certain ResourceData keys and +// returns a CustomizationIPSettings. +func expandCustomizationIPSettings(d *schema.ResourceData, n int, v4gwAdd, v6gwAdd bool, prefix string) (types.CustomizationIPSettings, bool, bool) { + var v4gwFound, v6gwFound bool + v4addr, v4addrOk := d.GetOk(netifKey("ipv4_address", n, prefix)) + v4mask := d.Get(netifKey("ipv4_netmask", n, prefix)).(int) + v4gw, v4gwOk := d.Get(prefix + "ipv4_gateway").(string) + var obj types.CustomizationIPSettings switch { - case family == string(types.VirtualMachineGuestOsFamilyLinuxGuest): - obj = expandCustomizationLinuxPrep(d) - case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && windowsExists: - obj = expandCustomizationSysprep(d) - case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && sysprepExists: - obj = expandCustomizationSysprepText(d) + case v4addrOk: + obj.Ip = &types.CustomizationFixedIp{ + IpAddress: v4addr.(string), + } + obj.SubnetMask = v4CIDRMaskToDotted(v4mask) + // Check for the gateway + if v4gwAdd && v4gwOk && matchGateway(v4addr.(string), v4mask, v4gw) { + obj.Gateway = []string{v4gw} + v4gwFound = true + } default: - obj = &types.CustomizationIdentitySettings{} + obj.Ip = &types.CustomizationDhcpIpGenerator{} } - return obj + obj.DnsServerList = structure.SliceInterfacesToStrings(d.Get(netifKey("dns_server_list", n, prefix)).([]interface{})) + obj.DnsDomain = d.Get(netifKey("dns_domain", n, prefix)).(string) + obj.IpV6Spec, v6gwFound = expandCustomizationIPSettingsIPV6AddressSpec(d, n, v6gwAdd, prefix) + return obj, v4gwFound, v6gwFound } // expandCustomizationIPSettingsIPV6AddressSpec reads certain ResourceData keys and // returns a CustomizationIPSettingsIpV6AddressSpec. -func expandCustomizationIPSettingsIPV6AddressSpec(d *schema.ResourceData, n int, gwAdd bool) (*types.CustomizationIPSettingsIpV6AddressSpec, bool) { - v, ok := d.GetOk(netifKey("ipv6_address", n)) +func expandCustomizationIPSettingsIPV6AddressSpec(d *schema.ResourceData, n int, gwAdd bool, prefix string) (*types.CustomizationIPSettingsIpV6AddressSpec, bool) { + v, ok := d.GetOk(netifKey("ipv6_address", n, prefix)) var gwFound bool if !ok { return nil, gwFound } addr := v.(string) - mask := d.Get(netifKey("ipv6_netmask", n)).(int) - gw, gwOk := d.Get(cKeyPrefix + "." + "ipv6_gateway").(string) + mask := d.Get(netifKey("ipv6_netmask", n, prefix)).(int) + gw, gwOk := d.Get(prefix + "ipv6_gateway").(string) obj := &types.CustomizationIPSettingsIpV6AddressSpec{ Ip: []types.BaseCustomizationIpV6Generator{ &types.CustomizationFixedIpV6{ @@ -437,77 +706,10 @@ func expandCustomizationIPSettingsIPV6AddressSpec(d *schema.ResourceData, n int, return obj, gwFound } -// expandCustomizationIPSettings reads certain ResourceData keys and -// returns a CustomizationIPSettings. -func expandCustomizationIPSettings(d *schema.ResourceData, n int, v4gwAdd, v6gwAdd bool) (types.CustomizationIPSettings, bool, bool) { - var v4gwFound, v6gwFound bool - v4addr, v4addrOk := d.GetOk(netifKey("ipv4_address", n)) - v4mask := d.Get(netifKey("ipv4_netmask", n)).(int) - v4gw, v4gwOk := d.Get(cKeyPrefix + "." + "ipv4_gateway").(string) - var obj types.CustomizationIPSettings - switch { - case v4addrOk: - obj.Ip = &types.CustomizationFixedIp{ - IpAddress: v4addr.(string), - } - obj.SubnetMask = v4CIDRMaskToDotted(v4mask) - // Check for the gateway - if v4gwAdd && v4gwOk && matchGateway(v4addr.(string), v4mask, v4gw) { - obj.Gateway = []string{v4gw} - v4gwFound = true - } - default: - obj.Ip = &types.CustomizationDhcpIpGenerator{} - } - obj.DnsServerList = structure.SliceInterfacesToStrings(d.Get(netifKey("dns_server_list", n)).([]interface{})) - obj.DnsDomain = d.Get(netifKey("dns_domain", n)).(string) - obj.IpV6Spec, v6gwFound = expandCustomizationIPSettingsIPV6AddressSpec(d, n, v6gwAdd) - return obj, v4gwFound, v6gwFound -} - -// expandSliceOfCustomizationAdapterMapping reads certain ResourceData keys and -// returns a CustomizationAdapterMapping slice. -func expandSliceOfCustomizationAdapterMapping(d *schema.ResourceData) []types.CustomizationAdapterMapping { - s := d.Get(cKeyPrefix + "." + "network_interface").([]interface{}) - if len(s) < 1 { - return nil - } - result := make([]types.CustomizationAdapterMapping, len(s)) - var v4gwFound, v6gwFound bool - for i := range s { - var adapter types.CustomizationIPSettings - adapter, v4gwFound, v6gwFound = expandCustomizationIPSettings(d, i, !v4gwFound, !v6gwFound) - obj := types.CustomizationAdapterMapping{ - Adapter: adapter, - } - result[i] = obj - } - return result -} - -// ExpandCustomizationSpec reads certain ResourceData keys and -// returns a CustomizationSpec. -func ExpandCustomizationSpec(d *schema.ResourceData, family string) types.CustomizationSpec { - obj := types.CustomizationSpec{ - Identity: expandBaseCustomizationIdentitySettings(d, family), - GlobalIPSettings: expandCustomizationGlobalIPSettings(d), - NicSettingMap: expandSliceOfCustomizationAdapterMapping(d), +func getSchemaPrefix(inVMClone bool) string { + if inVMClone { + return schemaPrefixVMClone } - return obj -} -// ValidateCustomizationSpec checks the validity of the supplied customization -// spec. It should be called during diff customization to veto invalid configs. -func ValidateCustomizationSpec(d *schema.ResourceDiff, family string) error { - // Validate that the proper section exists for OS family suboptions. - linuxExists := len(d.Get(cKeyPrefix+"."+"linux_options").([]interface{})) > 0 || !structure.ValuesAvailable(cKeyPrefix+"."+"linux_options.", []string{"host_name", "domain"}, d) - windowsExists := len(d.Get(cKeyPrefix+"."+"windows_options").([]interface{})) > 0 || !structure.ValuesAvailable(cKeyPrefix+"."+"windows_options.", []string{"computer_name"}, d) - sysprepExists := d.Get(cKeyPrefix+"."+"windows_sysprep_text").(string) != "" || !structure.ValuesAvailable(cKeyPrefix+".", []string{"windows_sysprep_text"}, d) - switch { - case family == string(types.VirtualMachineGuestOsFamilyLinuxGuest) && !linuxExists: - return errors.New("linux_options must exist in VM customization options for Linux operating systems") - case family == string(types.VirtualMachineGuestOsFamilyWindowsGuest) && !windowsExists && !sysprepExists: - return errors.New("one of windows_options or windows_sysprep_text must exist in VM customization options for Windows operating systems") - } - return nil + return schemaPrefixGOSC } diff --git a/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go b/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go index 5fb50adc6..54f14bdbb 100644 --- a/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go +++ b/vsphere/internal/vmworkflow/virtual_machine_clone_subresource.go @@ -5,6 +5,7 @@ package vmworkflow import ( "fmt" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" "log" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -26,6 +27,14 @@ import ( // of a virtual machine through cloning from an existing template. // Customization is nested here, even though it exists in its own workflow. func VirtualMachineCloneSchema() map[string]*schema.Schema { + customizatonSpecSchema := guestoscustomizations.SpecSchema(true) + customizatonSpecSchema["timeout"] = &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 10, + Description: "The amount of time, in minutes, to wait for guest OS customization to complete before returning with an error. Setting this value to 0 or a negative value skips the waiter. Default: 10.", + } + return map[string]*schema.Schema{ "template_uuid": { Type: schema.TypeString, @@ -45,11 +54,34 @@ func VirtualMachineCloneSchema() map[string]*schema.Schema { ValidateFunc: validation.IntAtLeast(10), }, "customize": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "The customization spec for this clone. This allows the user to configure the virtual machine post-clone.", - Elem: &schema.Resource{Schema: VirtualMachineCustomizeSchema()}, + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"clone.0.customization_spec"}, + Description: "The customization specification for the virtual machine post-clone.", + Elem: &schema.Resource{Schema: customizatonSpecSchema}, + }, + "customization_spec": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "The customization spec for this clone. This allows the user to configure the virtual machine post-clone.", + ConflictsWith: []string{"clone.0.customize"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + Description: "The id of the GOSC spec equals to the GOSC name - unique per VC", + }, + "timeout": { + Type: schema.TypeInt, + Optional: true, + Default: 10, + Description: "The amount of time, in minutes, to wait for guest OS customization to complete before returning with an error. Setting this value to 0 or a negative value skips the waiter. Default: 10.", + }, + }, + }, }, "ovf_network_map": { Type: schema.TypeMap, @@ -151,7 +183,7 @@ func ValidateVirtualMachineClone(d *schema.ResourceDiff, c *govmomi.Client) erro return fmt.Errorf("cannot find OS family for guest ID %q: %s", d.Get("guest_id").(string), err) } // Validating the customization spec is valid for the vm/template's guest OS family - if err := ValidateCustomizationSpec(d, family); err != nil { + if err := guestoscustomizations.ValidateCustomizationSpec(d, family, true); err != nil { return err } } else { diff --git a/vsphere/provider.go b/vsphere/provider.go index d731c0676..06a47d610 100644 --- a/vsphere/provider.go +++ b/vsphere/provider.go @@ -141,6 +141,7 @@ func Provider() *schema.Provider { "vsphere_vm_storage_policy": resourceVMStoragePolicy(), "vsphere_role": resourceVsphereRole(), "vsphere_entity_permissions": resourceVsphereEntityPermissions(), + "vsphere_guest_os_customization": resourceVSphereGuestOsCustomization(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -169,6 +170,7 @@ func Provider() *schema.Provider { "vsphere_virtual_machine": dataSourceVSphereVirtualMachine(), "vsphere_vmfs_disks": dataSourceVSphereVmfsDisks(), "vsphere_role": dataSourceVsphereRole(), + "vsphere_guest_os_customization": dataSourceVSphereGuestOSCustomization(), }, ConfigureFunc: providerConfigure, diff --git a/vsphere/resource_vsphere_guest_os_customization.go b/vsphere/resource_vsphere_guest_os_customization.go new file mode 100644 index 000000000..ebcef1f8f --- /dev/null +++ b/vsphere/resource_vsphere_guest_os_customization.go @@ -0,0 +1,142 @@ +package vsphere + +import ( + "context" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/provider" + "github.com/vmware/govmomi/object" + "log" +) + +func resourceVSphereGuestOsCustomization() *schema.Resource { + return &schema.Resource{ + Create: resourceVSphereGuestOsCustomizationCreate, + Read: resourceVSphereGuestOsCustomizationRead, + Update: resourceVSphereGuestOsCustomizationUpdate, + Delete: resourceVSphereGuestOsCustomizationDelete, + Schema: getSchema(), + } +} + +func resourceVSphereGuestOsCustomizationRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + specItem, err := guestoscustomizations.FromName(client, d.Id()) + if err != nil { + return err + } + + return guestoscustomizations.FlattenGuestOsCustomizationSpec(d, specItem) +} + +func resourceVSphereGuestOsCustomizationCreate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Beginning creation of guest customization spec %s", d.Get("name")) + client := meta.(*Client).vimClient + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + + csm := object.NewCustomizationSpecManager(client.Client) + spec, err := guestoscustomizations.ExpandGuestOsCustomizationSpec(d) + if err != nil { + log.Printf("[ERROR] Error creating customization specification %s expansion: %s", d.Get("name"), err.Error()) + return err + } + log.Printf("[DEBUG] Successfully expanded customization specification %s", d.Get("name")) + + err = csm.CreateCustomizationSpec(ctx, *spec) + if err == nil { + log.Printf("[DEBUG] Successfully created customization specification %s", d.Get("name")) + d.SetId(spec.Info.Name) + return resourceVSphereGuestOsCustomizationRead(d, meta) + } + + log.Printf("[ERROR] Error creating customization specification %s: %s ", d.Get("name"), err.Error()) + + return err +} + +func resourceVSphereGuestOsCustomizationUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Updating customization specification %s", d.Get("name")) + client := meta.(*Client).vimClient + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + csm := object.NewCustomizationSpecManager(client.Client) + + oldName, newName := d.GetChange("name") + if oldName != newName { + log.Printf("[DEBUG] Renaming customization specification name %s to %s", oldName, newName) + err := csm.RenameCustomizationSpec(ctx, oldName.(string), newName.(string)) + if err != nil { + log.Printf("[ERROR] Renaming customization specification %s to %s: %s", oldName, newName, err.Error()) + return err + } + + log.Printf("[DEBUG] Successfully renamed customization specification %s to %s. Reseting the ID", oldName, newName) + d.SetId(newName.(string)) + } + + spec, err := guestoscustomizations.ExpandGuestOsCustomizationSpec(d) + if err != nil { + log.Printf("[ERROR] Error expanding the customization specification %s: %s ", d.Get("name"), err.Error()) + return err + } + + log.Printf("[DEBUG] Updating customization specification %s", d.Get("name").(string)) + err = csm.OverwriteCustomizationSpec(ctx, *spec) + if err != nil { + log.Printf("[ERROR] Error updating customization specification %s: %s", d.Get("name").(string), err.Error()) + return err + } + + log.Printf("[DEBUG] Successfully updated customization specification %s", d.Get("name").(string)) + + return nil +} + +func resourceVSphereGuestOsCustomizationDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + ctx, cancel := context.WithTimeout(context.Background(), provider.DefaultAPITimeout) + defer cancel() + csm := object.NewCustomizationSpecManager(client.Client) + return csm.DeleteCustomizationSpec(ctx, d.Id()) +} + +func getSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the customization specification is the unique identifier per vCenter Server instance.", + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: "The type of customization specification: One among: Windows, Linux.", + ValidateFunc: validation.StringInSlice([]string{guestoscustomizations.GuestOsCustomizationTypeLinux, guestoscustomizations.GuestOsCustomizationTypeWindows}, false), + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "The description for the customization specification.", + }, + "last_update_time": { + Type: schema.TypeString, + Computed: true, + Description: "The time of last modification to the customization specification.", + }, + "change_version": { + Type: schema.TypeString, + Computed: true, + Description: "The number of last changed version to the customization specification.", + }, + "spec": { + Type: schema.TypeList, + MaxItems: 1, + Required: true, + Elem: &schema.Resource{ + Schema: guestoscustomizations.SpecSchema(false), + }, + }, + } +} diff --git a/vsphere/resource_vsphere_guest_os_customization_test.go b/vsphere/resource_vsphere_guest_os_customization_test.go new file mode 100644 index 000000000..050bd5ca6 --- /dev/null +++ b/vsphere/resource_vsphere_guest_os_customization_test.go @@ -0,0 +1,176 @@ +package vsphere + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" + "testing" +) + +func TestAccResourceVSpherGOSC_windows_basic(t *testing.T) { + goscName := acctest.RandomWithPrefix("win") + goscResourceName := acctest.RandomWithPrefix("gosc") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccGOSCExists(goscResourceName, goscName, false), + Steps: []resource.TestStep{ + { + Config: testAccGOSCWindows(goscResourceName, goscName), + Check: testAccGOSCExists(goscResourceName, goscName, true), + }, + }, + }) +} + +func TestAccResourceVSpherGOSC_windows_workGroup(t *testing.T) { + goscName := acctest.RandomWithPrefix("win") + goscResourceName := acctest.RandomWithPrefix("gosc") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccGOSCExists(goscResourceName, goscName, false), + Steps: []resource.TestStep{ + { + Config: testAccGOSCWindowsAllPropsWorkGroup(goscResourceName, goscName), + Check: testAccGOSCExists(goscResourceName, goscName, true), + }, + }, + }) +} + +func TestAccResourceVSpherGOSC_linux(t *testing.T) { + goscName := acctest.RandomWithPrefix("lin") + goscResourceName := acctest.RandomWithPrefix("gosc") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccGOSCExists(goscResourceName, goscName, false), + Steps: []resource.TestStep{ + { + Config: testAccGOSCLinux(goscResourceName, goscName), + Check: testAccGOSCExists(goscResourceName, goscName, true), + }, + }, + }) +} + +func TestAccResourceVSpherGOSC_sysprep(t *testing.T) { + goscName := acctest.RandomWithPrefix("lin") + goscResourceName := acctest.RandomWithPrefix("gosc") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccGOSCExists(goscResourceName, goscName, false), + Steps: []resource.TestStep{ + { + Config: testAccGOSCWindowsPrep(goscResourceName, goscName), + Check: testAccGOSCExists(goscResourceName, goscName, true), + }, + }, + }) +} + +func testAccGOSCExists(resourceName string, goscName string, expectToExist bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource := fmt.Sprintf("vsphere_guest_os_customization.%s", resourceName) + vars, err := testClientVariablesForResource(s, resource) + if err != nil { + return err + } + _, err = guestoscustomizations.FromName(vars.client, goscName) + if err != nil && expectToExist { + return err + } + + return nil + } +} + +func testAccGOSCWindows(resourceName string, goscName string) string { + return fmt.Sprintf(` + resource "vsphere_guest_os_customization" %q { + name = %q + type = "Windows" + spec { + windows_options { + computer_name = "windows" + } + } + } + `, + resourceName, + goscName, + ) +} + +func testAccGOSCWindowsAllPropsWorkGroup(resourceName string, goscName string) string { + return fmt.Sprintf(` + resource "vsphere_guest_os_customization" %q { + name = %q + type = "Windows" + spec { + windows_options { + run_once_command_list = ["command-1", "command-2"] + computer_name = "windows" + auto_logon = false + auto_logon_count = 0 + admin_password = "VMware1!" + time_zone = 004 #(GMT-08:00) Pacific Time (US and Canada); Tijuana + workgroup = "workgroup" + } + } + } + `, + resourceName, + goscName, + ) +} + +func testAccGOSCWindowsPrep(resourceName string, goscName string) string { + return fmt.Sprintf(` + resource "vsphere_guest_os_customization" %q { + name = %q + type = "Windows" + spec { + windows_sysprep_text = "Test sysprep text" + } + } + `, + resourceName, + goscName, + ) +} + +func testAccGOSCLinux(resourceName string, goscName string) string { + return fmt.Sprintf(` + resource "vsphere_guest_os_customization" %q { + name = %q + type = "Linux" + spec { + linux_options { + domain = "example.com" + host_name = "linux" + } + network_interface {} + } + } + `, + resourceName, + goscName, + ) +} diff --git a/vsphere/resource_vsphere_virtual_machine.go b/vsphere/resource_vsphere_virtual_machine.go index 9c5671c57..3fe2a7536 100644 --- a/vsphere/resource_vsphere_virtual_machine.go +++ b/vsphere/resource_vsphere_virtual_machine.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/guestoscustomizations" "log" "net" "os" @@ -1689,7 +1690,9 @@ func resourceVSphereVirtualMachinePostDeployChanges(d *schema.ResourceData, meta var cw *virtualMachineCustomizationWaiter // Send customization spec if any has been defined. - if len(d.Get("clone.0.customize").([]interface{})) > 0 { + hasCustomizeInCloneConfig := len(d.Get("clone.0.customize").([]interface{})) > 0 + hasCustomizationSpecInCloneConfig := len(d.Get("clone.0.customization_spec").([]interface{})) > 0 + if hasCustomizeInCloneConfig || hasCustomizationSpecInCloneConfig { vmHardwareVersion := virtualmachine.GetHardwareVersionNumber(vprops.Config.Version) vmSpecHardwareVersion := d.Get("hardware_version").(int) if vmSpecHardwareVersion > vmHardwareVersion { @@ -1700,9 +1703,28 @@ func resourceVSphereVirtualMachinePostDeployChanges(d *schema.ResourceData, meta if err != nil { return fmt.Errorf("cannot find OS family for guest ID %q: %s", d.Get("guest_id").(string), err) } - custSpec := vmworkflow.ExpandCustomizationSpec(d, family) - cw = newVirtualMachineCustomizationWaiter(client, vm, d.Get("clone.0.customize.0.timeout").(int)) - if err := virtualmachine.Customize(vm, custSpec); err != nil { + var timeout int + var customizationSpec types.CustomizationSpec + if hasCustomizeInCloneConfig { + timeout = d.Get("clone.0.customize.0.timeout").(int) + customizationSpec = guestoscustomizations.ExpandCustomizationSpec(d, family, true) + } else { + timeout = d.Get("clone.0.customization_spec.0.timeout").(int) + goscName := d.Get("clone.0.customization_spec.0.id").(string) + specItem, err := guestoscustomizations.FromName(client, goscName) + if err != nil { + return err + } + + if !guestoscustomizations.IsSpecOsApplicableToVmOs(types.VirtualMachineGuestOsFamily(family), specItem.Info.Type) { + return fmt.Errorf("customization specification type %s is not applicable to OS family %s", specItem.Info.Type, family) + } + + customizationSpec = specItem.Spec + } + + cw = newVirtualMachineCustomizationWaiter(client, vm, timeout) + if err := virtualmachine.Customize(vm, customizationSpec); err != nil { // Roll back the VMs as per the error handling in reconfigure. if derr := resourceVSphereVirtualMachineDelete(d, meta); derr != nil { return fmt.Errorf(formatVirtualMachinePostCloneRollbackError, vm.InventoryPath, err, derr) diff --git a/vsphere/resource_vsphere_virtual_machine_test.go b/vsphere/resource_vsphere_virtual_machine_test.go index 05ec04ff1..15b566444 100644 --- a/vsphere/resource_vsphere_virtual_machine_test.go +++ b/vsphere/resource_vsphere_virtual_machine_test.go @@ -2517,6 +2517,27 @@ func TestAccResourceVSphereVirtualMachine_deployOvaFromUrl(t *testing.T) { }) } +func TestAccResourceVSphereVirtualMachine_cloneWithCustomizationSpec(t *testing.T) { + goscName := acctest.RandomWithPrefix("gosc") + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereVirtualMachinePreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereVirtualMachineCheckExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereVirtualMachineConfigCloneWithCustomizationSpec(goscName), + Check: resource.ComposeTestCheckFunc( + testAccResourceVSphereVirtualMachineCheckExists(true), + ), + }, + }, + }) +} + func testAccResourceVSphereVirtualMachinePreCheck(t *testing.T) { // Note that TF_VAR_VSPHERE_USE_LINKED_CLONE is also a variable and its presence // speeds up tests greatly, but it's not a necessary variable, so we don't @@ -7217,6 +7238,74 @@ resource "vsphere_virtual_machine" "vm" { ) } +func testAccResourceVSphereVirtualMachineConfigCloneWithCustomizationSpec(goscName string) string { + return fmt.Sprintf(` + %s + +data "vsphere_network" "network" { + name = "VM Network" + datacenter_id = data.vsphere_datacenter.rootdc1.id +} + +resource "vsphere_guest_os_customization_spec" "gosc_spec" { + name = %q + type = "Linux" + spec { + linux_options { + domain = "example.com" + host_name = "linux" + } + network_interface {} + } + +} + +data "vsphere_virtual_machine" "template" { + name = %q + datacenter_id = data.vsphere_datacenter.rootdc1.id +} + + +resource "vsphere_virtual_machine" "vm" { + name = "vm-1-template-clone" + resource_pool_id = data.vsphere_compute_cluster.rootcompute_cluster1.resource_pool_id + guest_id = data.vsphere_virtual_machine.template.guest_id + network_interface { + network_id = data.vsphere_network.network.id + } + datastore_id = data.vsphere_datastore.rootds1.id + + num_cpus = 2 + memory = 2048 + + scsi_type = data.vsphere_virtual_machine.template.scsi_type + wait_for_guest_ip_timeout = 0 + wait_for_guest_net_timeout = 0 + + disk { + label = "disk0" + size = data.vsphere_virtual_machine.template.disks.0.size + } + + clone { + template_uuid = data.vsphere_virtual_machine.template.id + customization_spec { + id = vsphere_guest_os_customization_spec.gosc_spec.id + } + } +} + +`, + testhelper.CombineConfigs( + testhelper.ConfigDataRootDC1(), + testhelper.ConfigDataRootComputeCluster1(), + testhelper.ConfigDataRootDS1(), + ), + goscName, + os.Getenv("TF_VAR_VSPHERE_TEMPLATE"), + ) +} + // Tests to skip until new features are developed. // Needs storage policy resource diff --git a/website/docs/d/guest_os_customization.html.markdown b/website/docs/d/guest_os_customization.html.markdown new file mode 100644 index 000000000..0ab2b93d5 --- /dev/null +++ b/website/docs/d/guest_os_customization.html.markdown @@ -0,0 +1,37 @@ +--- +subcategory: "Virtual Machine" +layout: "vsphere" +page_title: "VMware vSphere: vsphere_guest_os_customization" +sidebar_current: "docs-vsphere-data-guest-os-customization" +description: |- + Provides a VMware vSphere guest customization spec data source. This can be used to apply the customization spec when virtual machine is cloned +--- + +# vsphere\_guest\_os\_customization + +The `vsphere_guest_os_customization` data source can be used to discover the details about a customization specification for a guest operating system. + +Suggested change +~> **NOTE:** The name attribute is the unique identifier for the customization specification per vCenter Server instance. + + +## Example Usage + +```hcl + data "vsphere_guest_os_customization" "gosc1" { + name = "linux-spec" + } +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the customization specification is the unique identifier per vCenter Server instance. +## Attribute Reference + +* `type` - The type of customization specification: One among: Windows, Linux. +* `description` - The description for the customization specification. +* `last_update_time` - The time of last modification to the customization specification. +* `change_version` - The number of last changed version to the customization specification. +* `spec` - Container object for the guest operating system properties to be customized. See [virtual machine customizations](#virtual-machine-customizations) \ No newline at end of file diff --git a/website/docs/r/guest_os_customization.html.markdown b/website/docs/r/guest_os_customization.html.markdown new file mode 100644 index 000000000..7d9e48e0f --- /dev/null +++ b/website/docs/r/guest_os_customization.html.markdown @@ -0,0 +1,48 @@ +--- +subcategory: "Virtual Machine" +layout: "vsphere" +page_title: "VMware vSphere: vsphere_guest_os_customization" +sidebar_current: "docs-vsphere-data-guest-os-customization" +description: |- + Provides a VMware vSphere customization specification resource. This can be used to apply a customization specification to the guest operating system of a virtual machine after cloning. +--- + +# vsphere\_guest\_os\_customization + +The `vsphere_guest_os_customization` resource can be used to a customization specification for a guest operating system. + +~> **NOTE:** The name attribute is unique identifier for the guest OS spec per VC. + +## Example Usage + +```hcl + resource "vsphere_guest_os_customization" "windows_customization" { + name = "windows-spec" + type = "Windows" + spec { + windows_options { + run_once_command_list = ["command-1", "command-2"] + computer_name = "windows" + auto_logon = false + auto_logon_count = 0 + admin_password = "VMware1!" + time_zone = 004 + workgroup = "workgroup" + } + } + } +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the customization specification is the unique identifier per vCenter Server instance. +* `type` - (Required) The type of customization specification: One among: Windows, Linux. +* `description` - (Optional) The description for the customization specification. +* `spec` - Container object for the Guest OS properties about to be customized . See [virtual machine customizations](#virtual-machine-customizations) + +## Attribute Reference + +* `last_update_time` - The time of last modification to the customization specification. +* `change_version` - The number of last changed version to the customization specification. \ No newline at end of file