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

DE19227:Terraform replace of a host fails when the host is in Failed … #160

Merged
merged 7 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/acceptance_test/resource_host_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// (C) Copyright 2020-2023 Hewlett Packard Enterprise Development LP
// (C) Copyright 2020-2024 Hewlett Packard Enterprise Development LP

package acceptance_test

Expand All @@ -18,7 +18,7 @@ import (

const (
hostStateReadyWait = 30 * time.Second
hostStatePollCount = 4
hostStatePollCount = 10
isAsync = true
isNotAsync = false
)
Expand Down
80 changes: 46 additions & 34 deletions internal/resources/resource_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
package resources

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

rest "github.com/hewlettpackard/hpegl-metal-client/v1/pkg/client"
Expand Down Expand Up @@ -494,7 +495,7 @@ func resourceMetalHostCreate(d *schema.ResourceData, meta interface{}) (err erro
}

// host create is asynchronous in Metal svc. Wait until host state is Ready.
createStateConf := &resource.StateChangeConf{
createStateConf := &retry.StateChangeConf{
Pending: []string{
string(rest.HOSTSTATE_NEW),
string(rest.HOSTSTATE_IMAGING_PREP),
Expand Down Expand Up @@ -789,7 +790,7 @@ func resourceMetalHostUpdate(d *schema.ResourceData, meta interface{}) (err erro
}

// host update is asynchronous in Metal svc. Wait until host state is Ready.
updateStateConf := &resource.StateChangeConf{
updateStateConf := &retry.StateChangeConf{
Pending: []string{
string(rest.HOSTSTATE_UPDATING_CONNECTIONS),
string(rest.HOSTSTATE_CONNECTING),
Expand Down Expand Up @@ -826,7 +827,6 @@ func resourceMetalHostDelete(d *schema.ResourceData, meta interface{}) (err erro
if err != nil {
return err
}
var host rest.Host

defer func() {
// This is the last in the deferred chain to fire. If there has been no
Expand All @@ -839,7 +839,8 @@ func resourceMetalHostDelete(d *schema.ResourceData, meta interface{}) (err erro
}()

ctx := p.GetContext()
host, _, err = p.Client.HostsApi.GetByID(ctx, d.Id())

host, _, err := p.Client.HostsApi.GetByID(ctx, d.Id())
if err != nil {
return err
}
Expand All @@ -848,38 +849,14 @@ func resourceMetalHostDelete(d *schema.ResourceData, meta interface{}) (err erro
return nil
}

if host.State != rest.HOSTSTATE_READY {
// Hosts that are still prvisioning can be
// deleted immediately.
_, err = p.Client.HostsApi.Delete(ctx, d.Id())

return err
}

// Hosts that are powered-on can not be deleted directly, so flip the power.
if host.PowerStatus == rest.HOSTPOWERSTATE_ON {
ctx = p.GetContext()
_, _, err = p.Client.HostsApi.PowerOff(ctx, d.Id())
if err != nil {
// Hosts that are in the Ready state and powered-on can not be deleted while the
// power is on, so turn off the power.
if host.State == rest.HOSTSTATE_READY && host.PowerStatus == rest.HOSTPOWERSTATE_ON {
if err := powerOffHost(ctx, p.Client.HostsApi, d.Id(), d.Timeout(schema.TimeoutDefault)); err != nil {
return err
}
// The call is asynchronous so wait for Metal svc to complete the request.
for host.PowerStatus != rest.HOSTPOWERSTATE_OFF {
time.Sleep(pollInterval)

host, _, err = p.Client.HostsApi.GetByID(ctx, d.Id())
if err != nil {
return err
}

if host.State == rest.HOSTSTATE_FAILED {
return fmt.Errorf("failed to turn off host power")
}
}
}

ctx = p.GetContext()

if _, err := p.Client.HostsApi.Delete(ctx, d.Id()); err != nil {
//nolint:wrapcheck // defer func is wrapping the error.
return err
Expand All @@ -889,7 +866,7 @@ func resourceMetalHostDelete(d *schema.ResourceData, meta interface{}) (err erro
// reference to the host until it has really gone from Metal svc. If we delete the
// reference too early, or in the presence of errors, we will never be able to retry
// the delete operation from Terraform (since it has no reference to the resource).
deleteStateConf := &resource.StateChangeConf{
deleteStateConf := &retry.StateChangeConf{
Pending: []string{
string(rest.HOSTSTATE_DELETING),
},
Expand All @@ -916,6 +893,41 @@ func resourceMetalHostDelete(d *schema.ResourceData, meta interface{}) (err erro
return nil
}

func powerOffHost(ctx context.Context, hostAPI rest.HostsAPI, hostID string, timeout time.Duration) error {
_, _, err := hostAPI.PowerOff(ctx, hostID)
if err != nil {
return fmt.Errorf("power off host %v: %v", hostID, err)
}

// The power-off call is asynchronous so wait for Metal svc to complete the request.
powerOffStateConf := &retry.StateChangeConf{
Pending: []string{
string(rest.HOSTPOWERSTATE_UNKNOWN),
string(rest.HOSTPOWERSTATE_ON),
},
Target: []string{
string(rest.HOSTPOWERSTATE_OFF),
},
Refresh: func() (interface{}, string, error) {
host, _, err := hostAPI.GetByID(ctx, hostID)
if err != nil {
return nil, "", fmt.Errorf("get host %v", hostID)
}

return host, string(host.PowerStatus), nil
},
Timeout: timeout,
Delay: mediumTimeout,
MinTimeout: shortTimeout,
}

if _, err := powerOffStateConf.WaitForStateContext(ctx); err != nil {
return fmt.Errorf("waiting for host instance (%v) to be powered off: %v", hostID, err)
}

return nil
}

// volumeExists returns true & the volume ID, if the input matches
// either the ID or the name from existing volumes.
func volumeExists(vID string, volumes []rest.Volume) (string, bool) {
Expand Down
Loading