OpenStack builder: add StepDetachVolume

Add a step of detaching Compute instance volume if it's created using
the Block Storage service.
It is needed to create a final image.

This commit also moves common volume functions to a different file and
fixes some messages in the StepCreateVolume.
This commit is contained in:
Andrei Ozerov 2018-05-24 14:15:03 +03:00 committed by Rickard von Essen
parent 005f7e56a7
commit c0ffe7eb89
4 changed files with 126 additions and 64 deletions

View File

@ -132,6 +132,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}, },
&common.StepProvision{}, &common.StepProvision{},
&StepStopServer{}, &StepStopServer{},
&StepDetachVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&stepCreateImage{}, &stepCreateImage{},
&stepUpdateImageVisibility{}, &stepUpdateImageVisibility{},
&stepAddImageMembers{}, &stepAddImageMembers{},

View File

@ -3,12 +3,8 @@ package openstack
import ( import (
"context" "context"
"fmt" "fmt"
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes" "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
@ -63,7 +59,6 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
Name: s.VolumeName, Name: s.VolumeName,
ImageID: s.SourceImage, ImageID: s.SourceImage,
} }
volume, err := volumes.Create(blockStorageClient, volumeOpts).Extract() volume, err := volumes.Create(blockStorageClient, volumeOpts).Extract()
if err != nil { if err != nil {
err := fmt.Errorf("Error creating volume: %s", err) err := fmt.Errorf("Error creating volume: %s", err)
@ -72,8 +67,8 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionHalt return multistep.ActionHalt
} }
// Wait for the volume to become ready. // Wait for volume to become available.
ui.Say(fmt.Sprintf("Waiting for volume %s (volume id: %s) to become ready...", config.VolumeName, volume.ID)) ui.Say(fmt.Sprintf("Waiting for volume %s (volume id: %s) to become available...", config.VolumeName, volume.ID))
if err := WaitForVolume(blockStorageClient, volume.ID); err != nil { if err := WaitForVolume(blockStorageClient, volume.ID); err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err) err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err) state.Put("error", err)
@ -92,63 +87,6 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionContinue return multistep.ActionContinue
} }
// GetVolumeSize returns volume size in gigabytes based on the image min disk
// value if it's not empty.
// Or it calculates needed gigabytes size from the image bytes size.
func GetVolumeSize(imageClient *gophercloud.ServiceClient, imageID string) (int, error) {
sourceImage, err := images.Get(imageClient, imageID).Extract()
if err != nil {
return 0, err
}
if sourceImage.MinDiskGigabytes != 0 {
return sourceImage.MinDiskGigabytes, nil
}
volumeSizeMB := sourceImage.SizeBytes / 1024 / 1024
volumeSizeGB := int(sourceImage.SizeBytes / 1024 / 1024 / 1024)
// Increment gigabytes size if the initial size can't be divided without
// remainder.
if volumeSizeMB%1024 > 0 {
volumeSizeGB++
}
return volumeSizeGB, nil
}
// WaitForVolume waits for the given volume to become ready.
func WaitForVolume(blockStorageClient *gophercloud.ServiceClient, volumeID string) error {
maxNumErrors := 10
numErrors := 0
for {
volume, err := volumes.Get(blockStorageClient, volumeID).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
if ok && (errCode.Actual == 500 || errCode.Actual == 404) {
numErrors++
if numErrors >= maxNumErrors {
log.Printf("[ERROR] Maximum number of errors (%d) reached; failing with: %s", numErrors, err)
return err
}
log.Printf("[ERROR] %d error received, will ignore and retry: %s", errCode.Actual, err)
time.Sleep(2 * time.Second)
continue
}
return err
}
if volume.Status == "available" {
return nil
}
log.Printf("Waiting for volume creation status: %s", volume.Status)
time.Sleep(2 * time.Second)
}
}
func (s *StepCreateVolume) Cleanup(state multistep.StateBag) { func (s *StepCreateVolume) Cleanup(state multistep.StateBag) {
if !s.doCleanup { if !s.doCleanup {
return return

View File

@ -0,0 +1,54 @@
package openstack
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepDetachVolume struct {
UseBlockStorageVolume bool
}
func (s *StepDetachVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// Proceed only if block storage volume is used.
if !s.UseBlockStorageVolume {
return multistep.ActionContinue
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
err = fmt.Errorf("Error initializing block storage client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
volume := state.Get("volume_id").(string)
ui.Say(fmt.Sprintf("Detaching volume %s (volume id: %s)", config.VolumeName, volume))
if err := volumeactions.Detach(blockStorageClient, volume, volumeactions.DetachOpts{}).ExtractErr(); err != nil {
err = fmt.Errorf("Error detaching block storage volume: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
// Wait for volume to become available.
ui.Say(fmt.Sprintf("Waiting for volume %s (volume id: %s) to become available...", config.VolumeName, volume))
if err := WaitForVolume(blockStorageClient, volume); err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepDetachVolume) Cleanup(multistep.StateBag) {
// No cleanup.
}

View File

@ -0,0 +1,67 @@
package openstack
import (
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
)
// WaitForVolume waits for the given volume to become available.
func WaitForVolume(blockStorageClient *gophercloud.ServiceClient, volumeID string) error {
maxNumErrors := 10
numErrors := 0
for {
volume, err := volumes.Get(blockStorageClient, volumeID).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
if ok && (errCode.Actual == 500 || errCode.Actual == 404) {
numErrors++
if numErrors >= maxNumErrors {
log.Printf("[ERROR] Maximum number of errors (%d) reached; failing with: %s", numErrors, err)
return err
}
log.Printf("[ERROR] %d error received, will ignore and retry: %s", errCode.Actual, err)
time.Sleep(2 * time.Second)
continue
}
return err
}
if volume.Status == "available" {
return nil
}
log.Printf("Waiting for volume creation status: %s", volume.Status)
time.Sleep(2 * time.Second)
}
}
// GetVolumeSize returns volume size in gigabytes based on the image min disk
// value if it's not empty.
// Or it calculates needed gigabytes size from the image bytes size.
func GetVolumeSize(imageClient *gophercloud.ServiceClient, imageID string) (int, error) {
sourceImage, err := images.Get(imageClient, imageID).Extract()
if err != nil {
return 0, err
}
if sourceImage.MinDiskGigabytes != 0 {
return sourceImage.MinDiskGigabytes, nil
}
volumeSizeMB := sourceImage.SizeBytes / 1024 / 1024
volumeSizeGB := int(sourceImage.SizeBytes / 1024 / 1024 / 1024)
// Increment gigabytes size if the initial size can't be divided without
// remainder.
if volumeSizeMB%1024 > 0 {
volumeSizeGB++
}
return volumeSizeGB, nil
}