diff --git a/CHANGELOG.md b/CHANGELOG.md index ad264a868..cd6ee38a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ FEATURES: * `vcd_vapp_network` supports isolated network and vApp network connected to Org VDC networks [GH-455] * **New Data Source:** `vcd_vapp_org_network` vApp org network [GH-455] * **New Data Source:** `vcd_vapp_network` vApp network [GH-455] +* `resource/vcd_vapp_vm` and `datasource/vcd_vapp_vm` `customization` block supports all available features [GH-462] IMPROVEMENTS: @@ -35,6 +36,12 @@ BUG FIXES: * `resource/vcd_vapp_vm` `network` block changes caused MAC address changes in existing NICs [GH-436,GH-407] * Fix a potential data race in client connection caching when VCD_CACHE is enabled [GH-453] +* `resource/vcd_vapp_vm` when customization.0.force=false crashes with interface {} is nil [GH-462] +* `resource/vcd_vapp_vm` `customization.0.force=true` could have skipped "Forced customization" on each apply [GH-462] + +DEPRECATIONS: + +* `resource/vcd_vapp_vm` field `initscript` is now deprecated in favor of `customization.0.initscript` [GH-462] NOTES: @@ -42,6 +49,7 @@ NOTES: * Bump terraform-plugin-sdk to v1.5.0 [GH-442] * `make seqtestacc` and `make test-binary` use `-race` flags for `go test` to check if there are no data races. Additionally GNUMakefile supports `make installrace` and `make buildrace` to build binary with race detection enabled. [GH-453] +* Added `make test-upgrade-prepare` directive [GH-462] ## 2.6.0 (December 13, 2019) diff --git a/GNUmakefile b/GNUmakefile index b4d712dfe..352b5ef7c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -40,6 +40,10 @@ test-binary-orguser: install test-upgrade: @sh -c "'$(CURDIR)/scripts/test-upgrade.sh'" +# makes .tf files from test templates for upgrade testing, but does not execute them +test-upgrade-prepare: + @sh -c "skip_upgrade_execution=1 '$(CURDIR)/scripts/test-upgrade.sh'" + # runs test using Terraform binary as system administrator using binary with race detection enabled test-binary: installrace @sh -c "'$(CURDIR)/scripts/runtest.sh' short-provider" diff --git a/go.mod b/go.mod index 3421310ae..e885d2ab5 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,5 @@ go 1.13 require ( github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/terraform-plugin-sdk v1.5.0 - github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.1 + github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.2 ) diff --git a/go.sum b/go.sum index bcabbe968..4a5313c63 100644 --- a/go.sum +++ b/go.sum @@ -190,8 +190,8 @@ github.com/vmihailenco/msgpack v3.3.3+incompatible h1:wapg9xDUZDzGCNFlwc5SqI1rvc github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.1+incompatible h1:RMF1enSPeKTlXrXdOcqjFUElywVZjjC6pqse21bKbEU= github.com/vmihailenco/msgpack v4.0.1+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.1 h1:S1yYH2T3VXMGiUgWSOnBU1qbFjuMIwkP6rZlxOQZgCU= -github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.1/go.mod h1:XY9Fmqp1JeHIU2nP87NykB6unNCsY6qV4Zdu5JI4aZU= +github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.2 h1:xsKRAxgn+zh1CdHw0joJumuTepX07sxfAtqbvCKjtto= +github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.2/go.mod h1:XY9Fmqp1JeHIU2nP87NykB6unNCsY6qV4Zdu5JI4aZU= github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= diff --git a/scripts/skip-upgrade-tests.txt b/scripts/skip-upgrade-tests.txt index 93ae6b430..603c2fd98 100644 --- a/scripts/skip-upgrade-tests.txt +++ b/scripts/skip-upgrade-tests.txt @@ -4,4 +4,5 @@ # FileName version-from-which-we-upgrade "reason" # vcd.TestAccVcdVappNetworkMulti.tf v2.5.0 "Bug in guest_vlan_allowed apply for multiple networks" - +vcd.TestAccVcdVAppVmUpdateCustomization-step1.tf v2.6.0 "customization.force=true always triggers plan change" +vcd.TestAccVcdVAppVmCreateCustomization.tf v2.6.0 "customization.force=true always triggers plan change" diff --git a/vcd/datasource_vcd_network_test.go b/vcd/datasource_vcd_network_test.go index d9a3b4410..0063ef9b9 100644 --- a/vcd/datasource_vcd_network_test.go +++ b/vcd/datasource_vcd_network_test.go @@ -1,4 +1,4 @@ -// +build network ALL functional +// +build network vm ALL functional package vcd diff --git a/vcd/datasource_vcd_vapp_org_network_test.go b/vcd/datasource_vcd_vapp_org_network_test.go index 36fb45dfc..7a3fc7024 100644 --- a/vcd/datasource_vcd_vapp_org_network_test.go +++ b/vcd/datasource_vcd_vapp_org_network_test.go @@ -4,9 +4,10 @@ package vcd import ( "fmt" - "github.com/hashicorp/terraform-plugin-sdk/terraform" "testing" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" ) @@ -51,7 +52,6 @@ func TestAccVcdVappOrgNetworkDS(t *testing.T) { var params = StringMap{ "Org": testConfig.VCD.Org, "Vdc": testConfig.VCD.Vdc, - "resourceName": resourceName, "vappName": vapp.VApp.Name, "orgNetwork": data.network.Name, "firewallEnabled": fwEnabled, diff --git a/vcd/datasource_vcd_vapp_vm.go b/vcd/datasource_vcd_vapp_vm.go index c4d564ba6..5a90300b7 100644 --- a/vcd/datasource_vcd_vapp_vm.go +++ b/vcd/datasource_vcd_vapp_vm.go @@ -207,6 +207,95 @@ func datasourceVcdVAppVm() *schema.Resource { Computed: true, Description: "Key/value settings for guest properties", }, + + "customization": &schema.Schema{ + Computed: true, + MinItems: 1, + MaxItems: 1, + Type: schema.TypeList, + Description: "Guest customization block", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "force": { + Type: schema.TypeBool, + Computed: true, + Description: "'true' value will cause the VM to reboot on every 'apply' operation", + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "'true' value will enable guest customization. It may occur on first boot or when 'force' is used", + }, + "change_sid": { + Type: schema.TypeBool, + Computed: true, + Description: "'true' value will change SID. Applicable only for Windows VMs", + }, + "allow_local_admin_password": { + Type: schema.TypeBool, + Computed: true, + Description: "Allow local administrator password", + }, + "must_change_password_on_first_login": { + Type: schema.TypeBool, + Computed: true, + Description: "Require Administrator to change password on first login", + }, + "auto_generate_password": { + Type: schema.TypeBool, + Computed: true, + Description: "Auto generate password", + }, + "admin_password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Manually specify admin password", + }, + "number_of_auto_logons": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of times to log on automatically", + }, + "join_domain": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable this VM to join a domain", + }, + "join_org_domain": { + Type: schema.TypeBool, + Computed: true, + Description: "Use organization's domain for joining", + }, + "join_domain_name": { + Type: schema.TypeString, + Computed: true, + Description: "Custom domain name for join", + }, + "join_domain_user": { + Type: schema.TypeString, + Computed: true, + Description: "Username for custom domain name join", + }, + "join_domain_password": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + Description: "Password for custom domain name join", + }, + "join_domain_account_ou": { + Type: schema.TypeString, + Computed: true, + Description: "Account organizational unit for domain name join", + }, + "initscript": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Description: "Script to run on initial boot or with customization.force=true set", + }, + }, + }, + }, }, } } diff --git a/vcd/datasource_vcd_vapp_vm_test.go b/vcd/datasource_vcd_vapp_vm_test.go index 0bfe4ad03..a796b367c 100644 --- a/vcd/datasource_vcd_vapp_vm_test.go +++ b/vcd/datasource_vcd_vapp_vm_test.go @@ -60,6 +60,11 @@ func TestAccVcdVappVmDS(t *testing.T) { resource.TestCheckOutput("storage_profile", vm.VM.StorageProfile.Name), resource.TestCheckOutput("description", vm.VM.Description), resource.TestCheckOutput("href", vm.VM.HREF), + resource.TestCheckResourceAttr("data.vcd_vapp_vm.vm-ds", "customization.0.enabled", "true"), + resource.TestCheckResourceAttr("data.vcd_vapp_vm.vm-ds", "customization.0.change_sid", "false"), + resource.TestCheckResourceAttr("data.vcd_vapp_vm.vm-ds", "customization.0.join_domain", "false"), + resource.TestCheckResourceAttr("data.vcd_vapp_vm.vm-ds", "customization.0.admin_password", ""), + resource.TestCheckResourceAttr("data.vcd_vapp_vm.vm-ds", "customization.0.number_of_auto_logons", "0"), ), }, }, diff --git a/vcd/resource_vcd_vapp_vm.go b/vcd/resource_vcd_vapp_vm.go index 809002e66..9009b712f 100644 --- a/vcd/resource_vcd_vapp_vm.go +++ b/vcd/resource_vcd_vapp_vm.go @@ -105,10 +105,12 @@ var vappVmSchema = map[string]*schema.Schema{ Type: schema.TypeString, }, "initscript": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "Script to run on initial boot or with customization.force=true set", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "Script to run on initial boot or with customization.force=true set", + Deprecated: "Please use `initscript` setting in `customization` block instead", + ConflictsWith: []string{"customization.0.initscript"}, }, "metadata": { Type: schema.TypeMap, @@ -358,6 +360,7 @@ var vappVmSchema = map[string]*schema.Schema{ }, "customization": &schema.Schema{ Optional: true, + Computed: true, MinItems: 1, MaxItems: 1, Type: schema.TypeList, @@ -376,6 +379,94 @@ var vappVmSchema = map[string]*schema.Schema{ DiffSuppressFunc: suppressFalse(), Description: "'true' value will cause the VM to reboot on every 'apply' operation", }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "'true' value will enable guest customization. It may occur on first boot or when 'force' is used", + }, + "change_sid": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "'true' value will change SID. Applicable only for Windows VMs", + }, + "allow_local_admin_password": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Allow local administrator password", + }, + "must_change_password_on_first_login": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Require Administrator to change password on first login", + }, + "auto_generate_password": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Auto generate password", + }, + "admin_password": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + Description: "Manually specify admin password", + }, + "number_of_auto_logons": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Number of times to log on automatically. '0' - disabled.", + ValidateFunc: validation.IntAtLeast(0), + }, + "join_domain": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Enable this VM to join a domain", + }, + "join_org_domain": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Use organization's domain for joining", + }, + "join_domain_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Custom domain name for join", + }, + "join_domain_user": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Username for custom domain name join", + }, + "join_domain_password": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + Description: "Password for custom domain name join", + }, + "join_domain_account_ou": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Account organizational unit for domain name join", + }, + "initscript": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Script to run on initial boot or with customization.force=true set", + ConflictsWith: []string{"initscript"}, + }, }, }, }, @@ -471,6 +562,9 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("[VM creation] error getting VM %s : %s", vmName, err) } + // VM creation already succeeded so ID must be set + d.SetId(vm.VM.ID) + // The below operation assumes VM is powered off and does not check for it because VM is being // powered on in the last stage of create/update cycle if d.Get("expose_hardware_virtualization").(bool) { @@ -486,37 +580,8 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { } } - // for back compatibility we allow to set computer name from `name` if computer_name isn't provided - var computerName string - if cName, ok := d.GetOk("computer_name"); ok { - computerName = cName.(string) - } else { - computerName = d.Get("name").(string) - } - - if initScript, ok := d.GetOk("initscript"); ok { - if _, ok := d.GetOk("computer_name"); !ok { - _, _ = fmt.Fprint(getTerraformStdout(), "WARNING of DEPRECATED behavior: when `initscript` is set,"+ - " VM `name` is used as a computer name - this behavior will be removed in future versions, hence please use the new `computer_name` field instead\n") - } - task, err := vm.RunCustomizationScript(computerName, initScript.(string)) - if err != nil { - return fmt.Errorf("error with init script setting: %s", err) - } - err = task.WaitTaskCompletion() - if err != nil { - return fmt.Errorf(errorCompletingTask, err) - } - } else if newComputerName, ok := d.GetOk("computer_name"); ok { - customizationSection, err := vm.GetGuestCustomizationSection() - if err != nil { - return fmt.Errorf("error get customization section before applying computer name: %s", err) - } - customizationSection.ComputerName = newComputerName.(string) - _, err = vm.SetGuestCustomizationSection(customizationSection) - if err != nil { - return fmt.Errorf("error with applying computer name: %s", err) - } + if err := updateGuestCustomizationSetting(d, vm); err != nil { + return fmt.Errorf("error setting guest customization during creation: %s", err) } if _, ok := d.GetOk("guest_properties"); ok { @@ -532,8 +597,6 @@ func resourceVcdVAppVmCreate(d *schema.ResourceData, meta interface{}) error { } } - d.SetId(vm.VM.ID) - // update existing internal disks in template err = updateTemplateInternalDisks(d, meta, *vm) if err != nil { @@ -788,13 +851,23 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er } } - if d.HasChange("memory") || d.HasChange("cpus") || d.HasChange("cpu_cores") || d.HasChange("power_on") || d.HasChange("disk") || - d.HasChange("expose_hardware_virtualization") || d.HasChange("network") || d.HasChange("computer_name") { + // Update guest customization if any of the customization related fields have changed + if d.HasChanges("customization", "computer_name", "name", "initscript") { + log.Printf("[TRACE] VM %s customization has changes: customization(%t), computer_name(%t), name(%t), initscript(%t)", + vm.VM.Name, d.HasChange("customization"), d.HasChange("computer_name"), d.HasChange("name"), d.HasChange("initscript")) + err = updateGuestCustomizationSetting(d, vm) + if err != nil { + return fmt.Errorf("errors updating guest customization: %s", err) + } + + } + + if d.HasChanges("memory", "cpus", "cpu_cores", "power_on", "disk", "expose_hardware_virtualization", "network") { log.Printf("[TRACE] VM %s has changes: memory(%t), cpus(%t), cpu_cores(%t), power_on(%t), disk(%t), expose_hardware_virtualization(%t),"+ - " network(%t), computer_name(%t)", + " network(%t)", vm.VM.Name, d.HasChange("memory"), d.HasChange("cpus"), d.HasChange("cpu_cores"), d.HasChange("power_on"), d.HasChange("disk"), - d.HasChange("expose_hardware_virtualization"), d.HasChange("network"), d.HasChange("computer_name")) + d.HasChange("expose_hardware_virtualization"), d.HasChange("network")) // If customization is not requested then a simple shutdown is enough if vmStatusBeforeUpdate != "POWERED_OFF" && !customizationNeeded { @@ -892,20 +965,6 @@ func resourceVcdVAppVmUpdateExecute(d *schema.ResourceData, meta interface{}) er return fmt.Errorf("unable to update network configuration: %s", err) } } - - // we pass init script, to not override with empty one - if d.HasChange("computer_name") { - customizationSection, err := vm.GetGuestCustomizationSection() - if err != nil { - return fmt.Errorf("error get customization section before applying computer name: %s", err) - } - customizationSection.ComputerName = d.Get("computer_name").(string) - _, err = vm.SetGuestCustomizationSection(customizationSection) - if err != nil { - return fmt.Errorf("error with applying computer name: %s", err) - } - } - } // If the VM was powered off during update but it has to be powered off @@ -1162,11 +1221,9 @@ func genericVcdVAppVmRead(d *schema.ResourceData, meta interface{}, origin strin return fmt.Errorf("[VM read] error reading attached disks : %s", err) } - guestCustomizationSection, err := vm.GetGuestCustomizationSection() - if err != nil { - return fmt.Errorf("[VM read] error reading guest customization : %s", err) + if err := setGuestCustomizationData(d, vm); err != nil { + return fmt.Errorf("error storing customzation block: %s", err) } - _ = d.Set("computer_name", guestCustomizationSection.ComputerName) log.Printf("[DEBUG] [VM read] finished with origin %s", origin) return nil @@ -1808,3 +1865,159 @@ func resourceVcdVappVmImport(d *schema.ResourceData, meta interface{}) ([]*schem d.SetId(vm.VM.ID) return []*schema.ResourceData{d}, nil } + +// updateGuestCustomizationSetting is responsible for setting all the data related to VM customization +func updateGuestCustomizationSetting(d *schema.ResourceData, vm *govcd.VM) error { + // Retrieve existing customization section to only customize what was throughout this function + customizationSection, err := vm.GetGuestCustomizationSection() + if err != nil { + return fmt.Errorf("error getting existing customization section before changing: %s", err) + } + + // for back compatibility we allow to set computer name from `name` if computer_name isn't provided + var computerName string + if cName, ok := d.GetOk("computer_name"); ok { + computerName = cName.(string) + } else { + computerName = d.Get("name").(string) + } + + // When initscript is set + if initScript, ok := d.GetOk("initscript"); ok { + if _, ok := d.GetOk("computer_name"); !ok { + _, _ = fmt.Fprint(getTerraformStdout(), "WARNING of DEPRECATED behavior: when `initscript` is set,"+ + " VM `name` is used as a computer name - this behavior will be removed in future versions, hence please use the new `computer_name` field instead\n") + } + // As per legacy behavior compatibility - specifying initscript automatically enables customization + customizationSection.Enabled = takeBoolPointer(true) + customizationSection.CustomizationScript = initScript.(string) + customizationSection.ComputerName = computerName + } else if _, isSetComputerName := d.GetOk("computer_name"); isSetComputerName { + customizationSection.ComputerName = computerName + } + + // Process parameters from 'customization' block + customizationInterface := d.Get("customization") + customizationSlice := customizationInterface.([]interface{}) + if len(customizationSlice) == 1 { + cust := customizationSlice[0] + if cust != nil { + + // Only react to "enabled" field when legacy `initscript` is not specified. Legacy behavior is such that when `initscript` + // is specified - guest customization is enabled by default therefore we ignore "enabled" field + if _, isSetDeprecatedInitScript := d.GetOk("initscript"); !isSetDeprecatedInitScript { + if enabled, isSetEnabled := d.GetOkExists("customization.0.enabled"); isSetEnabled { + customizationSection.Enabled = takeBoolPointer(enabled.(bool)) + } + } + + // customization.0.initscript should be set here. Once people migrate to 'customization.0.initscript' there is + // no longer need for previous "magic" behaviour which automatically set `customization=true` + if initScript, isSetInitScript := d.GetOkExists("customization.0.initscript"); isSetInitScript { + customizationSection.CustomizationScript = initScript.(string) + } + + if changeSid, isSetChangeSid := d.GetOkExists("customization.0.change_sid"); isSetChangeSid { + customizationSection.ChangeSid = takeBoolPointer(changeSid.(bool)) + } + + if allowLocalAdminPasswd, isSetAllowLocalAdminPasswd := d.GetOkExists("customization.0.allow_local_admin_password"); isSetAllowLocalAdminPasswd { + customizationSection.AdminPasswordEnabled = takeBoolPointer(allowLocalAdminPasswd.(bool)) + + } + + if mustChangeOnFirstLogin, isSetMustChangeOnFirstLogin := d.GetOkExists("customization.0.must_change_password_on_first_login"); isSetMustChangeOnFirstLogin { + customizationSection.ResetPasswordRequired = takeBoolPointer(mustChangeOnFirstLogin.(bool)) + } + + if autoGeneratePasswd, isSetAutoGeneratePasswd := d.GetOkExists("customization.0.auto_generate_password"); isSetAutoGeneratePasswd { + customizationSection.AdminPasswordAuto = takeBoolPointer(autoGeneratePasswd.(bool)) + } + + if adminPasswd, isSetAdminPasswd := d.GetOkExists("customization.0.admin_password"); isSetAdminPasswd { + customizationSection.AdminPassword = adminPasswd.(string) + // customizationSection.AdminPasswordEnabled = takeBoolPointer(true) + } + + if nrTimesForLogin, isSetNrTimesForLogin := d.GetOkExists("customization.0.number_of_auto_logons"); isSetNrTimesForLogin { + // The AdminAutoLogonEnabled is "hidden" from direct user input to behave exactly like UI does. UI sets + // the value of this field behind the scenes based on number_of_auto_logons count. + // AdminAutoLogonEnabled=false if number_of_auto_logons == 0 + // AdminAutoLogonEnabled=true if number_of_auto_logons > 0 + isMoreThanZero := nrTimesForLogin.(int) > 0 + customizationSection.AdminAutoLogonEnabled = takeBoolPointer(isMoreThanZero) + + customizationSection.AdminAutoLogonCount = nrTimesForLogin.(int) + } + + if joinDomain, isSetJoinDomain := d.GetOkExists("customization.0.join_domain"); isSetJoinDomain { + customizationSection.JoinDomainEnabled = takeBoolPointer(joinDomain.(bool)) + } + + if joinOrgDomain, isSetJoinOrgDomain := d.GetOkExists("customization.0.join_org_domain"); isSetJoinOrgDomain { + customizationSection.UseOrgSettings = takeBoolPointer(joinOrgDomain.(bool)) + } + + if joinDomainName, isSetJoinDomainName := d.GetOkExists("customization.0.join_domain_name"); isSetJoinDomainName { + customizationSection.DomainName = joinDomainName.(string) + } + + if joinDomainUser, isSetJoinDomainUser := d.GetOkExists("customization.0.join_domain_user"); isSetJoinDomainUser { + customizationSection.DomainUserName = joinDomainUser.(string) + } + + if joinDomainPasswd, isSetJoinDomainPasswd := d.GetOkExists("customization.0.join_domain_password"); isSetJoinDomainPasswd { + customizationSection.DomainUserPassword = joinDomainPasswd.(string) + } + + if joinDomainOu, isSetJoinDomainOu := d.GetOkExists("customization.0.join_domain_account_ou"); isSetJoinDomainOu { + customizationSection.MachineObjectOU = joinDomainOu.(string) + } + + } + } + + // Apply any of the settings we have set + if _, err = vm.SetGuestCustomizationSection(customizationSection); err != nil { + return fmt.Errorf("error applying guest customization details: %s", err) + } + + return nil +} + +// setGuestCustomizationData is responsible for persisting all guest customization details into statefile +func setGuestCustomizationData(d *schema.ResourceData, vm *govcd.VM) error { + customizationSection, err := vm.GetGuestCustomizationSection() + if err != nil { + return fmt.Errorf("unable to get guest customization section: %s", err) + } + + _ = d.Set("computer_name", customizationSection.ComputerName) + + customizationBlock := make([]interface{}, 1) + customizationBlockAttributes := make(map[string]interface{}) + + customizationBlockAttributes["enabled"] = customizationSection.Enabled + customizationBlockAttributes["change_sid"] = customizationSection.ChangeSid + customizationBlockAttributes["allow_local_admin_password"] = customizationSection.AdminPasswordEnabled + customizationBlockAttributes["must_change_password_on_first_login"] = customizationSection.ResetPasswordRequired + customizationBlockAttributes["auto_generate_password"] = customizationSection.AdminPasswordAuto + customizationBlockAttributes["admin_password"] = customizationSection.AdminPassword + customizationBlockAttributes["number_of_auto_logons"] = customizationSection.AdminAutoLogonCount + customizationBlockAttributes["join_domain"] = customizationSection.JoinDomainEnabled + customizationBlockAttributes["join_org_domain"] = customizationSection.UseOrgSettings + customizationBlockAttributes["join_domain_name"] = customizationSection.DomainName + customizationBlockAttributes["join_domain_user"] = customizationSection.DomainUserName + customizationBlockAttributes["join_domain_password"] = customizationSection.DomainUserPassword + customizationBlockAttributes["join_domain_account_ou"] = customizationSection.MachineObjectOU + customizationBlockAttributes["initscript"] = customizationSection.CustomizationScript + + customizationBlock[0] = customizationBlockAttributes + + err = d.Set("customization", customizationBlock) + if err != nil { + return fmt.Errorf("") + } + + return nil +} diff --git a/vcd/resource_vcd_vapp_vm_customization_test.go b/vcd/resource_vcd_vapp_vm_customization_test.go index 1497442e1..267771b26 100644 --- a/vcd/resource_vcd_vapp_vm_customization_test.go +++ b/vcd/resource_vcd_vapp_vm_customization_test.go @@ -4,6 +4,7 @@ package vcd import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" @@ -38,7 +39,10 @@ func TestAccVcdVAppVmUpdateCustomization(t *testing.T) { configTextVM := templateFill(testAccCheckVcdVAppVmUpdateCustomization, params) params["FuncName"] = t.Name() + "-step1" - configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmUpdateCustomizationStep1, params) + params["Customization"] = "true" + params["SkipTest"] = "# skip-binary-test: customization.force=true must always request for update" + configTextVMUpdateStep1 := templateFill(testAccCheckVcdVAppVmCreateCustomization, params) + if vcdShortTest { t.Skip(acceptanceTestsSkipped) return @@ -57,22 +61,24 @@ func TestAccVcdVAppVmUpdateCustomization(t *testing.T) { testAccCheckVcdVMCustomization("vcd_vapp_vm.test-vm", false), testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "0"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), ), }, // Step 1 - Update - change network configuration and force customization resource.TestStep{ Config: configTextVMUpdateStep1, + // The plan should never be empty because force works as a flag and every update triggers "update" + ExpectNonEmptyPlan: true, Check: resource.ComposeAggregateTestCheckFunc( testAccCheckVcdVMCustomization("vcd_vapp_vm.test-vm", true), testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "2"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "false"), ), }, }, @@ -93,16 +99,17 @@ func TestAccVcdVAppVmCreateCustomization(t *testing.T) { ) var params = StringMap{ - "Org": testConfig.VCD.Org, - "Vdc": testConfig.VCD.Vdc, - "EdgeGateway": testConfig.Networking.EdgeGateway, - "Catalog": testSuiteCatalogName, - "CatalogItem": testSuiteCatalogOVAItem, - "VAppName": netVappName, - "VMName": netVmName1, - "Tags": "vapp vm", + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "Catalog": testSuiteCatalogName, + "CatalogItem": testSuiteCatalogOVAItem, + "VAppName": netVappName, + "VMName": netVmName1, + "Tags": "vapp vm", + "Customization": "true", } - + params["SkipTest"] = "# skip-binary-test: customization.force=true must always request for update" configTextVMUpdateStep2 := templateFill(testAccCheckVcdVAppVmCreateCustomization, params) if vcdShortTest { @@ -118,13 +125,16 @@ func TestAccVcdVAppVmCreateCustomization(t *testing.T) { // Step 0 - Create new VM and force customization initially resource.TestStep{ Config: configTextVMUpdateStep2, + // The plan should never be empty because force works as a flag and every update triggers "update" + ExpectNonEmptyPlan: true, Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm2", &vapp, &vm), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "name", netVmName1), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "network.#", "1"), + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "customization.#", "1"), - resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm2", "customization.0.force", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), + // Always store 'customization.0.force=false' in statefile so that a diff is always triggered + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.force", "false"), ), }, }, @@ -213,27 +223,26 @@ resource "vcd_vapp" "test-vapp" { name = "{{.VAppName}}" } +` -resource "vcd_vapp_network" "vappNet" { +const testAccCheckVcdVAppVmUpdateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` +resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" - name = "vapp-net" - vapp_name = vcd_vapp.test-vapp.name - gateway = "192.168.2.1" - netmask = "255.255.255.0" - dns1 = "192.168.2.1" - dns2 = "192.168.2.2" - dns_suffix = "mybiz.biz" - - static_ip_pool { - start_address = "192.168.2.51" - end_address = "192.168.2.100" - } + vapp_name = vcd_vapp.test-vapp.name + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 + } ` -const testAccCheckVcdVAppVmUpdateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` +const testAccCheckVcdVAppVmCreateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` +{{.SkipTest}} resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -246,15 +255,159 @@ resource "vcd_vapp_vm" "test-vm" { cpus = 2 cpu_cores = 1 - network { - type = "vapp" - name = vcd_vapp_network.vappNet.name - ip_allocation_mode = "POOL" + customization { + force = {{.Customization}} } } ` -const testAccCheckVcdVAppVmUpdateCustomizationStep1 = testAccCheckVcdVAppVmCustomizationShared + ` +// TestAccVcdVAppVmCreateCustomizationFalse checks if VM is booted up successfully when customization.force=true. +// This test covers a previous bug. +func TestAccVcdVAppVmCreateCustomizationFalse(t *testing.T) { + var ( + vapp govcd.VApp + vm govcd.VM + netVappName string = t.Name() + netVmName1 string = t.Name() + "VM" + ) + + var params = StringMap{ + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "Catalog": testSuiteCatalogName, + "CatalogItem": testSuiteCatalogOVAItem, + "VAppName": netVappName, + "VMName": netVmName1, + "Tags": "vapp vm", + "Customization": "false", + "SkipTest": "", + } + + configTextVM := templateFill(testAccCheckVcdVAppVmCreateCustomization, params) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), + Steps: []resource.TestStep{ + // Step 0 - Create new VM and set set customization.force=false + resource.TestStep{ + Config: configTextVM, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + ), + }, + }, + }) +} + +// TestAccVcdVAppVmCustomizationSettings tests out possible customization options +func TestAccVcdVAppVmCustomizationSettings(t *testing.T) { + var ( + vapp govcd.VApp + vm govcd.VM + netVappName string = t.Name() + netVmName1 string = t.Name() + "VM" + ) + + var params = StringMap{ + "Org": testConfig.VCD.Org, + "Vdc": testConfig.VCD.Vdc, + "EdgeGateway": testConfig.Networking.EdgeGateway, + "Catalog": testSuiteCatalogName, + "CatalogItem": testSuiteCatalogOVAItem, + "VAppName": netVappName, + "VMName": netVmName1, + "Tags": "vapp vm", + } + + configTextVM := templateFill(testAccCheckVcdVAppVmUpdateCustomizationSettings, params) + + params["FuncName"] = t.Name() + "-step1" + configTextVMStep1 := templateFill(testAccCheckVcdVAppVmUpdateCustomizationSettingsStep1, params) + + params["FuncName"] = t.Name() + "-step2" + configTextVMStep2 := templateFill(testAccCheckVcdVAppVmUpdateCustomizationSettingsStep2, params) + + if vcdShortTest { + t.Skip(acceptanceTestsSkipped) + return + } + + debugPrintf("#[DEBUG] CONFIGURATION: %s\n", configTextVM) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVcdVAppVmDestroy(netVappName), + Steps: []resource.TestStep{ + // Step 0 + resource.TestStep{ + Config: configTextVM, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.enabled", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.change_sid", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.allow_local_admin_password", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.must_change_password_on_first_login", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.number_of_auto_logons", "4"), + ), + }, + // Step 1 - join org domain must fail + resource.TestStep{ + Taint: []string{"vcd_vapp_vm.test-vm"}, + Config: configTextVMStep1, + // Our testing suite does not have Windows OS to actually try domain join so the point of this test is + // to prove that values are actually set and try to be applied on vCD. + // ExpectError: regexp.MustCompile(`Join Domain is not supported for OS type .*`), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.enabled", "false"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.admin_password", "some password"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_org_domain", "true"), + ), + }, + // Step 2 - join org domain enabled + resource.TestStep{ + Taint: []string{"vcd_vapp_vm.test-vm"}, + Config: configTextVMStep2, + // Our testing suite does not have Windows OS to actually try domain join so the point of this test is + // to prove that values are actually set and try to be applied on vCD. + ExpectError: regexp.MustCompile(`Join Domain is not supported for OS type .*`), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckVcdVAppVmExists(netVappName, netVmName1, "vcd_vapp_vm.test-vm", &vapp, &vm), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "name", netVmName1), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "network.#", "0"), + + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.#", "1"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.enabled", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain", "true"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain_name", "UnrealDomain"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain_user", "NoUser"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain_password", "NoPass"), + resource.TestCheckResourceAttr("vcd_vapp_vm.test-vm", "customization.0.join_domain_account_ou", "ou=IT,dc=some,dc=com"), + ), + }, + }, + }) +} + +const testAccCheckVcdVAppVmUpdateCustomizationSettings = testAccCheckVcdVAppVmCustomizationShared + ` resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -267,25 +420,44 @@ resource "vcd_vapp_vm" "test-vm" { cpus = 2 cpu_cores = 1 - network { - type = "vapp" - name = vcd_vapp_network.vappNet.name - ip_allocation_mode = "POOL" + customization { + enabled = true + change_sid = true + allow_local_admin_password = false + must_change_password_on_first_login = true + auto_generate_password = true + number_of_auto_logons = 4 } +} +` - network { - type = "none" - ip_allocation_mode = "NONE" - } +const testAccCheckVcdVAppVmUpdateCustomizationSettingsStep1 = testAccCheckVcdVAppVmCustomizationShared + ` +# skip-binary-test: it will fail on purpose +resource "vcd_vapp_vm" "test-vm" { + org = "{{.Org}}" + vdc = "{{.Vdc}}" + + vapp_name = vcd_vapp.test-vapp.name + name = "{{.VMName}}" + catalog_name = "{{.Catalog}}" + template_name = "{{.CatalogItem}}" + memory = 512 + cpus = 2 + cpu_cores = 1 customization { - force = true + enabled = false + admin_password = "some password" + auto_generate_password = false + join_domain = true + join_org_domain = true } } ` -const testAccCheckVcdVAppVmCreateCustomization = testAccCheckVcdVAppVmCustomizationShared + ` -resource "vcd_vapp_vm" "test-vm2" { +const testAccCheckVcdVAppVmUpdateCustomizationSettingsStep2 = testAccCheckVcdVAppVmCustomizationShared + ` +# skip-binary-test: it will fail on purpose +resource "vcd_vapp_vm" "test-vm" { org = "{{.Org}}" vdc = "{{.Vdc}}" @@ -297,14 +469,13 @@ resource "vcd_vapp_vm" "test-vm2" { cpus = 2 cpu_cores = 1 - network { - type = "vapp" - name = vcd_vapp_network.vappNet.name - ip_allocation_mode = "POOL" - } - customization { - force = true + enabled = true + join_domain = true + join_domain_name = "UnrealDomain" + join_domain_user = "NoUser" + join_domain_password = "NoPass" + join_domain_account_ou = "ou=IT,dc=some,dc=com" } } ` diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go index 6ecb29dc1..b509ae5d5 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/api.go @@ -560,3 +560,7 @@ func combinedTaskErrorMessage(task *types.Task, err error) string { } return extendedError } + +func takeBoolPointer(value bool) *bool { + return &value +} diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go index cbba929ab..2bc99972b 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vapp.go @@ -374,6 +374,9 @@ func (vapp *VApp) RunCustomizationScript(computername, script string) (Task, err return vapp.Customize(computername, script, false) } +// Customize applies customization to first child VM +// +// Deprecated: Use vm.SetGuestCustomizationSection() func (vapp *VApp) Customize(computername, script string, changeSid bool) (Task, error) { err := vapp.Refresh() if err != nil { @@ -393,10 +396,10 @@ func (vapp *VApp) Customize(computername, script string, changeSid bool) (Task, HREF: vapp.VApp.Children.VM[0].HREF, Type: types.MimeGuestCustomizationSection, Info: "Specifies Guest OS Customization Settings", - Enabled: true, + Enabled: takeBoolPointer(true), ComputerName: computername, CustomizationScript: script, - ChangeSid: false, + ChangeSid: takeBoolPointer(changeSid), } apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go index 990823e44..106d65eac 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/govcd/vm.go @@ -445,6 +445,9 @@ func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutA } } +// Customize function allows to set ComputerName, apply customization script and enable or disable the changeSid option +// +// Deprecated: Use vm.SetGuestCustomizationSection() func (vm *VM) Customize(computername, script string, changeSid bool) (Task, error) { err := vm.Refresh() if err != nil { @@ -459,10 +462,10 @@ func (vm *VM) Customize(computername, script string, changeSid bool) (Task, erro HREF: vm.VM.HREF, Type: types.MimeGuestCustomizationSection, Info: "Specifies Guest OS Customization Settings", - Enabled: true, + Enabled: takeBoolPointer(true), ComputerName: computername, CustomizationScript: script, - ChangeSid: false, + ChangeSid: takeBoolPointer(changeSid), } apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) diff --git a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go index f5b9c6cea..6b8c28e3c 100644 --- a/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go +++ b/vendor/github.com/vmware/go-vcloud-director/v2/types/v56/types.go @@ -1368,6 +1368,10 @@ type VM struct { VmSpecSection *VmSpecSection `xml:"VmSpecSection,omitempty"` + // GuestCustomizationSection contains settings for VM customization like admin password, SID + // changes, domain join configuration, etc + GuestCustomizationSection *GuestCustomizationSection `xml:"GuestCustomizationSection,omitempty"` + VMCapabilities *VMCapabilities `xml:"VmCapabilities,omitempty"` // Allows you to specify certain capabilities of this virtual machine. StorageProfile *Reference `xml:"StorageProfile,omitempty"` // A reference to a storage profile to be used for this object. The specified storage profile must exist in the organization vDC that contains the object. If not specified, the default storage profile for the vDC is used. ProductSection *ProductSection `xml:"ProductSection,omitempty"` @@ -1603,21 +1607,21 @@ type GuestCustomizationSection struct { // FIXME: Fix the OVF section Info string `xml:"ovf:Info"` // Elements - Enabled bool `xml:"Enabled,omitempty"` // True if guest customization is enabled. - ChangeSid bool `xml:"ChangeSid,omitempty"` // True if customization can change the Windows SID of this virtual machine. + Enabled *bool `xml:"Enabled,omitempty"` // True if guest customization is enabled. + ChangeSid *bool `xml:"ChangeSid,omitempty"` // True if customization can change the Windows SID of this virtual machine. VirtualMachineID string `xml:"VirtualMachineId,omitempty"` // Virtual machine ID to apply. - JoinDomainEnabled bool `xml:"JoinDomainEnabled,omitempty"` // True if this virtual machine can join a Windows Domain. - UseOrgSettings bool `xml:"UseOrgSettings,omitempty"` // True if customization should use organization settings (OrgGuestPersonalizationSettings) when joining a Windows Domain. + JoinDomainEnabled *bool `xml:"JoinDomainEnabled,omitempty"` // True if this virtual machine can join a Windows Domain. + UseOrgSettings *bool `xml:"UseOrgSettings,omitempty"` // True if customization should use organization settings (OrgGuestPersonalizationSettings) when joining a Windows Domain. DomainName string `xml:"DomainName,omitempty"` // The name of the Windows Domain to join. DomainUserName string `xml:"DomainUserName,omitempty"` // User name to specify when joining a Windows Domain. DomainUserPassword string `xml:"DomainUserPassword,omitempty"` // Password to use with DomainUserName. MachineObjectOU string `xml:"MachineObjectOU,omitempty"` // The name of the Windows Domain Organizational Unit (OU) in which the computer account for this virtual machine will be created. - AdminPasswordEnabled bool `xml:"AdminPasswordEnabled,omitempty"` // True if guest customization can modify administrator password settings for this virtual machine. - AdminPasswordAuto bool `xml:"AdminPasswordAuto,omitempty"` // True if the administrator password for this virtual machine should be automatically generated. + AdminPasswordEnabled *bool `xml:"AdminPasswordEnabled,omitempty"` // True if guest customization can modify administrator password settings for this virtual machine. + AdminPasswordAuto *bool `xml:"AdminPasswordAuto,omitempty"` // True if the administrator password for this virtual machine should be automatically generated. AdminPassword string `xml:"AdminPassword,omitempty"` // True if the administrator password for this virtual machine should be set to this string. (AdminPasswordAuto must be false.) - AdminAutoLogonEnabled bool `xml:"AdminAutoLogonEnabled,omitempty"` // True if guest administrator should automatically log into this virtual machine. + AdminAutoLogonEnabled *bool `xml:"AdminAutoLogonEnabled,omitempty"` // True if guest administrator should automatically log into this virtual machine. AdminAutoLogonCount int `xml:"AdminAutoLogonCount,omitempty"` // Number of times administrator can automatically log into this virtual machine. In case AdminAutoLogon is set to True, this value should be between 1 and 100. Otherwise, it should be 0. - ResetPasswordRequired bool `xml:"ResetPasswordRequired,omitempty"` // True if the administrator password for this virtual machine must be reset after first use. + ResetPasswordRequired *bool `xml:"ResetPasswordRequired,omitempty"` // True if the administrator password for this virtual machine must be reset after first use. CustomizationScript string `xml:"CustomizationScript,omitempty"` // Script to run on guest customization. The entire script must appear in this element. Use the XML entity to represent a newline. Unicode characters can be represented in the form &#xxxx; where xxxx is the character number. ComputerName string `xml:"ComputerName,omitempty"` // Computer name to assign to this virtual machine. Link LinkList `xml:"Link,omitempty"` // A link to an operation on this section. diff --git a/vendor/modules.txt b/vendor/modules.txt index ac339ddf9..bdf8aa9f1 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -215,7 +215,7 @@ github.com/ulikunitz/xz/lzma # github.com/vmihailenco/msgpack v4.0.1+incompatible github.com/vmihailenco/msgpack github.com/vmihailenco/msgpack/codes -# github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.1 +# github.com/vmware/go-vcloud-director/v2 v2.6.0-beta.2 github.com/vmware/go-vcloud-director/v2/govcd github.com/vmware/go-vcloud-director/v2/types/v56 github.com/vmware/go-vcloud-director/v2/util diff --git a/website/docs/r/vapp_vm.html.markdown b/website/docs/r/vapp_vm.html.markdown index fac04c98d..383921747 100644 --- a/website/docs/r/vapp_vm.html.markdown +++ b/website/docs/r/vapp_vm.html.markdown @@ -203,7 +203,9 @@ The following arguments are supported: * `cpus` - (Optional) The number of virtual CPUs to allocate to the VM. Socket count is a result of: virtual logical processors/cores per socket. The default is 1 * `cpu_cores` - (Optional; *v2.1+*) The number of cores per socket. The default is 1 * `metadata` - (Optional; *v2.2+*) Key value map of metadata to assign to this VM -* `initscript` (Optional) Script to run on initial boot or with customization.force=true set +* `initscript` (Optional **Deprecated** by `customization.0.initscript`) Script to run on initial boot or with +customization.force=true set. See [Customization](#customization-block) to read more about Guest customization and other +options. * `storage_profile` (Optional; *v2.6+*) Storage profile to override the default one * `network_name` - (Optional; **Deprecated** by `network`) Name of the network this VM should connect to. * `vapp_network_name` - (Optional; v2.1+; **Deprecated** by `network`) Name of the vApp network this VM should connect to. @@ -220,7 +222,7 @@ guest operating system so that applications that require hardware virtualization translation or paravirtualization. Useful for hypervisor nesting provided underlying hardware supports it. Default is `false`. * `network` - (Optional; *v2.2+*) A block to define network interface. Multiple can be used. See [Network](#network-block) and example for usage details. **Deprecates**: `network_name`, `ip`, `vapp_network_name`. -* `customization` - (Optional; *v2.5+*) A block to define for guest customization options. See [Customization](#customization) +* `customization` - (Optional; *v2.5+*) A block to define for guest customization options. See [Customization](#customization-block) * `guest_properties` - (Optional; *v2.5+*) Key value map of guest properties * `description` - (Computed; *v2.6+*) The VM description. Note: description is read only. Currently, this field has the description of the OVA used to create the VM @@ -307,16 +309,46 @@ Changes are ignored on update. This part isn't reread on refresh. To manage inte * `storage_profile` - (Optional) Storage profile which overrides the VM default one. - + ## Customization +When you customize your guest OS you can set up a virtual machine with the operating system that you want. + +vCloud Director can customize the network settings of the guest operating system of a virtual machine created from a +vApp template. When you customize your guest operating system, you can create and deploy multiple unique virtual +machines based on the same vApp template without machine name or network conflicts. + +When you configure a vApp template with the prerequisites for guest customization and add a virtual machine to a vApp +based on that template, vCloud Director creates a package with guest customization tools. When you deploy and power on +the virtual machine for the first time, vCloud Director copies the package, runs the tools, and deletes the package from +the virtual machine. + +~> **Note:** The settings below work so that all values are inherited from template and only the specified fields are +overridden with exception being `force` field which works like a flag. + * `force` (Optional) **Warning.** `true` value will cause the VM to reboot on every `apply` operation. This field works as a flag and triggers force customization when `true` during an update (`terraform apply`) every time. It never complains about a change in statefile. Can be used when guest customization is needed after VM configuration (e.g. NIC change, customization options change, etc.) and then set back to `false`. **Note.** It will not have effect when `power_on` field is set to `false`. See [example workflow below](#example-forced-customization-workflow). - -## Example forced customization workflow +* `enabled` (Optional; *v2.7+*) `true` will enable guest customization which may occur on first boot or if the `force` flag is used. +This option should be selected for **Power on and Force re-customization to work**. For backwards compatibility it is +enabled by default when deprecated field `initscript` is used. +* `change_sid` (Optional; *v2.7+*) Allows to change SID (security identifier). Only applicable for Windows operating systems. +* `allow_local_admin_password` (Optional; *v2.7+*) Allow local administrator password. +* `must_change_password_on_first_login` (Optional; *v2.7+*) Require Administrator to change password on first login. +* `auto_generate_password` (Optional; *v2.7+*) Auto generate password. +* `admin_password` (Optional; *v2.7+*) Manually specify Administrator password. +* `number_of_auto_logons` (Optional; *v2.7+*) Number of times to log on automatically. `0` means disabled. +* `join_domain` (Optional; *v2.7+*) Enable this VM to join a domain. +* `join_org_domain` (Optional; *v2.7+*) Set to `true` to use organization's domain. +* `join_domain_name` (Optional; *v2.7+*) Set the domain name to override organization's domain name. +* `join_domain_user` (Optional; *v2.7+*) User to be used for domain join. +* `join_domain_password` (Optional; *v2.7+*) Password to be used for domain join. +* `join_domain_account_ou` (Optional; *v2.7+*) Organizational unit to be used for domain join. +* `initscript` (Optional; *v2.7+*) Provide initscript to be executed when customization is applied. + +## Example of a Forced Customization Workflow Step 1 - Setup VM: @@ -325,7 +357,7 @@ resource "vcd_vapp_vm" "web2" { vapp_name = "${vcd_vapp.web.name}" name = "web2" catalog_name = "Boxes" - template_name = "lampstack-1.10.1-ubuntu-10.04" + template_name = "windows" memory = 2048 cpus = 1 @@ -338,12 +370,12 @@ resource "vcd_vapp_vm" "web2" { } ``` -Step 2 - Change VM configuration and force customization (VM will be rebooted during -`terraform apply`): +Step 2 - Override some VM customization options and force customization (VM will be rebooted during `terraform apply`): ```hcl resource "vcd_vapp_vm" "web2" { -//... + #... + network { type = "org" name = "net" @@ -351,17 +383,23 @@ resource "vcd_vapp_vm" "web2" { } customization { - force = true + force = true + change_sid = true + allow_local_admin_password = true + auto_generate_password = false + admin_password = "my-secure-password" + # Other customization options to override the ones from template } } ``` -Step 3 - Once customization is done, set the force customization flag to false (or remove it) to -prevent forcing customization on every `terraform apply` command: +Step 3 - Once customization is done, set the force customization flag to false (or remove it) to prevent forcing +customization on every `terraform apply` command: ```hcl resource "vcd_vapp_vm" "web2" { -//... + #... + network { type = "org" name = "net" @@ -369,7 +407,12 @@ resource "vcd_vapp_vm" "web2" { } customization { - force = false + force = false + change_sid = true + allow_local_admin_password = true + auto_generate_password = false + admin_password = "my-secure-password" + # Other customization options to override the ones from template } } ```