Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow VM resource's power_state to be managed #278

Merged
4 changes: 3 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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{
Expand Down
96 changes: 62 additions & 34 deletions client/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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})

Expand All @@ -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()
Expand All @@ -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
}

Expand Down
3 changes: 2 additions & 1 deletion xoa/internal/state/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand Down
82 changes: 69 additions & 13 deletions xoa/resource_xenorchestra_vm.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package xoa

import (
"context"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -34,6 +35,13 @@ var validFirmware = []string{
"uefi",
}

var validPowerState = []string{
client.HaltedPowerState,
client.PausedPowerState,
client.RunningPowerState,
client.SuspendedPowerState,
}

var validInstallationMethods = []string{
"network",
}
Expand All @@ -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{

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
},
Expand Down Expand Up @@ -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") {
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading