Skip to content

Commit ccbefce

Browse files
Merge pull request #346 from mkumatag/fix/issue-300-cleanup-timeout
Fix: Add configurable timeout for VM deletion cleanup (#300)
2 parents dde1e2a + a87f4b0 commit ccbefce

File tree

5 files changed

+115
-15
lines changed

5 files changed

+115
-15
lines changed

builder/powervs/builder.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package powervs
55

66
import (
77
"context"
8+
"time"
89

910
"github.com/hashicorp/hcl/v2/hcldec"
1011
"github.com/hashicorp/packer-plugin-sdk/common"
@@ -94,6 +95,12 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
9495

9596
var steps []multistep.Step
9697

98+
// Parse cleanup timeout
99+
cleanupTimeout, _ := time.ParseDuration(b.config.CleanupTimeout)
100+
if cleanupTimeout == 0 {
101+
cleanupTimeout = 10 * time.Minute // Default
102+
}
103+
97104
steps = append(steps,
98105
&StepImageBaseImage{
99106
Source: b.config.Source,
@@ -103,9 +110,10 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
103110
DHCPNetwork: b.config.DHCPNetwork,
104111
},
105112
&StepCreateInstance{
106-
InstanceName: b.config.InstanceName,
107-
KeyPairName: b.config.KeyPairName,
108-
UserData: b.config.UserData,
113+
InstanceName: b.config.InstanceName,
114+
KeyPairName: b.config.KeyPairName,
115+
UserData: b.config.UserData,
116+
CleanupTimeout: cleanupTimeout,
109117
},
110118
&communicator.StepConnect{
111119
Config: &b.config.RunConfig.Comm,

builder/powervs/builder.hcl2spec.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/powervs/common/run_config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
package common
55

66
import (
7+
"fmt"
8+
"time"
9+
710
"github.com/hashicorp/packer-plugin-sdk/communicator"
811
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
912
)
@@ -48,12 +51,29 @@ type RunConfig struct {
4851
Source Source `mapstructure:"source" required:"true"`
4952
Capture Capture `mapstructure:"capture" required:"true"`
5053

54+
// CleanupTimeout specifies the maximum time to wait for instance deletion during cleanup.
55+
// If the instance is not deleted within this time, cleanup will fail gracefully with a warning.
56+
// Format: duration string (e.g., "10m", "15m30s")
57+
// Default: 10 minutes
58+
CleanupTimeout string `mapstructure:"cleanup_timeout" required:"false"`
59+
5160
// Communicator settings
5261
Comm communicator.Config `mapstructure:",squash"`
5362
}
5463

5564
func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
5665
// Validation
5766
errs := c.Comm.Prepare(ctx)
67+
68+
// Set default cleanup timeout if not specified
69+
if c.CleanupTimeout == "" {
70+
c.CleanupTimeout = "10m"
71+
}
72+
73+
// Validate cleanup timeout format
74+
if _, err := time.ParseDuration(c.CleanupTimeout); err != nil {
75+
errs = append(errs, fmt.Errorf("invalid cleanup_timeout format: %s (use format like '10m', '15m30s')", c.CleanupTimeout))
76+
}
77+
5878
return errs
5979
}

builder/powervs/step_create_instance.go

Lines changed: 75 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,18 @@ import (
1414
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
1515
)
1616

17+
const (
18+
// DefaultCleanupTimeout is the default maximum time to wait for instance deletion
19+
DefaultCleanupTimeout = 10 * time.Minute
20+
// CleanupPollInterval is how often to check instance deletion status
21+
CleanupPollInterval = 10 * time.Second
22+
)
23+
1724
type StepCreateInstance struct {
18-
InstanceName string
19-
KeyPairName string
20-
UserData string
25+
InstanceName string
26+
KeyPairName string
27+
UserData string
28+
CleanupTimeout time.Duration
2129

2230
doCleanup bool
2331
}
@@ -114,20 +122,75 @@ func (s *StepCreateInstance) Cleanup(state multistep.StateBag) {
114122
ui.Say("Deleting the Instance")
115123
instanceClient := state.Get("instanceClient").(*instance.IBMPIInstanceClient)
116124
i := state.Get("instance").(*models.PVMInstance)
125+
126+
// Initiate deletion
117127
err := instanceClient.Delete(*i.PvmInstanceID)
118128
if err != nil {
119129
ui.Error(fmt.Sprintf(
120-
"Error cleaning up instance. Please delete the instance manually: %s", *i.ServerName))
130+
"Error initiating instance deletion. Please delete the instance manually: %s (ID: %s)",
131+
*i.ServerName, *i.PvmInstanceID))
132+
ui.Error(fmt.Sprintf("Deletion error: %v", err))
133+
return
134+
}
135+
136+
// Wait for deletion with timeout
137+
timeout := s.CleanupTimeout
138+
if timeout == 0 {
139+
timeout = DefaultCleanupTimeout
121140
}
122-
for {
141+
142+
begin := time.Now()
143+
errorStateCount := 0
144+
maxErrorStateRetries := 5 // Give some retries even in ERROR state
145+
146+
//nolint:staticcheck // SA1015 this disable staticcheck for the next line
147+
err = pollUntil(time.Tick(CleanupPollInterval), time.After(timeout), func() (bool, error) {
123148
in, err := instanceClient.Get(*i.PvmInstanceID)
124-
if err == nil {
125-
ui.Message(fmt.Sprintf("VM still exists, state: %s", *in.Status))
126-
time.Sleep(10 * time.Second)
127-
continue
128-
} else {
129-
ui.Message("instance deleted successfully")
130-
break
149+
150+
// Instance not found means it was successfully deleted
151+
if err != nil {
152+
ui.Message("Instance deleted successfully")
153+
return true, nil
154+
}
155+
156+
// Instance still exists, check its state
157+
currentState := *in.Status
158+
elapsed := time.Since(begin).Round(time.Second)
159+
ui.Message(fmt.Sprintf("VM still exists, state: %s (elapsed: %s)", currentState, elapsed))
160+
161+
// If instance is in ERROR state, warn but continue for a few retries
162+
// Sometimes instances in ERROR state can still be deleted
163+
if currentState == "ERROR" {
164+
errorStateCount++
165+
if errorStateCount == 1 {
166+
ui.Message("Warning: Instance entered ERROR state during deletion")
167+
ui.Message("Continuing to wait for deletion (may require manual cleanup)")
168+
}
169+
if errorStateCount >= maxErrorStateRetries {
170+
ui.Error(fmt.Sprintf(
171+
"Instance has been in ERROR state for %d checks. Deletion may have failed.",
172+
errorStateCount))
173+
ui.Error(fmt.Sprintf(
174+
"Please verify instance status manually: %s (ID: %s)",
175+
*i.ServerName, *i.PvmInstanceID))
176+
// Don't return error, let timeout handle it
177+
}
178+
}
179+
180+
return false, nil
181+
})
182+
183+
if err != nil {
184+
// Timeout occurred
185+
ui.Error(fmt.Sprintf(
186+
"Timed out waiting for instance deletion after %s", timeout))
187+
ui.Error(fmt.Sprintf(
188+
"Instance may need manual cleanup: %s (ID: %s)",
189+
*i.ServerName, *i.PvmInstanceID))
190+
191+
// Try to get final state
192+
if finalInstance, getErr := instanceClient.Get(*i.PvmInstanceID); getErr == nil {
193+
ui.Error(fmt.Sprintf("Final instance state: %s", *finalInstance.Status))
131194
}
132195
}
133196
}

docs-partials/builder/powervs/common/RunConfig-not-required.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
- `subnet_ids` ([]string) - Subnet I Ds
44

5+
- `user_data` (string) - User Data
6+
57
- `dhcp_network` (bool) - DHCP Network
68

9+
- `cleanup_timeout` (string) - CleanupTimeout specifies the maximum time to wait for instance deletion during cleanup.
10+
If the instance is not deleted within this time, cleanup will fail gracefully with a warning.
11+
Format: duration string (e.g., "10m", "15m30s")
12+
Default: 10 minutes
13+
714
<!-- End of code generated from the comments of the RunConfig struct in builder/powervs/common/run_config.go; -->

0 commit comments

Comments
 (0)