Merge pull request #7652 from OpenSource-THG/master

Openstack block device mapping support
This commit is contained in:
Kubernetes Prow Robot 2019-10-03 14:27:56 -07:00 committed by GitHub
commit f671873629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1341 additions and 3 deletions

View File

@ -549,3 +549,28 @@ spec:
minSize: 2
role: Node
```
## Booting from a volume in OpenStack
If you want to boot from a volume when you are running in openstack you can set annotations on the instance groups.
```yaml
# Example for nodes
apiVersion: kops.k8s.io/v1alpha2
kind: InstanceGroup
metadata:
labels:
kops.k8s.io/cluster: k8s.dev.local
name: nodes
annotations:
openstack.kops.io/osVolumeBoot: enabled
openstack.kops.io/osVolumeSize: "15" # In gigabytes
spec:
detailedInstanceMonitoring: true
machineType: t2.medium
maxSize: 2
minSize: 2
role: Node
```
If `openstack.kops.io/osVolumeSize` is not set it will default to the minimum disk specified by the image.

View File

@ -64,6 +64,14 @@ func (b *ServerGroupModelBuilder) buildInstances(c *fi.ModelBuilderContext, sg *
igMeta[openstack.INSTANCE_GROUP_GENERATION] = fmt.Sprintf("%d", ig.GetGeneration())
igMeta[openstack.CLUSTER_GENERATION] = fmt.Sprintf("%d", b.Cluster.GetGeneration())
if e, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.BOOT_FROM_VOLUME]; ok {
igMeta[openstack.BOOT_FROM_VOLUME] = e
}
if v, ok := ig.ObjectMeta.Annotations[openstack.OS_ANNOTATION+openstack.BOOT_VOLUME_SIZE]; ok {
igMeta[openstack.BOOT_VOLUME_SIZE] = v
}
startupScript, err := b.BootstrapScript.ResourceNodeUp(ig, b.Cluster)
if err != nil {
return fmt.Errorf("Could not create startup script for instance group %s: %v", ig.Name, err)

View File

@ -8,6 +8,7 @@ go_library(
"cloud.go",
"dns.go",
"floatingip.go",
"image.go",
"instance.go",
"keypair.go",
"loadbalancer.go",
@ -44,6 +45,7 @@ go_library(
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/dns/v2/zones:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors:go_default_library",

View File

@ -23,8 +23,6 @@ import (
"strings"
"time"
"k8s.io/kops/pkg/dns"
"github.com/gophercloud/gophercloud"
os "github.com/gophercloud/gophercloud/openstack"
cinder "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
@ -36,6 +34,7 @@ import (
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners"
"github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/loadbalancers"
"github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/monitors"
@ -54,6 +53,7 @@ import (
"k8s.io/kops/dnsprovider/pkg/dnsprovider/providers/openstack/designate"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/cloudinstances"
"k8s.io/kops/pkg/dns"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/util/pkg/vfs"
)
@ -274,6 +274,8 @@ type OpenstackCloud interface {
GetFloatingIP(id string) (fip *floatingips.FloatingIP, err error)
GetImage(name string) (i *images.Image, err error)
AssociateFloatingIPToInstance(serverID string, opts floatingips.AssociateOpts) (err error)
ListServerFloatingIPs(id string) ([]*string, error)
@ -292,6 +294,7 @@ type openstackCloud struct {
novaClient *gophercloud.ServiceClient
dnsClient *gophercloud.ServiceClient
lbClient *gophercloud.ServiceClient
glanceClient *gophercloud.ServiceClient
floatingEnabled bool
extNetworkName *string
extSubnetName *string
@ -368,6 +371,14 @@ func NewOpenstackCloud(tags map[string]string, spec *kops.ClusterSpec) (Openstac
return nil, fmt.Errorf("error building nova client: %v", err)
}
glanceClient, err := os.NewImageServiceV2(provider, gophercloud.EndpointOpts{
Type: "image",
Region: region,
})
if err != nil {
return nil, fmt.Errorf("error building glance client: %v", err)
}
var dnsClient *gophercloud.ServiceClient
if !dns.IsGossipHostname(tags[TagClusterName]) {
//TODO: This should be replaced with the environment variable methods as done above
@ -387,6 +398,7 @@ func NewOpenstackCloud(tags map[string]string, spec *kops.ClusterSpec) (Openstac
neutronClient: neutronClient,
novaClient: novaClient,
dnsClient: dnsClient,
glanceClient: glanceClient,
tags: tags,
region: region,
useOctavia: false,

View File

@ -0,0 +1,46 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package openstack
import (
"fmt"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
)
func (c *openstackCloud) GetImage(name string) (*images.Image, error) {
opts := images.ListOpts{Name: name}
pager := images.List(c.glanceClient, opts)
page, err := pager.AllPages()
if err != nil {
return nil, fmt.Errorf("failed to list images: %v", err)
}
i, err := images.ExtractImages(page)
if err != nil {
return nil, fmt.Errorf("failed to extract images: %v", err)
}
switch len(i) {
case 1:
return &i[0], nil
case 0:
return nil, fmt.Errorf("no image found with name %v", name)
default:
return nil, fmt.Errorf("multiple images found with name %v", name)
}
}

View File

@ -32,6 +32,9 @@ import (
const (
INSTANCE_GROUP_GENERATION = "ig_generation"
CLUSTER_GENERATION = "cluster_generation"
OS_ANNOTATION = "openstack.kops.io/"
BOOT_FROM_VOLUME = "osVolumeBoot"
BOOT_VOLUME_SIZE = "osVolumeSize"
)
// floatingBackoff is the backoff strategy for listing openstack floatingips

View File

@ -45,6 +45,7 @@ go_library(
"//util/pkg/vfs:go_default_library",
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints:go_default_library",

View File

@ -18,7 +18,9 @@ package openstacktasks
import (
"fmt"
"strconv"
"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/extensions/schedulerhints"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
@ -183,7 +185,13 @@ func (_ *Instance) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, change
Group: *e.ServerGroup.ID,
},
}
v, err := t.Cloud.CreateInstance(sgext)
opts, err := includeBootVolumeOptions(t, e, sgext)
if err != nil {
return err
}
v, err := t.Cloud.CreateInstance(opts)
if err != nil {
return fmt.Errorf("Error creating instance: %v", err)
}
@ -198,3 +206,51 @@ func (_ *Instance) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, change
klog.V(2).Infof("Openstack task Instance::RenderOpenstack did nothing")
return nil
}
func includeBootVolumeOptions(t *openstack.OpenstackAPITarget, e *Instance, opts servers.CreateOptsBuilder) (servers.CreateOptsBuilder, error) {
if !bootFromVolume(e.Metadata) {
return opts, nil
}
i, err := t.Cloud.GetImage(fi.StringValue(e.Image))
if err != nil {
return nil, fmt.Errorf("Error getting image information: %v", err)
}
bfv := bootfromvolume.CreateOptsExt{
CreateOptsBuilder: opts,
BlockDevice: []bootfromvolume.BlockDevice{{
BootIndex: 0,
DeleteOnTermination: true,
DestinationType: "volume",
SourceType: "image",
UUID: i.ID,
VolumeSize: i.MinDiskGigabytes,
}},
}
if s, ok := e.Metadata[openstack.BOOT_VOLUME_SIZE]; ok {
i, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return nil, fmt.Errorf("Invalid value for %v: %v", openstack.BOOT_VOLUME_SIZE, err)
}
bfv.BlockDevice[0].VolumeSize = int(i)
}
return bfv, nil
}
func bootFromVolume(m map[string]string) bool {
v, ok := m[openstack.BOOT_FROM_VOLUME]
if !ok {
return false
}
switch v {
case "true", "enabled":
return true
default:
return false
}
}

View File

@ -0,0 +1,12 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"pkg.go",
"util.go",
],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/internal",
importpath = "github.com/gophercloud/gophercloud/internal",
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1 @@
package internal

View File

@ -0,0 +1,34 @@
package internal
import (
"reflect"
"strings"
)
// RemainingKeys will inspect a struct and compare it to a map. Any struct
// field that does not have a JSON tag that matches a key in the map or
// a matching lower-case field in the map will be returned as an extra.
//
// This is useful for determining the extra fields returned in response bodies
// for resources that can contain an arbitrary or dynamic number of fields.
func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) {
extras = make(map[string]interface{})
for k, v := range m {
extras[k] = v
}
valueOf := reflect.ValueOf(s)
typeOf := reflect.TypeOf(s)
for i := 0; i < valueOf.NumField(); i++ {
field := typeOf.Field(i)
lowerField := strings.ToLower(field.Name)
delete(extras, lowerField)
if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" {
delete(extras, tagValue)
}
}
return
}

View File

@ -0,0 +1,18 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"requests.go",
"results.go",
"urls.go",
],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
importpath = "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
],
)

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,128 @@
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"`
// DeviceType specifies the device type of the block devices.
// Examples of this are disk, cdrom, floppy, lun, etc.
DeviceType string `json:"device_type,omitempty"`
// DiskBus is the bus type of the block devices.
// Examples of this are ide, usb, virtio, scsi, etc.
DiskBus string `json:"disk_bus,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")
}

View File

@ -0,0 +1,21 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"requests.go",
"results.go",
"types.go",
"urls.go",
],
importmap = "k8s.io/kops/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images",
importpath = "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/internal:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/utils:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
],
)

View File

@ -0,0 +1,60 @@
/*
Package images enables management and retrieval of images from the OpenStack
Image Service.
Example to List Images
images.ListOpts{
Owner: "a7509e1ae65945fda83f3e52c6296017",
}
allPages, err := images.List(imagesClient, listOpts).AllPages()
if err != nil {
panic(err)
}
allImages, err := images.ExtractImages(allPages)
if err != nil {
panic(err)
}
for _, image := range allImages {
fmt.Printf("%+v\n", image)
}
Example to Create an Image
createOpts := images.CreateOpts{
Name: "image_name",
Visibility: images.ImageVisibilityPrivate,
}
image, err := images.Create(imageClient, createOpts)
if err != nil {
panic(err)
}
Example to Update an Image
imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27"
updateOpts := images.UpdateOpts{
images.ReplaceImageName{
NewName: "new_name",
},
}
image, err := images.Update(imageClient, imageID, updateOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete an Image
imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27"
err := images.Delete(imageClient, imageID).ExtractErr()
if err != nil {
panic(err)
}
*/
package images

View File

@ -0,0 +1,366 @@
package images
import (
"fmt"
"net/url"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToImageListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to
// the server attributes you want to see returned. Marker and Limit are used
// for pagination.
//
// http://developer.openstack.org/api-ref-image-v2.html
type ListOpts struct {
// ID is the ID of the image.
// Multiple IDs can be specified by constructing a string
// such as "in:uuid1,uuid2,uuid3".
ID string `q:"id"`
// Integer value for the limit of values to return.
Limit int `q:"limit"`
// UUID of the server at which you want to set a marker.
Marker string `q:"marker"`
// Name filters on the name of the image.
// Multiple names can be specified by constructing a string
// such as "in:name1,name2,name3".
Name string `q:"name"`
// Visibility filters on the visibility of the image.
Visibility ImageVisibility `q:"visibility"`
// MemberStatus filters on the member status of the image.
MemberStatus ImageMemberStatus `q:"member_status"`
// Owner filters on the project ID of the image.
Owner string `q:"owner"`
// Status filters on the status of the image.
// Multiple statuses can be specified by constructing a string
// such as "in:saving,queued".
Status ImageStatus `q:"status"`
// SizeMin filters on the size_min image property.
SizeMin int64 `q:"size_min"`
// SizeMax filters on the size_max image property.
SizeMax int64 `q:"size_max"`
// Sort sorts the results using the new style of sorting. See the OpenStack
// Image API reference for the exact syntax.
//
// Sort cannot be used with the classic sort options (sort_key and sort_dir).
Sort string `q:"sort"`
// SortKey will sort the results based on a specified image property.
SortKey string `q:"sort_key"`
// SortDir will sort the list results either ascending or decending.
SortDir string `q:"sort_dir"`
// Tags filters on specific image tags.
Tags []string `q:"tag"`
// CreatedAtQuery filters images based on their creation date.
CreatedAtQuery *ImageDateQuery
// UpdatedAtQuery filters images based on their updated date.
UpdatedAtQuery *ImageDateQuery
// ContainerFormat filters images based on the container_format.
// Multiple container formats can be specified by constructing a
// string such as "in:bare,ami".
ContainerFormat string `q:"container_format"`
// DiskFormat filters images based on the disk_format.
// Multiple disk formats can be specified by constructing a string
// such as "in:qcow2,iso".
DiskFormat string `q:"disk_format"`
}
// ToImageListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToImageListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
params := q.Query()
if opts.CreatedAtQuery != nil {
createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339)
if v := opts.CreatedAtQuery.Filter; v != "" {
createdAt = fmt.Sprintf("%s:%s", v, createdAt)
}
params.Add("created_at", createdAt)
}
if opts.UpdatedAtQuery != nil {
updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339)
if v := opts.UpdatedAtQuery.Filter; v != "" {
updatedAt = fmt.Sprintf("%s:%s", v, updatedAt)
}
params.Add("updated_at", updatedAt)
}
q = &url.URL{RawQuery: params.Encode()}
return q.String(), err
}
// List implements image list request.
func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(c)
if opts != nil {
query, err := opts.ToImageListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
imagePage := ImagePage{
serviceURL: c.ServiceURL(),
LinkedPageBase: pagination.LinkedPageBase{PageResult: r},
}
return imagePage
})
}
// CreateOptsBuilder allows extensions to add parameters to the Create request.
type CreateOptsBuilder interface {
// Returns value that can be passed to json.Marshal
ToImageCreateMap() (map[string]interface{}, error)
}
// CreateOpts represents options used to create an image.
type CreateOpts struct {
// Name is the name of the new image.
Name string `json:"name" required:"true"`
// Id is the the image ID.
ID string `json:"id,omitempty"`
// Visibility defines who can see/use the image.
Visibility *ImageVisibility `json:"visibility,omitempty"`
// Tags is a set of image tags.
Tags []string `json:"tags,omitempty"`
// ContainerFormat is the format of the
// container. Valid values are ami, ari, aki, bare, and ovf.
ContainerFormat string `json:"container_format,omitempty"`
// DiskFormat is the format of the disk. If set,
// valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi,
// and iso.
DiskFormat string `json:"disk_format,omitempty"`
// MinDisk is the amount of disk space in
// GB that is required to boot the image.
MinDisk int `json:"min_disk,omitempty"`
// MinRAM is the amount of RAM in MB that
// is required to boot the image.
MinRAM int `json:"min_ram,omitempty"`
// protected is whether the image is not deletable.
Protected *bool `json:"protected,omitempty"`
// properties is a set of properties, if any, that
// are associated with the image.
Properties map[string]string `json:"-"`
}
// ToImageCreateMap assembles a request body based on the contents of
// a CreateOpts.
func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil {
return nil, err
}
if opts.Properties != nil {
for k, v := range opts.Properties {
b[k] = v
}
}
return b, nil
}
// Create implements create image request.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToImageCreateMap()
if err != nil {
r.Err = err
return r
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}})
return
}
// Delete implements image delete request.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// Get implements image get request.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// Update implements image updated request.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToImageUpdateMap()
if err != nil {
r.Err = err
return r
}
_, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"},
})
return
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateOptsBuilder interface {
// returns value implementing json.Marshaler which when marshaled matches
// the patch schema:
// http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html
ToImageUpdateMap() ([]interface{}, error)
}
// UpdateOpts implements UpdateOpts
type UpdateOpts []Patch
// ToImageUpdateMap assembles a request body based on the contents of
// UpdateOpts.
func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) {
m := make([]interface{}, len(opts))
for i, patch := range opts {
patchJSON := patch.ToImagePatchMap()
m[i] = patchJSON
}
return m, nil
}
// Patch represents a single update to an existing image. Multiple updates
// to an image can be submitted at the same time.
type Patch interface {
ToImagePatchMap() map[string]interface{}
}
// UpdateVisibility represents an updated visibility property request.
type UpdateVisibility struct {
Visibility ImageVisibility
}
// ToImagePatchMap assembles a request body based on UpdateVisibility.
func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/visibility",
"value": r.Visibility,
}
}
// ReplaceImageName represents an updated image_name property request.
type ReplaceImageName struct {
NewName string
}
// ToImagePatchMap assembles a request body based on ReplaceImageName.
func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/name",
"value": r.NewName,
}
}
// ReplaceImageChecksum represents an updated checksum property request.
type ReplaceImageChecksum struct {
Checksum string
}
// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum.
func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/checksum",
"value": r.Checksum,
}
}
// ReplaceImageTags represents an updated tags property request.
type ReplaceImageTags struct {
NewTags []string
}
// ToImagePatchMap assembles a request body based on ReplaceImageTags.
func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/tags",
"value": r.NewTags,
}
}
// ReplaceImageMinDisk represents an updated min_disk property request.
type ReplaceImageMinDisk struct {
NewMinDisk int
}
// ToImagePatchMap assembles a request body based on ReplaceImageTags.
func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/min_disk",
"value": r.NewMinDisk,
}
}
// UpdateOp represents a valid update operation.
type UpdateOp string
const (
AddOp UpdateOp = "add"
ReplaceOp UpdateOp = "replace"
RemoveOp UpdateOp = "remove"
)
// UpdateImageProperty represents an update property request.
type UpdateImageProperty struct {
Op UpdateOp
Name string
Value string
}
// ToImagePatchMap assembles a request body based on UpdateImageProperty.
func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} {
updateMap := map[string]interface{}{
"op": r.Op,
"path": fmt.Sprintf("/%s", r.Name),
}
if r.Value != "" {
updateMap["value"] = r.Value
}
return updateMap
}

View File

@ -0,0 +1,202 @@
package images
import (
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/internal"
"github.com/gophercloud/gophercloud/pagination"
)
// Image represents an image found in the OpenStack Image service.
type Image struct {
// ID is the image UUID.
ID string `json:"id"`
// Name is the human-readable display name for the image.
Name string `json:"name"`
// Status is the image status. It can be "queued" or "active"
// See imageservice/v2/images/type.go
Status ImageStatus `json:"status"`
// Tags is a list of image tags. Tags are arbitrarily defined strings
// attached to an image.
Tags []string `json:"tags"`
// ContainerFormat is the format of the container.
// Valid values are ami, ari, aki, bare, and ovf.
ContainerFormat string `json:"container_format"`
// DiskFormat is the format of the disk.
// If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi,
// and iso.
DiskFormat string `json:"disk_format"`
// MinDiskGigabytes is the amount of disk space in GB that is required to
// boot the image.
MinDiskGigabytes int `json:"min_disk"`
// MinRAMMegabytes [optional] is the amount of RAM in MB that is required to
// boot the image.
MinRAMMegabytes int `json:"min_ram"`
// Owner is the tenant ID the image belongs to.
Owner string `json:"owner"`
// Protected is whether the image is deletable or not.
Protected bool `json:"protected"`
// Visibility defines who can see/use the image.
Visibility ImageVisibility `json:"visibility"`
// Checksum is the checksum of the data that's associated with the image.
Checksum string `json:"checksum"`
// SizeBytes is the size of the data that's associated with the image.
SizeBytes int64 `json:"-"`
// Metadata is a set of metadata associated with the image.
// Image metadata allow for meaningfully define the image properties
// and tags.
// See http://docs.openstack.org/developer/glance/metadefs-concepts.html.
Metadata map[string]string `json:"metadata"`
// Properties is a set of key-value pairs, if any, that are associated with
// the image.
Properties map[string]interface{}
// CreatedAt is the date when the image has been created.
CreatedAt time.Time `json:"created_at"`
// UpdatedAt is the date when the last change has been made to the image or
// it's properties.
UpdatedAt time.Time `json:"updated_at"`
// File is the trailing path after the glance endpoint that represent the
// location of the image or the path to retrieve it.
File string `json:"file"`
// Schema is the path to the JSON-schema that represent the image or image
// entity.
Schema string `json:"schema"`
// VirtualSize is the virtual size of the image
VirtualSize int64 `json:"virtual_size"`
}
func (r *Image) UnmarshalJSON(b []byte) error {
type tmp Image
var s struct {
tmp
SizeBytes interface{} `json:"size"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Image(s.tmp)
switch t := s.SizeBytes.(type) {
case nil:
r.SizeBytes = 0
case float32:
r.SizeBytes = int64(t)
case float64:
r.SizeBytes = int64(t)
default:
return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t)
}
// Bundle all other fields into Properties
var result interface{}
err = json.Unmarshal(b, &result)
if err != nil {
return err
}
if resultMap, ok := result.(map[string]interface{}); ok {
delete(resultMap, "self")
delete(resultMap, "size")
r.Properties = internal.RemainingKeys(Image{}, resultMap)
}
return err
}
type commonResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as an Image.
func (r commonResult) Extract() (*Image, error) {
var s *Image
err := r.ExtractInto(&s)
return s, err
}
// CreateResult represents the result of a Create operation. Call its Extract
// method to interpret it as an Image.
type CreateResult struct {
commonResult
}
// UpdateResult represents the result of an Update operation. Call its Extract
// method to interpret it as an Image.
type UpdateResult struct {
commonResult
}
// GetResult represents the result of a Get operation. Call its Extract
// method to interpret it as an Image.
type GetResult struct {
commonResult
}
// DeleteResult represents the result of a Delete operation. Call its
// ExtractErr method to interpret it as an Image.
type DeleteResult struct {
gophercloud.ErrResult
}
// ImagePage represents the results of a List request.
type ImagePage struct {
serviceURL string
pagination.LinkedPageBase
}
// IsEmpty returns true if an ImagePage contains no Images results.
func (r ImagePage) IsEmpty() (bool, error) {
images, err := ExtractImages(r)
return len(images) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to
// the next page of results.
func (r ImagePage) NextPageURL() (string, error) {
var s struct {
Next string `json:"next"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
if s.Next == "" {
return "", nil
}
return nextPageURL(r.serviceURL, s.Next)
}
// ExtractImages interprets the results of a single page from a List() call,
// producing a slice of Image entities.
func ExtractImages(r pagination.Page) ([]Image, error) {
var s struct {
Images []Image `json:"images"`
}
err := (r.(ImagePage)).ExtractInto(&s)
return s.Images, err
}

View File

@ -0,0 +1,104 @@
package images
import (
"time"
)
// ImageStatus image statuses
// http://docs.openstack.org/developer/glance/statuses.html
type ImageStatus string
const (
// ImageStatusQueued is a status for an image which identifier has
// been reserved for an image in the image registry.
ImageStatusQueued ImageStatus = "queued"
// ImageStatusSaving denotes that an images raw data is currently being
// uploaded to Glance
ImageStatusSaving ImageStatus = "saving"
// ImageStatusActive denotes an image that is fully available in Glance.
ImageStatusActive ImageStatus = "active"
// ImageStatusKilled denotes that an error occurred during the uploading
// of an images data, and that the image is not readable.
ImageStatusKilled ImageStatus = "killed"
// ImageStatusDeleted is used for an image that is no longer available to use.
// The image information is retained in the image registry.
ImageStatusDeleted ImageStatus = "deleted"
// ImageStatusPendingDelete is similar to Delete, but the image is not yet
// deleted.
ImageStatusPendingDelete ImageStatus = "pending_delete"
// ImageStatusDeactivated denotes that access to image data is not allowed to
// any non-admin user.
ImageStatusDeactivated ImageStatus = "deactivated"
)
// ImageVisibility denotes an image that is fully available in Glance.
// This occurs when the image data is uploaded, or the image size is explicitly
// set to zero on creation.
// According to design
// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design
type ImageVisibility string
const (
// ImageVisibilityPublic all users
ImageVisibilityPublic ImageVisibility = "public"
// ImageVisibilityPrivate users with tenantId == tenantId(owner)
ImageVisibilityPrivate ImageVisibility = "private"
// ImageVisibilityShared images are visible to:
// - users with tenantId == tenantId(owner)
// - users with tenantId in the member-list of the image
// - users with tenantId in the member-list with member_status == 'accepted'
ImageVisibilityShared ImageVisibility = "shared"
// ImageVisibilityCommunity images:
// - all users can see and boot it
// - users with tenantId in the member-list of the image with
// member_status == 'accepted' have this image in their default image-list.
ImageVisibilityCommunity ImageVisibility = "community"
)
// MemberStatus is a status for adding a new member (tenant) to an image
// member list.
type ImageMemberStatus string
const (
// ImageMemberStatusAccepted is the status for an accepted image member.
ImageMemberStatusAccepted ImageMemberStatus = "accepted"
// ImageMemberStatusPending shows that the member addition is pending
ImageMemberStatusPending ImageMemberStatus = "pending"
// ImageMemberStatusAccepted is the status for a rejected image member
ImageMemberStatusRejected ImageMemberStatus = "rejected"
// ImageMemberStatusAll
ImageMemberStatusAll ImageMemberStatus = "all"
)
// ImageDateFilter represents a valid filter to use for filtering
// images by their date during a List.
type ImageDateFilter string
const (
FilterGT ImageDateFilter = "gt"
FilterGTE ImageDateFilter = "gte"
FilterLT ImageDateFilter = "lt"
FilterLTE ImageDateFilter = "lte"
FilterNEQ ImageDateFilter = "neq"
FilterEQ ImageDateFilter = "eq"
)
// ImageDateQuery represents a date field to be used for listing images.
// If no filter is specified, the query will act as though FilterEQ was
// set.
type ImageDateQuery struct {
Date time.Time
Filter ImageDateFilter
}

View File

@ -0,0 +1,65 @@
package images
import (
"net/url"
"strings"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/utils"
)
// `listURL` is a pure function. `listURL(c)` is a URL for which a GET
// request will respond with a list of images in the service `c`.
func listURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("images")
}
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("images")
}
// `imageURL(c,i)` is the URL for the image identified by ID `i` in
// the service `c`.
func imageURL(c *gophercloud.ServiceClient, imageID string) string {
return c.ServiceURL("images", imageID)
}
// `getURL(c,i)` is a URL for which a GET request will respond with
// information about the image identified by ID `i` in the service
// `c`.
func getURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
func updateURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
func deleteURL(c *gophercloud.ServiceClient, imageID string) string {
return imageURL(c, imageID)
}
// builds next page full url based on current url
func nextPageURL(serviceURL, requestedNext string) (string, error) {
base, err := utils.BaseEndpoint(serviceURL)
if err != nil {
return "", err
}
requestedNextURL, err := url.Parse(requestedNext)
if err != nil {
return "", err
}
base = gophercloud.NormalizeURL(base)
nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/")
nextURL, err := url.Parse(nextPath)
if err != nil {
return "", err
}
nextURL.RawQuery = requestedNextURL.RawQuery
return nextURL.String(), nil
}

3
vendor/modules.txt vendored
View File

@ -252,9 +252,11 @@ github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/servergroups
github.com/gophercloud/gophercloud/openstack/compute/v2/flavors
github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners
github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/pools
github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/schedulerhints
github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers
github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects
@ -262,6 +264,7 @@ github.com/gophercloud/gophercloud/openstack/identity/v2/tokens
github.com/gophercloud/gophercloud/openstack/identity/v3/tokens
github.com/gophercloud/gophercloud/openstack/utils
github.com/gophercloud/gophercloud/openstack/compute/v2/images
github.com/gophercloud/gophercloud/internal
github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies
github.com/gophercloud/gophercloud/openstack/objectstorage/v1/accounts
github.com/gophercloud/gophercloud/openstack/identity/v2/tenants