diff --git a/client/client.go b/client/client.go index 442e5015..36af7e40 100644 --- a/client/client.go +++ b/client/client.go @@ -38,6 +38,8 @@ type XOClient interface { DeleteVm(id string) error HaltVm(id string) error StartVm(id string) error + SuspendVm(id string) error + PauseVm(id string) error GetCloudConfigByName(name string) ([]CloudConfig, error) CreateCloudConfig(name, template string) (*CloudConfig, error) @@ -190,7 +192,7 @@ func GetConfigFromEnv() Config { if err == nil { retryMaxTime = duration } else { - fmt.Println("[ERROR] failed to set retry mode, disabling retries\n") + fmt.Println("[ERROR] failed to set retry mode, disabling retries") } } return Config{ diff --git a/client/vm.go b/client/vm.go index 131b7e7f..17c75223 100644 --- a/client/vm.go +++ b/client/vm.go @@ -17,6 +17,13 @@ type allObjectResponse struct { Objects map[string]Vm `json:"-"` } +const ( + HaltedPowerState string = "Halted" + PausedPowerState string = "Paused" + RunningPowerState string = "Running" + SuspendedPowerState string = "Suspended" +) + type CPUs struct { Number int `json:"number"` Max int `json:"max"` @@ -175,6 +182,42 @@ func (v Vm) Compare(obj interface{}) bool { return false } +func (c *Client) SuspendVm(id string) error { + return c.changeVmState(id, "suspend", []string{SuspendedPowerState}, []string{RunningPowerState}, 2*time.Minute) +} + +func (c *Client) changeVmState(id, action string, target, pending []string, timeout time.Duration) error { + // PV drivers are necessary for the XO api to issue a graceful shutdown. + // See https://github.com/terra-farm/terraform-provider-xenorchestra/issues/220 + // for more details. + if err := c.waitForPVDriversDetected(id); err != nil { + return errors.New( + fmt.Sprintf("failed to gracefully %s vm (%s) since PV drivers were never detected", action, id)) + } + + params := map[string]interface{}{ + "id": id, + } + var success bool + err := c.Call(fmt.Sprintf("vm.%s", action), params, &success) + + if err != nil { + return err + } + return c.waitForVmState( + id, + StateChangeConf{ + Pending: pending, + Target: target, + Timeout: timeout, + }, + ) +} + +func (c *Client) PauseVm(id string) error { + return c.changeVmState(id, "pause", []string{PausedPowerState}, []string{RunningPowerState}, 2*time.Minute) +} + func (c *Client) CreateVm(vmReq Vm, createTime time.Duration) (*Vm, error) { tmpl, err := c.GetTemplate(Template{ Id: vmReq.Template, @@ -304,14 +347,14 @@ func (c *Client) CreateVm(vmReq Vm, createTime time.Duration) (*Vm, error) { } bootAfterCreate := params["bootAfterCreate"].(bool) - if !bootAfterCreate { + if !bootAfterCreate && vmReq.PowerState == RunningPowerState { err = c.StartVm(vmId) if err != nil { return nil, err } } - err = c.waitForModifyVm(vmId, vmReq.WaitForIps, createTime) + err = c.waitForModifyVm(vmId, vmReq.PowerState, vmReq.WaitForIps, createTime) if err != nil { return nil, err @@ -434,40 +477,15 @@ func (c *Client) StartVm(id string) error { return c.waitForVmState( id, StateChangeConf{ - Pending: []string{"Halted", "Stopped"}, - Target: []string{"Running"}, + Pending: []string{HaltedPowerState}, + Target: []string{RunningPowerState}, Timeout: 2 * time.Minute, }, ) } func (c *Client) HaltVm(id string) error { - // PV drivers are necessary for the XO api to issue a graceful shutdown. - // See https://github.com/terra-farm/terraform-provider-xenorchestra/issues/220 - // for more details. - if err := c.waitForPVDriversDetected(id); err != nil { - return errors.New( - fmt.Sprintf("failed to gracefully halt vm (%s) since PV drivers were never detected", id)) - } - - params := map[string]interface{}{ - "id": id, - } - var success bool - // TODO: This can block indefinitely before we get to the waitForVmHalt - err := c.Call("vm.stop", params, &success) - - if err != nil { - return err - } - return c.waitForVmState( - id, - StateChangeConf{ - Pending: []string{"Running", "Stopped"}, - Target: []string{"Halted"}, - Timeout: 2 * time.Minute, - }, - ) + return c.changeVmState(id, "stop", []string{HaltedPowerState}, []string{RunningPowerState}, 2*time.Minute) } func (c *Client) DeleteVm(id string) error { @@ -558,8 +576,18 @@ func (c *Client) waitForVmState(id string, stateConf StateChangeConf) error { return err } -func (c *Client) waitForModifyVm(id string, waitForIp bool, timeout time.Duration) error { +func (c *Client) waitForModifyVm(id string, desiredPowerState string, waitForIp bool, timeout time.Duration) error { if !waitForIp { + var pending []string + target := desiredPowerState + switch desiredPowerState { + case RunningPowerState: + pending = []string{HaltedPowerState} + case HaltedPowerState: + pending = []string{RunningPowerState} + default: + return errors.New(fmt.Sprintf("Invalid VM power state requested: %s\n", desiredPowerState)) + } refreshFn := func() (result interface{}, state string, err error) { vm, err := c.GetVm(Vm{Id: id}) @@ -570,9 +598,9 @@ func (c *Client) waitForModifyVm(id string, waitForIp bool, timeout time.Duratio return vm, vm.PowerState, nil } stateConf := &StateChangeConf{ - Pending: []string{"Halted", "Stopped"}, + Pending: pending, Refresh: refreshFn, - Target: []string{"Running"}, + Target: []string{target}, Timeout: timeout, } _, err := stateConf.WaitForState() @@ -586,7 +614,7 @@ func (c *Client) waitForModifyVm(id string, waitForIp bool, timeout time.Duratio } l := len(vm.Addresses) - if l == 0 || vm.PowerState != "Running" { + if l == 0 || vm.PowerState != RunningPowerState { return vm, "Waiting", nil } diff --git a/xoa/internal/state/migrate.go b/xoa/internal/state/migrate.go index 54a0ece2..9a14d33d 100644 --- a/xoa/internal/state/migrate.go +++ b/xoa/internal/state/migrate.go @@ -6,6 +6,7 @@ import ( "log" "net" + "github.com/ddelnano/terraform-provider-xenorchestra/client" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -355,7 +356,7 @@ func suppressAttachedDiffWhenHalted(k, old, new string, d *schema.ResourceData) powerState := d.Get("power_state").(string) suppress = true - if powerState == "Running" { + if powerState == client.RunningPowerState { suppress = false } log.Printf("[DEBUG] VM '%s' attribute has transitioned from '%s' to '%s' when PowerState '%s'. Suppress diff: %t", k, old, new, powerState, suppress) diff --git a/xoa/resource_xenorchestra_vm.go b/xoa/resource_xenorchestra_vm.go index c52e0b55..9bed9035 100644 --- a/xoa/resource_xenorchestra_vm.go +++ b/xoa/resource_xenorchestra_vm.go @@ -1,6 +1,7 @@ package xoa import ( + "context" "errors" "fmt" "log" @@ -34,6 +35,13 @@ var validFirmware = []string{ "uefi", } +var validPowerState = []string{ + client.HaltedPowerState, + client.PausedPowerState, + client.RunningPowerState, + client.SuspendedPowerState, +} + var validInstallationMethods = []string{ "network", } @@ -56,6 +64,16 @@ func resourceVm() *schema.Resource { } } +func vmDestroyCloudConfigCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { + destroyCloudConfig := diff.Get("destroy_cloud_config_vdi_after_boot").(bool) + powerState := diff.Get("power_state").(string) + + if destroyCloudConfig && powerState != client.RunningPowerState { + return errors.New(fmt.Sprintf("power_state must be `%s` when destroy_cloud_config_vdi_after_boot set to `true`", client.RunningPowerState)) + } + return nil +} + func resourceVmSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ @@ -107,9 +125,11 @@ func resourceVmSchema() map[string]*schema.Schema { ValidateFunc: validation.StringInSlice(validFirmware, false), }, "power_state": &schema.Schema{ - Description: "The power state of the VM. This can be Running, Halted, Paused or Suspended.", - Type: schema.TypeString, - Computed: true, + Description: "The power state of the VM. This can be Running, Halted, Paused or Suspended.", + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(validPowerState, false), + Optional: true, + Default: client.RunningPowerState, }, "installation_method": &schema.Schema{ Type: schema.TypeString, @@ -144,7 +164,7 @@ func resourceVmSchema() map[string]*schema.Schema { "cloud_config", }, ForceNew: true, - Description: "Determines whether the cloud config VDI should be deleted once the VM has booted. Defaults to `false`.", + Description: "Determines whether the cloud config VDI should be deleted once the VM has booted. Defaults to `false`. If set to `true`, power_state must be set to `Running`.", }, "core_os": &schema.Schema{ Type: schema.TypeBool, @@ -379,6 +399,7 @@ $ xo-cli xo.getAllObjects filter='json:{"id": "cf7b5d7d-3cd5-6b7c-5025-5c935c8cd func resourceRecord() *schema.Resource { duration := 5 * time.Minute return &schema.Resource{ + CustomizeDiff: vmDestroyCloudConfigCustomizeDiff, Description: "Creates a Xen Orchestra vm resource.", Create: resourceVmCreate, Read: resourceVmRead, @@ -495,6 +516,7 @@ func resourceVmCreate(d *schema.ResourceData, m interface{}) error { ResourceSet: rs, HA: d.Get("high_availability").(string), AutoPoweron: d.Get("auto_poweron").(bool), + PowerState: d.Get("power_state").(string), CPUs: client.CPUs{ Number: d.Get("cpus").(int), }, @@ -876,27 +898,57 @@ func resourceVmUpdate(d *schema.ResourceData, m interface{}) error { vmReq.AffinityHost = &affinityHost } + haltPerformed := false + if haltForUpdates { err := c.HaltVm(id) if err != nil { return err } + haltPerformed = true } vm, err = c.UpdateVm(vmReq) - if haltForUpdates { - err := c.StartVm(vmReq.Id) - - if err != nil { - return err - } + if err != nil { + return err } log.Printf("[DEBUG] Retrieved vm after update: %+v\n", vm) - if err != nil { - return err + powerStateChanged := d.HasChange("power_state") + _, newPowerState := d.GetChange("power_state") + log.Printf("[DEBUG] powerStateChanged=%t newPowerState=%s\n", powerStateChanged, newPowerState) + if haltForUpdates || powerStateChanged { + switch newPowerState { + case client.PausedPowerState: + err := c.PauseVm(vmReq.Id) + + if err != nil { + return err + } + case client.SuspendedPowerState: + err := c.SuspendVm(vmReq.Id) + + if err != nil { + return err + } + case client.RunningPowerState: + err := c.StartVm(vmReq.Id) + + if err != nil { + return err + } + case client.HaltedPowerState: + // If the VM wasn't halted as part of the update, perform the halt now + if !haltPerformed { + err := c.HaltVm(id) + + if err != nil { + return err + } + } + } } if d.HasChange("tags") { @@ -1340,8 +1392,12 @@ func extractIpsFromNetworks(networks map[string]string) []guestNetwork { func suppressAttachedDiffWhenHalted(k, old, new string, d *schema.ResourceData) (suppress bool) { powerState := d.Get("power_state").(string) suppress = true + ok := d.HasChange("power_state") + if ok { + log.Printf("[DEBUG] Power state has been changed\n") + } - if powerState == "Running" { + if !ok && powerState == client.RunningPowerState { suppress = false } log.Printf("[DEBUG] VM '%s' attribute has transitioned from '%s' to '%s' when PowerState '%s'. Suppress diff: %t", k, old, new, powerState, suppress) diff --git a/xoa/resource_xenorchestra_vm_test.go b/xoa/resource_xenorchestra_vm_test.go index 0f6d3886..de8f37e8 100644 --- a/xoa/resource_xenorchestra_vm_test.go +++ b/xoa/resource_xenorchestra_vm_test.go @@ -340,6 +340,123 @@ func TestAccXenorchestraVm_createWithShorterResourceTimeout(t *testing.T) { }) } +func TestAccXenorchestraVm_destroyCloudConfigRequiresRunningVm(t *testing.T) { + vmName := fmt.Sprintf("%s - %s", accTestPrefix, t.Name()) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckXenorchestraVmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName, client.HaltedPowerState), + ExpectError: regexp.MustCompile("power_state must be `Running` when destroy_cloud_config_vdi_after_boot set to `true`"), + PlanOnly: true, + }, + }, + }) +} + +func TestAccXenorchestraVm_createWithPowerStateChanges(t *testing.T) { + resourceName := "xenorchestra_vm.bar" + vmName := fmt.Sprintf("%s - %s", accTestPrefix, t.Name()) + runningPowerState := client.RunningPowerState + stoppedPowerState := client.HaltedPowerState + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckXenorchestraVmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVmConfigWithPowerState(vmName, stoppedPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", stoppedPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, runningPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", runningPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, stoppedPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", stoppedPowerState)), + }, + }, + }) +} + +func TestAccXenorchestraVm_createAndSuspend(t *testing.T) { + resourceName := "xenorchestra_vm.bar" + vmName := fmt.Sprintf("%s - %s", accTestPrefix, t.Name()) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckXenorchestraVmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVmConfigWithPowerState(vmName, client.RunningPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.RunningPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, client.SuspendedPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.SuspendedPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, client.RunningPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.RunningPowerState)), + }, + }, + }) +} + +func TestAccXenorchestraVm_createAndPause(t *testing.T) { + resourceName := "xenorchestra_vm.bar" + vmName := fmt.Sprintf("%s - %s", accTestPrefix, t.Name()) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckXenorchestraVmDestroy, + Steps: []resource.TestStep{ + { + Config: testAccVmConfigWithPowerState(vmName, client.RunningPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.RunningPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, client.PausedPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.PausedPowerState)), + }, + { + Config: testAccVmConfigWithPowerState(vmName, client.RunningPowerState), + Check: resource.ComposeAggregateTestCheckFunc( + testAccVmExists(resourceName), + resource.TestCheckResourceAttrSet(resourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "power_state", client.RunningPowerState)), + }, + }, + }) +} + func TestAccXenorchestraVm_createAndPlanWithNonExistantVm(t *testing.T) { resourceName := "xenorchestra_vm.bar" vmName := fmt.Sprintf("%s - %s", accTestPrefix, t.Name()) @@ -419,7 +536,7 @@ func TestAccXenorchestraVm_createWithDestroyCloudConfigDrive(t *testing.T) { CheckDestroy: testAccCheckXenorchestraVmDestroy, Steps: []resource.TestStep{ { - Config: testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName), + Config: testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName, client.RunningPowerState), Check: resource.ComposeAggregateTestCheckFunc( testAccVmExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "id"), @@ -428,7 +545,7 @@ func TestAccXenorchestraVm_createWithDestroyCloudConfigDrive(t *testing.T) { }, { PreConfig: verifyCloudConfigDiskDeleted, - Config: testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName), + Config: testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName, client.RunningPowerState), Check: resource.ComposeAggregateTestCheckFunc( testAccVmExists(resourceName), resource.TestCheckResourceAttrSet(resourceName, "id"), @@ -1465,7 +1582,7 @@ func TestAccXenorchestraVm_diskAndNetworkAttachmentIgnoredWhenHalted(t *testing. }, { PreConfig: shutdownVm, - Config: testAccVmConfig(vmName), + Config: testAccVmConfigWithPowerState(vmName, "Halted"), PlanOnly: true, Check: resource.ComposeAggregateTestCheckFunc( testAccVmExists(resourceName), @@ -1795,11 +1912,40 @@ resource "xenorchestra_vm" "bar" { `, accDefaultNetwork.NameLabel, accTestPool.Id, vmName, accDefaultSr.Id, waitForIp) } +func testAccVmConfigWithPowerState(vmName, powerState string) string { + return testAccCloudConfigConfig(fmt.Sprintf("vm-template-%s", vmName), "template") + testAccTemplateConfig() + fmt.Sprintf(` +data "xenorchestra_network" "network" { + name_label = "%s" + pool_id = "%s" +} + +resource "xenorchestra_vm" "bar" { + memory_max = 4295000000 + cpus = 1 + cloud_config = "${xenorchestra_cloud_config.bar.template}" + name_label = "%s" + name_description = "description" + template = "${data.xenorchestra_template.template.id}" + network { + network_id = "${data.xenorchestra_network.network.id}" + } + + disk { + sr_id = "%s" + name_label = "disk 1" + size = 10001317888 + } + + power_state = "%s" +} +`, accDefaultNetwork.NameLabel, accTestPool.Id, vmName, accDefaultSr.Id, powerState) +} + // This sets destroy_cloud_config_vdi_after_boot and wait_for_ip. The former is required for // the test expectations while the latter is to ensure the test holds its assertions until the // disk was actually deleted. The XO api uses the guest metrics to determine when it can remove // the disk, so an IP address allocation happens at the same time. -func testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName string) string { +func testAccVmConfigWithDestroyCloudConfigAfterBoot(vmName string, powerState string) string { return testAccCloudConfigConfig(fmt.Sprintf("vm-template-%s", vmName), "template") + testAccTemplateConfig() + fmt.Sprintf(` data "xenorchestra_network" "network" { name_label = "%s" @@ -1817,6 +1963,7 @@ resource "xenorchestra_vm" "bar" { network { network_id = "${data.xenorchestra_network.network.id}" } + power_state = "%s" wait_for_ip = true disk { @@ -1825,7 +1972,7 @@ resource "xenorchestra_vm" "bar" { size = 10001317888 } } -`, accDefaultNetwork.NameLabel, accTestPool.Id, vmName, accDefaultSr.Id) +`, accDefaultNetwork.NameLabel, accTestPool.Id, vmName, powerState, accDefaultSr.Id) } func testAccVmConfigPXEBoot(vmName string) string {