Merge pull request #6596 from rickard-von-essen/openstack-cinder-root-volume

OpenStack: Block Storage volumes support
This commit is contained in:
Rickard von Essen 2018-08-16 12:47:57 +02:00 committed by GitHub
commit 71c515fda1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1741 additions and 40 deletions

View File

@ -194,6 +194,13 @@ func (c *AccessConfig) imageV2Client() (*gophercloud.ServiceClient, error) {
})
}
func (c *AccessConfig) blockStorageV3Client() (*gophercloud.ServiceClient, error) {
return openstack.NewBlockStorageV3(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
}
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
return gophercloud.AvailabilityInternal

View File

@ -86,6 +86,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
SSHAgentAuth: b.config.RunConfig.Comm.SSHAgentAuth,
},
&StepCreateVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
SourceImage: b.config.SourceImage,
VolumeName: b.config.VolumeName,
VolumeType: b.config.VolumeType,
VolumeAvailabilityZone: b.config.VolumeAvailabilityZone,
},
&StepRunSourceServer{
Name: b.config.InstanceName,
SourceImage: b.config.SourceImage,
@ -98,6 +105,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
UserDataFile: b.config.UserDataFile,
ConfigDrive: b.config.ConfigDrive,
InstanceMetadata: b.config.InstanceMetadata,
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&StepGetPassword{
Debug: b.config.PackerDebug,
@ -124,7 +132,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&common.StepProvision{},
&StepStopServer{},
&stepCreateImage{},
&StepDetachVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&stepCreateImage{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&stepUpdateImageVisibility{},
&stepAddImageMembers{},
}

View File

@ -36,6 +36,11 @@ type RunConfig struct {
ConfigDrive bool `mapstructure:"config_drive"`
UseBlockStorageVolume bool `mapstructure:"use_blockstorage_volume"`
VolumeName string `mapstructure:"volume_name"`
VolumeType string `mapstructure:"volume_type"`
VolumeAvailabilityZone string `mapstructure:"volume_availability_zone"`
// Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
@ -90,5 +95,18 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
if c.UseBlockStorageVolume {
// Use Compute instance availability zone for the Block Storage volume if
// it's not provided.
if c.VolumeAvailabilityZone == "" {
c.VolumeAvailabilityZone = c.AvailabilityZone
}
// Use random name for the Block Storage volume if it's not provided.
if c.VolumeName == "" {
c.VolumeName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
}
return errs
}

View File

@ -70,3 +70,42 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_BlockStorage(t *testing.T) {
c := testRunConfig()
c.UseBlockStorageVolume = true
c.VolumeType = "fast"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeType != "fast" {
t.Fatalf("invalid value: %s", c.VolumeType)
}
c.AvailabilityZone = "RegionTwo"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeAvailabilityZone != "RegionTwo" {
t.Fatalf("invalid value: %s", c.VolumeAvailabilityZone)
}
c.VolumeAvailabilityZone = "RegionOne"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeAvailabilityZone != "RegionOne" {
t.Fatalf("invalid value: %s", c.VolumeAvailabilityZone)
}
c.VolumeName = "PackerVolume"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeName != "PackerVolume" {
t.Fatalf("invalid value: %s", c.VolumeName)
}
}

View File

@ -6,6 +6,8 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
@ -13,7 +15,9 @@ import (
"github.com/hashicorp/packer/packer"
)
type stepCreateImage struct{}
type stepCreateImage struct {
UseBlockStorageVolume bool
}
func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
@ -28,9 +32,32 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
return multistep.ActionHalt
}
// Create the image
// Create the image.
// Image source depends on the type of the Compute instance. It can be
// Block Storage service volume or regular Compute service local volume.
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{
var imageId string
if s.UseBlockStorageVolume {
// We need the v3 block storage client.
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)
image, err := volumeactions.UploadImage(blockStorageClient, volume, volumeactions.UploadImageOpts{
ImageName: config.ImageName,
}).Extract()
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
imageId = image.ImageID
} else {
imageId, err = servers.CreateImage(client, server.ID, servers.CreateImageOpts{
Name: config.ImageName,
Metadata: config.ImageMetadata,
}).ExtractImageID()
@ -40,6 +67,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Set the Image ID in the state
ui.Message(fmt.Sprintf("Image: %s", imageId))

View File

@ -0,0 +1,111 @@
package openstack
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepCreateVolume struct {
UseBlockStorageVolume bool
SourceImage string
VolumeName string
VolumeType string
VolumeAvailabilityZone string
volumeID string
doCleanup bool
}
func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// Proceed only if block storage volume is required.
if !s.UseBlockStorageVolume {
return multistep.ActionContinue
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
// We will need Block Storage and Image services clients.
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
err = fmt.Errorf("Error initializing block storage client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
imageClient, err := config.imageV2Client()
if err != nil {
err = fmt.Errorf("Error initializing image client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
// Get needed volume size from the source image.
volumeSize, err := GetVolumeSize(imageClient, s.SourceImage)
if err != nil {
err := fmt.Errorf("Error creating volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating volume...")
volumeOpts := volumes.CreateOpts{
Size: volumeSize,
VolumeType: s.VolumeType,
AvailabilityZone: s.VolumeAvailabilityZone,
Name: s.VolumeName,
ImageID: s.SourceImage,
}
volume, err := volumes.Create(blockStorageClient, volumeOpts).Extract()
if err != nil {
err := fmt.Errorf("Error creating volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
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.ID))
if err := WaitForVolume(blockStorageClient, volume.ID); err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Volume was created, so remember to clean it up.
s.doCleanup = true
// Set the Volume ID in the state.
ui.Message(fmt.Sprintf("Volume ID: %s", volume.ID))
state.Put("volume_id", volume.ID)
s.volumeID = volume.ID
return multistep.ActionContinue
}
func (s *StepCreateVolume) Cleanup(state multistep.StateBag) {
if !s.doCleanup {
return
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up volume. Please delete the volume manually: %s", s.volumeID))
return
}
ui.Say(fmt.Sprintf("Deleting volume: %s ...", s.volumeID))
err = volumes.Delete(blockStorageClient, s.volumeID).ExtractErr()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up volume. Please delete the volume manually: %s", s.volumeID))
}
}

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

@ -6,6 +6,7 @@ import (
"io/ioutil"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/hashicorp/packer/helper/multistep"
@ -24,6 +25,7 @@ type StepRunSourceServer struct {
UserDataFile string
ConfigDrive bool
InstanceMetadata map[string]string
UseBlockStorageVolume bool
server *servers.Server
}
@ -74,18 +76,40 @@ func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) m
ServiceClient: computeClient,
Metadata: s.InstanceMetadata,
}
var serverOptsExt servers.CreateOptsBuilder
keyName, hasKey := state.GetOk("keyPair")
if hasKey {
serverOptsExt = keypairs.CreateOptsExt{
// Create root volume in the Block Storage service if required.
// Add block device mapping v2 to the server create options if required.
if s.UseBlockStorageVolume {
volume := state.Get("volume_id").(string)
blockDeviceMappingV2 := []bootfromvolume.BlockDevice{
{
BootIndex: 0,
DestinationType: bootfromvolume.DestinationVolume,
SourceType: bootfromvolume.SourceVolume,
UUID: volume,
},
}
// ImageRef and block device mapping is an invalid options combination.
serverOpts.ImageRef = ""
serverOptsExt = bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverOpts,
KeyName: keyName.(string),
BlockDevice: blockDeviceMappingV2,
}
} else {
serverOptsExt = serverOpts
}
// Add keypair to the server create options.
keyName, hasKey := state.GetOk("keyPair")
if hasKey {
serverOptsExt = keypairs.CreateOptsExt{
CreateOptsBuilder: serverOptsExt,
KeyName: keyName.(string),
}
}
ui.Say("Launching server...")
s.server, err = servers.Create(computeClient, serverOptsExt).Extract()
if err != nil {
err := fmt.Errorf("Error launching source server: %s", err)

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
}

View File

@ -0,0 +1,86 @@
/*
Package volumeactions provides information and interaction with volumes in the
OpenStack Block Storage service. A volume is a detachable block storage
device, akin to a USB hard drive.
Example of Attaching a Volume to an Instance
attachOpts := volumeactions.AttachOpts{
MountPoint: "/mnt",
Mode: "rw",
InstanceUUID: server.ID,
}
err := volumeactions.Attach(client, volume.ID, attachOpts).ExtractErr()
if err != nil {
panic(err)
}
detachOpts := volumeactions.DetachOpts{
AttachmentID: volume.Attachments[0].AttachmentID,
}
err = volumeactions.Detach(client, volume.ID, detachOpts).ExtractErr()
if err != nil {
panic(err)
}
Example of Creating an Image from a Volume
uploadImageOpts := volumeactions.UploadImageOpts{
ImageName: "my_vol",
Force: true,
}
volumeImage, err := volumeactions.UploadImage(client, volume.ID, uploadImageOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", volumeImage)
Example of Extending a Volume's Size
extendOpts := volumeactions.ExtendSizeOpts{
NewSize: 100,
}
err := volumeactions.ExtendSize(client, volume.ID, extendOpts).ExtractErr()
if err != nil {
panic(err)
}
Example of Initializing a Volume Connection
connectOpts := &volumeactions.InitializeConnectionOpts{
IP: "127.0.0.1",
Host: "stack",
Initiator: "iqn.1994-05.com.redhat:17cf566367d2",
Multipath: gophercloud.Disabled,
Platform: "x86_64",
OSType: "linux2",
}
connectionInfo, err := volumeactions.InitializeConnection(client, volume.ID, connectOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", connectionInfo["data"])
terminateOpts := &volumeactions.InitializeConnectionOpts{
IP: "127.0.0.1",
Host: "stack",
Initiator: "iqn.1994-05.com.redhat:17cf566367d2",
Multipath: gophercloud.Disabled,
Platform: "x86_64",
OSType: "linux2",
}
err = volumeactions.TerminateConnection(client, volume.ID, terminateOpts).ExtractErr()
if err != nil {
panic(err)
}
*/
package volumeactions

View File

@ -0,0 +1,269 @@
package volumeactions
import (
"github.com/gophercloud/gophercloud"
)
// AttachOptsBuilder allows extensions to add additional parameters to the
// Attach request.
type AttachOptsBuilder interface {
ToVolumeAttachMap() (map[string]interface{}, error)
}
// AttachMode describes the attachment mode for volumes.
type AttachMode string
// These constants determine how a volume is attached.
const (
ReadOnly AttachMode = "ro"
ReadWrite AttachMode = "rw"
)
// AttachOpts contains options for attaching a Volume.
type AttachOpts struct {
// The mountpoint of this volume.
MountPoint string `json:"mountpoint,omitempty"`
// The nova instance ID, can't set simultaneously with HostName.
InstanceUUID string `json:"instance_uuid,omitempty"`
// The hostname of baremetal host, can't set simultaneously with InstanceUUID.
HostName string `json:"host_name,omitempty"`
// Mount mode of this volume.
Mode AttachMode `json:"mode,omitempty"`
}
// ToVolumeAttachMap assembles a request body based on the contents of a
// AttachOpts.
func (opts AttachOpts) ToVolumeAttachMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-attach")
}
// Attach will attach a volume based on the values in AttachOpts.
func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder) (r AttachResult) {
b, err := opts.ToVolumeAttachMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// BeginDetach will mark the volume as detaching.
func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) {
b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// DetachOptsBuilder allows extensions to add additional parameters to the
// Detach request.
type DetachOptsBuilder interface {
ToVolumeDetachMap() (map[string]interface{}, error)
}
// DetachOpts contains options for detaching a Volume.
type DetachOpts struct {
// AttachmentID is the ID of the attachment between a volume and instance.
AttachmentID string `json:"attachment_id,omitempty"`
}
// ToVolumeDetachMap assembles a request body based on the contents of a
// DetachOpts.
func (opts DetachOpts) ToVolumeDetachMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-detach")
}
// Detach will detach a volume based on volume ID.
func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder) (r DetachResult) {
b, err := opts.ToVolumeDetachMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// Reserve will reserve a volume based on volume ID.
func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
b := map[string]interface{}{"os-reserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return
}
// Unreserve will unreserve a volume based on volume ID.
func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) {
b := map[string]interface{}{"os-unreserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return
}
// InitializeConnectionOptsBuilder allows extensions to add additional parameters to the
// InitializeConnection request.
type InitializeConnectionOptsBuilder interface {
ToVolumeInitializeConnectionMap() (map[string]interface{}, error)
}
// InitializeConnectionOpts hosts options for InitializeConnection.
// The fields are specific to the storage driver in use and the destination
// attachment.
type InitializeConnectionOpts struct {
IP string `json:"ip,omitempty"`
Host string `json:"host,omitempty"`
Initiator string `json:"initiator,omitempty"`
Wwpns []string `json:"wwpns,omitempty"`
Wwnns string `json:"wwnns,omitempty"`
Multipath *bool `json:"multipath,omitempty"`
Platform string `json:"platform,omitempty"`
OSType string `json:"os_type,omitempty"`
}
// ToVolumeInitializeConnectionMap assembles a request body based on the contents of a
// InitializeConnectionOpts.
func (opts InitializeConnectionOpts) ToVolumeInitializeConnectionMap() (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "connector")
return map[string]interface{}{"os-initialize_connection": b}, err
}
// InitializeConnection initializes an iSCSI connection by volume ID.
func InitializeConnection(client *gophercloud.ServiceClient, id string, opts InitializeConnectionOptsBuilder) (r InitializeConnectionResult) {
b, err := opts.ToVolumeInitializeConnectionMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
return
}
// TerminateConnectionOptsBuilder allows extensions to add additional parameters to the
// TerminateConnection request.
type TerminateConnectionOptsBuilder interface {
ToVolumeTerminateConnectionMap() (map[string]interface{}, error)
}
// TerminateConnectionOpts hosts options for TerminateConnection.
type TerminateConnectionOpts struct {
IP string `json:"ip,omitempty"`
Host string `json:"host,omitempty"`
Initiator string `json:"initiator,omitempty"`
Wwpns []string `json:"wwpns,omitempty"`
Wwnns string `json:"wwnns,omitempty"`
Multipath *bool `json:"multipath,omitempty"`
Platform string `json:"platform,omitempty"`
OSType string `json:"os_type,omitempty"`
}
// ToVolumeTerminateConnectionMap assembles a request body based on the contents of a
// TerminateConnectionOpts.
func (opts TerminateConnectionOpts) ToVolumeTerminateConnectionMap() (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "connector")
return map[string]interface{}{"os-terminate_connection": b}, err
}
// TerminateConnection terminates an iSCSI connection by volume ID.
func TerminateConnection(client *gophercloud.ServiceClient, id string, opts TerminateConnectionOptsBuilder) (r TerminateConnectionResult) {
b, err := opts.ToVolumeTerminateConnectionMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// ExtendSizeOptsBuilder allows extensions to add additional parameters to the
// ExtendSize request.
type ExtendSizeOptsBuilder interface {
ToVolumeExtendSizeMap() (map[string]interface{}, error)
}
// ExtendSizeOpts contains options for extending the size of an existing Volume.
// This object is passed to the volumes.ExtendSize function.
type ExtendSizeOpts struct {
// NewSize is the new size of the volume, in GB.
NewSize int `json:"new_size" required:"true"`
}
// ToVolumeExtendSizeMap assembles a request body based on the contents of an
// ExtendSizeOpts.
func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-extend")
}
// ExtendSize will extend the size of the volume based on the provided information.
// This operation does not return a response body.
func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) {
b, err := opts.ToVolumeExtendSizeMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// UploadImageOptsBuilder allows extensions to add additional parameters to the
// UploadImage request.
type UploadImageOptsBuilder interface {
ToVolumeUploadImageMap() (map[string]interface{}, error)
}
// UploadImageOpts contains options for uploading a Volume to image storage.
type UploadImageOpts struct {
// Container format, may be bare, ofv, ova, etc.
ContainerFormat string `json:"container_format,omitempty"`
// Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc.
DiskFormat string `json:"disk_format,omitempty"`
// The name of image that will be stored in glance.
ImageName string `json:"image_name,omitempty"`
// Force image creation, usable if volume attached to instance.
Force bool `json:"force,omitempty"`
}
// ToVolumeUploadImageMap assembles a request body based on the contents of a
// UploadImageOpts.
func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-volume_upload_image")
}
// UploadImage will upload an image based on the values in UploadImageOptsBuilder.
func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) {
b, err := opts.ToVolumeUploadImageMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// ForceDelete will delete the volume regardless of state.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil)
return
}

View File

@ -0,0 +1,191 @@
package volumeactions
import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud"
)
// AttachResult contains the response body and error from an Attach request.
type AttachResult struct {
gophercloud.ErrResult
}
// BeginDetachingResult contains the response body and error from a BeginDetach
// request.
type BeginDetachingResult struct {
gophercloud.ErrResult
}
// DetachResult contains the response body and error from a Detach request.
type DetachResult struct {
gophercloud.ErrResult
}
// UploadImageResult contains the response body and error from an UploadImage
// request.
type UploadImageResult struct {
gophercloud.Result
}
// ReserveResult contains the response body and error from a Reserve request.
type ReserveResult struct {
gophercloud.ErrResult
}
// UnreserveResult contains the response body and error from an Unreserve
// request.
type UnreserveResult struct {
gophercloud.ErrResult
}
// TerminateConnectionResult contains the response body and error from a
// TerminateConnection request.
type TerminateConnectionResult struct {
gophercloud.ErrResult
}
// InitializeConnectionResult contains the response body and error from an
// InitializeConnection request.
type InitializeConnectionResult struct {
gophercloud.Result
}
// ExtendSizeResult contains the response body and error from an ExtendSize request.
type ExtendSizeResult struct {
gophercloud.ErrResult
}
// Extract will get the connection information out of the
// InitializeConnectionResult object.
//
// This will be a generic map[string]interface{} and the results will be
// dependent on the type of connection made.
func (r InitializeConnectionResult) Extract() (map[string]interface{}, error) {
var s struct {
ConnectionInfo map[string]interface{} `json:"connection_info"`
}
err := r.ExtractInto(&s)
return s.ConnectionInfo, err
}
// ImageVolumeType contains volume type information obtained from UploadImage
// action.
type ImageVolumeType struct {
// The ID of a volume type.
ID string `json:"id"`
// Human-readable display name for the volume type.
Name string `json:"name"`
// Human-readable description for the volume type.
Description string `json:"display_description"`
// Flag for public access.
IsPublic bool `json:"is_public"`
// Extra specifications for volume type.
ExtraSpecs map[string]interface{} `json:"extra_specs"`
// ID of quality of service specs.
QosSpecsID string `json:"qos_specs_id"`
// Flag for deletion status of volume type.
Deleted bool `json:"deleted"`
// The date when volume type was deleted.
DeletedAt time.Time `json:"-"`
// The date when volume type was created.
CreatedAt time.Time `json:"-"`
// The date when this volume was last updated.
UpdatedAt time.Time `json:"-"`
}
func (r *ImageVolumeType) UnmarshalJSON(b []byte) error {
type tmp ImageVolumeType
var s struct {
tmp
CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = ImageVolumeType(s.tmp)
r.CreatedAt = time.Time(s.CreatedAt)
r.UpdatedAt = time.Time(s.UpdatedAt)
r.DeletedAt = time.Time(s.DeletedAt)
return err
}
// VolumeImage contains information about volume uploaded to an image service.
type VolumeImage struct {
// The ID of a volume an image is created from.
VolumeID string `json:"id"`
// Container format, may be bare, ofv, ova, etc.
ContainerFormat string `json:"container_format"`
// Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc.
DiskFormat string `json:"disk_format"`
// Human-readable description for the volume.
Description string `json:"display_description"`
// The ID of the created image.
ImageID string `json:"image_id"`
// Human-readable display name for the image.
ImageName string `json:"image_name"`
// Size of the volume in GB.
Size int `json:"size"`
// Current status of the volume.
Status string `json:"status"`
// The date when this volume was last updated.
UpdatedAt time.Time `json:"-"`
// Volume type object of used volume.
VolumeType ImageVolumeType `json:"volume_type"`
}
func (r *VolumeImage) UnmarshalJSON(b []byte) error {
type tmp VolumeImage
var s struct {
tmp
UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = VolumeImage(s.tmp)
r.UpdatedAt = time.Time(s.UpdatedAt)
return err
}
// Extract will get an object with info about the uploaded image out of the
// UploadImageResult object.
func (r UploadImageResult) Extract() (VolumeImage, error) {
var s struct {
VolumeImage VolumeImage `json:"os-volume_upload_image"`
}
err := r.ExtractInto(&s)
return s.VolumeImage, err
}
// ForceDeleteResult contains the response body and error from a ForceDelete request.
type ForceDeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,7 @@
package volumeactions
import "github.com/gophercloud/gophercloud"
func actionURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("volumes", id, "action")
}

View File

@ -0,0 +1,5 @@
// Package volumes provides information and interaction with volumes in the
// OpenStack Block Storage service. A volume is a detachable block storage
// device, akin to a USB hard drive. It can only be attached to one instance at
// a time.
package volumes

View File

@ -0,0 +1,202 @@
package volumes
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToVolumeCreateMap() (map[string]interface{}, error)
}
// CreateOpts contains options for creating a Volume. This object is passed to
// the volumes.Create function. For more information about these parameters,
// see the Volume object.
type CreateOpts struct {
// The size of the volume, in GB
Size int `json:"size" required:"true"`
// The availability zone
AvailabilityZone string `json:"availability_zone,omitempty"`
// ConsistencyGroupID is the ID of a consistency group
ConsistencyGroupID string `json:"consistencygroup_id,omitempty"`
// The volume description
Description string `json:"description,omitempty"`
// One or more metadata key and value pairs to associate with the volume
Metadata map[string]string `json:"metadata,omitempty"`
// The volume name
Name string `json:"name,omitempty"`
// the ID of the existing volume snapshot
SnapshotID string `json:"snapshot_id,omitempty"`
// SourceReplica is a UUID of an existing volume to replicate with
SourceReplica string `json:"source_replica,omitempty"`
// the ID of the existing volume
SourceVolID string `json:"source_volid,omitempty"`
// The ID of the image from which you want to create the volume.
// Required to create a bootable volume.
ImageID string `json:"imageRef,omitempty"`
// The associated volume type
VolumeType string `json:"volume_type,omitempty"`
}
// ToVolumeCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToVolumeCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "volume")
}
// Create will create a new Volume based on the values in CreateOpts. To extract
// the Volume object from the response, call the Extract method on the
// CreateResult.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToVolumeCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
return
}
// Delete will delete the existing Volume with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// ListOptsBuilder allows extensions to add additional parameters to the List
// request.
type ListOptsBuilder interface {
ToVolumeListQuery() (string, error)
}
// ListOpts holds options for listing Volumes. It is passed to the volumes.List
// function.
type ListOpts struct {
// AllTenants will retrieve volumes of all tenants/projects.
AllTenants bool `q:"all_tenants"`
// Metadata will filter results based on specified metadata.
Metadata map[string]string `q:"metadata"`
// Name will filter by the specified volume name.
Name string `q:"name"`
// Status will filter by the specified status.
Status string `q:"status"`
// TenantID will filter by a specific tenant/project ID.
// Setting AllTenants is required for this.
TenantID string `q:"project_id"`
// Comma-separated list of sort keys and optional sort directions in the
// form of <key>[:<direction>].
Sort string `q:"sort"`
// Requests a page size of items.
Limit int `q:"limit"`
// Used in conjunction with limit to return a slice of items.
Offset int `q:"offset"`
// The ID of the last-seen item.
Marker string `q:"marker"`
}
// ToVolumeListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToVolumeListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// List returns Volumes optionally limited by the conditions provided in ListOpts.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToVolumeListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return VolumePage{pagination.LinkedPageBase{PageResult: r}}
})
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateOptsBuilder interface {
ToVolumeUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts contain options for updating an existing Volume. This object is passed
// to the volumes.Update function. For more information about the parameters, see
// the Volume object.
type UpdateOpts struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// ToVolumeUpdateMap assembles a request body based on the contents of an
// UpdateOpts.
func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "volume")
}
// Update will update the Volume with provided information. To extract the updated
// Volume from the response, call the Extract method on the UpdateResult.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToVolumeUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// IDFromName is a convienience function that returns a server's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
pages, err := List(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := ExtractVolumes(pages)
if err != nil {
return "", err
}
for _, s := range all {
if s.Name == name {
count++
id = s.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"}
}
}

View File

@ -0,0 +1,170 @@
package volumes
import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Attachment represents a Volume Attachment record
type Attachment struct {
AttachedAt time.Time `json:"-"`
AttachmentID string `json:"attachment_id"`
Device string `json:"device"`
HostName string `json:"host_name"`
ID string `json:"id"`
ServerID string `json:"server_id"`
VolumeID string `json:"volume_id"`
}
// UnmarshalJSON is our unmarshalling helper
func (r *Attachment) UnmarshalJSON(b []byte) error {
type tmp Attachment
var s struct {
tmp
AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Attachment(s.tmp)
r.AttachedAt = time.Time(s.AttachedAt)
return err
}
// Volume contains all the information associated with an OpenStack Volume.
type Volume struct {
// Unique identifier for the volume.
ID string `json:"id"`
// Current status of the volume.
Status string `json:"status"`
// Size of the volume in GB.
Size int `json:"size"`
// AvailabilityZone is which availability zone the volume is in.
AvailabilityZone string `json:"availability_zone"`
// The date when this volume was created.
CreatedAt time.Time `json:"-"`
// The date when this volume was last updated
UpdatedAt time.Time `json:"-"`
// Instances onto which the volume is attached.
Attachments []Attachment `json:"attachments"`
// Human-readable display name for the volume.
Name string `json:"name"`
// Human-readable description for the volume.
Description string `json:"description"`
// The type of volume to create, either SATA or SSD.
VolumeType string `json:"volume_type"`
// The ID of the snapshot from which the volume was created
SnapshotID string `json:"snapshot_id"`
// The ID of another block storage volume from which the current volume was created
SourceVolID string `json:"source_volid"`
// Arbitrary key-value pairs defined by the user.
Metadata map[string]string `json:"metadata"`
// UserID is the id of the user who created the volume.
UserID string `json:"user_id"`
// Indicates whether this is a bootable volume.
Bootable string `json:"bootable"`
// Encrypted denotes if the volume is encrypted.
Encrypted bool `json:"encrypted"`
// ReplicationStatus is the status of replication.
ReplicationStatus string `json:"replication_status"`
// ConsistencyGroupID is the consistency group ID.
ConsistencyGroupID string `json:"consistencygroup_id"`
// Multiattach denotes if the volume is multi-attach capable.
Multiattach bool `json:"multiattach"`
}
// UnmarshalJSON another unmarshalling function
func (r *Volume) UnmarshalJSON(b []byte) error {
type tmp Volume
var s struct {
tmp
CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Volume(s.tmp)
r.CreatedAt = time.Time(s.CreatedAt)
r.UpdatedAt = time.Time(s.UpdatedAt)
return err
}
// VolumePage is a pagination.pager that is returned from a call to the List function.
type VolumePage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if a ListResult contains no Volumes.
func (r VolumePage) IsEmpty() (bool, error) {
volumes, err := ExtractVolumes(r)
return len(volumes) == 0, err
}
func (page VolumePage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"volumes_links"`
}
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
func ExtractVolumes(r pagination.Page) ([]Volume, error) {
var s []Volume
err := ExtractVolumesInto(r, &s)
return s, err
}
type commonResult struct {
gophercloud.Result
}
// Extract will get the Volume object out of the commonResult object.
func (r commonResult) Extract() (*Volume, error) {
var s Volume
err := r.ExtractInto(&s)
return &s, err
}
// ExtractInto converts our response data into a volume struct
func (r commonResult) ExtractInto(v interface{}) error {
return r.Result.ExtractIntoStructPtr(v, "volume")
}
// ExtractVolumesInto similar to ExtractInto but operates on a `list` of volumes
func ExtractVolumesInto(r pagination.Page, v interface{}) error {
return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes")
}
// CreateResult contains the response body and error from a Create request.
type CreateResult struct {
commonResult
}
// GetResult contains the response body and error from a Get request.
type GetResult struct {
commonResult
}
// UpdateResult contains the response body and error from an Update request.
type UpdateResult struct {
commonResult
}
// DeleteResult contains the response body and error from a Delete request.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,23 @@
package volumes
import "github.com/gophercloud/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes")
}
func listURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("volumes", "detail")
}
func deleteURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("volumes", id)
}
func getURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}
func updateURL(c *gophercloud.ServiceClient, id string) string {
return deleteURL(c, id)
}

View File

@ -0,0 +1,22 @@
package volumes
import (
"github.com/gophercloud/gophercloud"
)
// WaitForStatus will continually poll the resource, checking for a particular
// status. It will do this for the amount of seconds defined.
func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return gophercloud.WaitFor(secs, func() (bool, error) {
current, err := Get(c, id).Extract()
if err != nil {
return false, err
}
if current.Status == status {
return true, nil
}
return false, nil
})
}

View File

@ -0,0 +1,152 @@
/*
Package bootfromvolume extends a server create request with the ability to
specify block device options. This can be used to boot a server from a block
storage volume as well as specify multiple ephemeral disks upon creation.
It is recommended to refer to the Block Device Mapping documentation to see
all possible ways to configure a server's block devices at creation time:
https://docs.openstack.org/nova/latest/user/block-device-mapping.html
Note that this package implements `block_device_mapping_v2`.
Example of Creating a Server From an Image
This example will boot a server from an image and use a standard ephemeral
disk as the server's root disk. This is virtually no different than creating
a server without using block device mappings.
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
BootIndex: 0,
DeleteOnTermination: true,
DestinationType: bootfromvolume.DestinationLocal,
SourceType: bootfromvolume.SourceImage,
UUID: "image-uuid",
},
}
serverCreateOpts := servers.CreateOpts{
Name: "server_name",
FlavorRef: "flavor-uuid",
ImageRef: "image-uuid",
}
createOpts := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
BlockDevice: blockDevices,
}
server, err := bootfromvolume.Create(client, createOpts).Extract()
if err != nil {
panic(err)
}
Example of Creating a Server From a New Volume
This example will create a block storage volume based on the given Image. The
server will use this volume as its root disk.
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
DeleteOnTermination: true,
DestinationType: bootfromvolume.DestinationVolume,
SourceType: bootfromvolume.SourceImage,
UUID: "image-uuid",
VolumeSize: 2,
},
}
serverCreateOpts := servers.CreateOpts{
Name: "server_name",
FlavorRef: "flavor-uuid",
}
createOpts := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
BlockDevice: blockDevices,
}
server, err := bootfromvolume.Create(client, createOpts).Extract()
if err != nil {
panic(err)
}
Example of Creating a Server From an Existing Volume
This example will create a server with an existing volume as its root disk.
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
DeleteOnTermination: true,
DestinationType: bootfromvolume.DestinationVolume,
SourceType: bootfromvolume.SourceVolume,
UUID: "volume-uuid",
},
}
serverCreateOpts := servers.CreateOpts{
Name: "server_name",
FlavorRef: "flavor-uuid",
}
createOpts := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
BlockDevice: blockDevices,
}
server, err := bootfromvolume.Create(client, createOpts).Extract()
if err != nil {
panic(err)
}
Example of Creating a Server with Multiple Ephemeral Disks
This example will create a server with multiple ephemeral disks. The first
block device will be based off of an existing Image. Each additional
ephemeral disks must have an index of -1.
blockDevices := []bootfromvolume.BlockDevice{
bootfromvolume.BlockDevice{
BootIndex: 0,
DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
SourceType: bootfromvolume.SourceImage,
UUID: "image-uuid",
VolumeSize: 5,
},
bootfromvolume.BlockDevice{
BootIndex: -1,
DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
GuestFormat: "ext4",
SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
bootfromvolume.BlockDevice{
BootIndex: -1,
DestinationType: bootfromvolume.DestinationLocal,
DeleteOnTermination: true,
GuestFormat: "ext4",
SourceType: bootfromvolume.SourceBlank,
VolumeSize: 1,
},
}
serverCreateOpts := servers.CreateOpts{
Name: "server_name",
FlavorRef: "flavor-uuid",
ImageRef: "image-uuid",
}
createOpts := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverCreateOpts,
BlockDevice: blockDevices,
}
server, err := bootfromvolume.Create(client, createOpts).Extract()
if err != nil {
panic(err)
}
*/
package bootfromvolume

View File

@ -0,0 +1,120 @@
package bootfromvolume
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
)
type (
// DestinationType represents the type of medium being used as the
// destination of the bootable device.
DestinationType string
// SourceType represents the type of medium being used as the source of the
// bootable device.
SourceType string
)
const (
// DestinationLocal DestinationType is for using an ephemeral disk as the
// destination.
DestinationLocal DestinationType = "local"
// DestinationVolume DestinationType is for using a volume as the destination.
DestinationVolume DestinationType = "volume"
// SourceBlank SourceType is for a "blank" or empty source.
SourceBlank SourceType = "blank"
// SourceImage SourceType is for using images as the source of a block device.
SourceImage SourceType = "image"
// SourceSnapshot SourceType is for using a volume snapshot as the source of
// a block device.
SourceSnapshot SourceType = "snapshot"
// SourceVolume SourceType is for using a volume as the source of block
// device.
SourceVolume SourceType = "volume"
)
// BlockDevice is a structure with options for creating block devices in a
// server. The block device may be created from an image, snapshot, new volume,
// or existing volume. The destination may be a new volume, existing volume
// which will be attached to the instance, ephemeral disk, or boot device.
type BlockDevice struct {
// SourceType must be one of: "volume", "snapshot", "image", or "blank".
SourceType SourceType `json:"source_type" required:"true"`
// UUID is the unique identifier for the existing volume, snapshot, or
// image (see above).
UUID string `json:"uuid,omitempty"`
// BootIndex is the boot index. It defaults to 0.
BootIndex int `json:"boot_index"`
// DeleteOnTermination specifies whether or not to delete the attached volume
// when the server is deleted. Defaults to `false`.
DeleteOnTermination bool `json:"delete_on_termination"`
// DestinationType is the type that gets created. Possible values are "volume"
// and "local".
DestinationType DestinationType `json:"destination_type,omitempty"`
// GuestFormat specifies the format of the block device.
GuestFormat string `json:"guest_format,omitempty"`
// VolumeSize is the size of the volume to create (in gigabytes). This can be
// omitted for existing volumes.
VolumeSize int `json:"volume_size,omitempty"`
}
// CreateOptsExt is a structure that extends the server `CreateOpts` structure
// by allowing for a block device mapping.
type CreateOptsExt struct {
servers.CreateOptsBuilder
BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"`
}
// ToServerCreateMap adds the block device mapping option to the base server
// creation options.
func (opts CreateOptsExt) ToServerCreateMap() (map[string]interface{}, error) {
base, err := opts.CreateOptsBuilder.ToServerCreateMap()
if err != nil {
return nil, err
}
if len(opts.BlockDevice) == 0 {
err := gophercloud.ErrMissingInput{}
err.Argument = "bootfromvolume.CreateOptsExt.BlockDevice"
return nil, err
}
serverMap := base["server"].(map[string]interface{})
blockDevice := make([]map[string]interface{}, len(opts.BlockDevice))
for i, bd := range opts.BlockDevice {
b, err := gophercloud.BuildRequestBody(bd, "")
if err != nil {
return nil, err
}
blockDevice[i] = b
}
serverMap["block_device_mapping_v2"] = blockDevice
return base, nil
}
// Create requests the creation of a server from the given block device mapping.
func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) (r servers.CreateResult) {
b, err := opts.ToServerCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})
return
}

View File

@ -0,0 +1,12 @@
package bootfromvolume
import (
os "github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
)
// CreateResult temporarily contains the response from a Create call.
// It embeds the standard servers.CreateResults type and so can be used the
// same way as a standard server request result.
type CreateResult struct {
os.CreateResult
}

View File

@ -0,0 +1,7 @@
package bootfromvolume
import "github.com/gophercloud/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-volumes_boot")
}

30
vendor/vendor.json vendored
View File

@ -737,6 +737,36 @@
"revision": "7112fcd50da4ea27e8d4d499b30f04eea143bec2",
"revisionTime": "2018-05-31T02:06:30Z"
},
{
"checksumSHA1": "8YtBD+Um7I8ee1Xf1ZAWu74eP7w=",
"path": "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions",
"revision": "282f25e4025de0a42015d2e2b5faef1d920aad3c",
"revisionTime": "2018-05-15T01:47:05Z"
},
{
"checksumSHA1": "vqXNCd2My0y/5tPC/Bs79uf6Q4E=",
"path": "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes",
"revision": "282f25e4025de0a42015d2e2b5faef1d920aad3c",
"revisionTime": "2018-05-15T01:47:05Z"
},
{
"checksumSHA1": "Au6MAsI90lewLByg9n+Yjtdqdh8=",
"path": "github.com/gophercloud/gophercloud/openstack/common/extensions",
"revision": "95a28eb606def6aaaed082b6b82d3244b0552184",
"revisionTime": "2017-06-23T01:44:30Z"
},
{
"checksumSHA1": "4XWDCGMYqipwJymi9xJo9UffD7g=",
"path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions",
"revision": "95a28eb606def6aaaed082b6b82d3244b0552184",
"revisionTime": "2017-06-23T01:44:30Z"
},
{
"checksumSHA1": "kCHEEeRVZeR1LhbvNP+WyvB8z2s=",
"path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
"revision": "282f25e4025de0a42015d2e2b5faef1d920aad3c",
"revisionTime": "2018-05-15T01:47:05Z"
},
{
"checksumSHA1": "vFS5BwnCdQIfKm1nNWrR+ijsAZA=",
"path": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips",

View File

@ -36,6 +36,9 @@ installed *if you are using temporary key pairs*, i.e. don't use
OS'es have OpenSSL installed by default except Windows. This have been
resolved in OpenStack Ocata(Feb 2017).
~&gt; **Note:** OpenStack Block Storage volume support is available only for
V3 Block Storage API. It's available in OpenStack since Mitaka release
(Apr 2016).
## Configuration Reference
@ -206,6 +209,22 @@ builder.
- `user_data_file` (string) - Path to a file that will be used for the user
data when launching the instance.
- `use_blockstorage_volume` (boolean) - Use Block Storage service volume for
the instance root volume instead of Compute service local volume (default).
- `volume_name` (string) - Name of the Block Storage service volume. If this
isn't specified, random string will be used.
- `volume_type` (string) - Type of the Block Storage service volume. If this
isn't specified, the default enforced by your OpenStack cluster will be
used.
- `volume_availability_zone` (string) - Availability zone of the Block
Storage service volume. If omitted, Compute instance availability zone will
be used. If both of Compute instance and Block Storage volume availability
zones aren't specified, the default enforced by your OpenStack cluster will
be used.
## Basic Example: DevStack
Here is a basic example. This is a example to build on DevStack running in a VM.
@ -293,6 +312,31 @@ export OS_USER_DOMAIN_NAME="mydomain"
export OS_PROJECT_DOMAIN_NAME="mydomain"
```
## Basic Example: Instance with Block Storage root volume
A basic example of Instance with a remote root Block Storage service volume.
This is a working example to build an image on private OpenStack cloud powered
by Selectel VPC.
``` json
{
"type": "openstack",
"identity_endpoint": "https://api.selvpc.com/identity/v3",
"tenant_id": "2e90c5c04c7b4c509be78723e2b55b77",
"username": "foo",
"password": "foo",
"region": "ru-3",
"ssh_username": "root",
"image_name": "Test image",
"source_image": "5f58ea7e-6264-4939-9d0f-0c23072b1132",
"networks": "9aab504e-bedf-48af-9256-682a7fa3dabb",
"flavor": "1001",
"availability_zone": "ru-3a",
"use_blockstorage_volume": true,
"volume_type": "fast.ru-3a"
}
```
## Notes on OpenStack Authorization
The simplest way to get all settings for authorization against OpenStack is to