add keypair role

This commit is contained in:
Jesse Haka 2018-05-05 14:09:52 +03:00
parent 02daea62ae
commit ed8d03d402
31 changed files with 3188 additions and 1 deletions

6
Gopkg.lock generated
View File

@ -483,6 +483,10 @@
".",
"openstack",
"openstack/blockstorage/v2/volumes",
"openstack/compute/v2/extensions/keypairs",
"openstack/compute/v2/flavors",
"openstack/compute/v2/images",
"openstack/compute/v2/servers",
"openstack/identity/v2/tenants",
"openstack/identity/v2/tokens",
"openstack/identity/v3/tokens",
@ -1706,6 +1710,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "33c4809d9ffb5425549fab80b4edb6a9fa84726ca461e8caa314b12b27f61a8d"
inputs-digest = "4708af635b2bbadafcb67be11520a8a6b34557b2f2ad2fec6f80b39a5ca646ad"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -6,6 +6,7 @@ go_library(
"context.go",
"convenience.go",
"network.go",
"sshkey.go",
],
importpath = "k8s.io/kops/pkg/model/openstackmodel",
visibility = ["//visibility:public"],

View File

@ -0,0 +1,45 @@
/*
Copyright 2016 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 openstackmodel
import (
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/openstacktasks"
)
// SSHKeyModelBuilder configures SSH objects
type SSHKeyModelBuilder struct {
*OpenstackModelContext
Lifecycle *fi.Lifecycle
}
var _ fi.ModelBuilder = &SSHKeyModelBuilder{}
func (b *SSHKeyModelBuilder) Build(c *fi.ModelBuilderContext) error {
name, err := b.SSHKeyName()
if err != nil {
return err
}
t := &openstacktasks.SSHKey{
Name: s(name),
Lifecycle: b.Lifecycle,
PublicKey: fi.WrapResource(fi.NewStringResource(string(b.SSHPublicKeys[0]))),
}
c.AddTask(t)
return nil
}

View File

@ -463,10 +463,21 @@ func (c *ApplyClusterCmd) Run() error {
region = osCloud.Region()
l.AddTypes(map[string]interface{}{
"sshKey": &openstacktasks.SSHKey{},
// Networking
"network": &openstacktasks.Network{},
"router": &openstacktasks.Router{},
})
if len(sshPublicKeys) == 0 {
return fmt.Errorf("SSH public key must be specified when running with Openstack (create with `kops create secret --name %s sshpublickey admin -i ~/.ssh/id_rsa.pub`)", cluster.ObjectMeta.Name)
}
modelContext.SSHPublicKeys = sshPublicKeys
if len(sshPublicKeys) != 1 {
return fmt.Errorf("Exactly one 'admin' SSH public key can be specified when running with Openstack; please delete a key using `kops delete secret`")
}
}
default:
return fmt.Errorf("unknown CloudProvider %q", cluster.Spec.CloudProvider)
@ -604,6 +615,7 @@ func (c *ApplyClusterCmd) Run() error {
l.Builders = append(l.Builders,
&openstackmodel.NetworkModelBuilder{OpenstackModelContext: openstackModelContext, Lifecycle: &networkLifecycle},
&openstackmodel.SSHKeyModelBuilder{OpenstackModelContext: openstackModelContext, Lifecycle: &securityLifecycle},
)
default:

View File

@ -18,6 +18,7 @@ go_library(
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules:go_default_library",

View File

@ -24,6 +24,7 @@ import (
"github.com/gophercloud/gophercloud"
os "github.com/gophercloud/gophercloud/openstack"
cinder "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers"
sg "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups"
sgr "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules"
@ -42,6 +43,9 @@ const TagNameEtcdClusterPrefix = "k8s.io/etcd/"
const TagNameRolePrefix = "k8s.io/role/"
const TagClusterName = "KubernetesCluster"
// ErrNotFound is used to inform that the object is not found
var ErrNotFound = "Resource not found"
// readBackoff is the backoff strategy for openstack read retries.
var readBackoff = wait.Backoff{
Duration: time.Second,
@ -105,11 +109,18 @@ type OpenstackCloud interface {
//CreateSubnet will create a new Neutron subnet
CreateSubnet(opt subnets.CreateOptsBuilder) (*subnets.Subnet, error)
// ListKeypair will return the Nova keypairs
ListKeypair(name string) (*keypairs.KeyPair, error)
// CreateKeypair will create a new Nova Keypair
CreateKeypair(opt keypairs.CreateOptsBuilder) (*keypairs.KeyPair, error)
}
type openstackCloud struct {
cinderClient *gophercloud.ServiceClient
neutronClient *gophercloud.ServiceClient
novaClient *gophercloud.ServiceClient
tags map[string]string
region string
}
@ -145,11 +156,22 @@ func NewOpenstackCloud(tags map[string]string) (OpenstackCloud, error) {
if err != nil {
return nil, fmt.Errorf("error building neutron client: %v", err)
}
endpointOpt, err = config.GetServiceConfig("Nova")
if err != nil {
return nil, err
}
novaClient, err := os.NewComputeV2(provider, endpointOpt)
if err != nil {
return nil, fmt.Errorf("error building nova client: %v", err)
}
region := endpointOpt.Region
c := &openstackCloud{
cinderClient: cinderClient,
neutronClient: neutronClient,
novaClient: novaClient,
tags: tags,
region: region,
}
@ -483,3 +505,45 @@ func (c *openstackCloud) CreateSubnet(opt subnets.CreateOptsBuilder) (*subnets.S
return s, wait.ErrWaitTimeout
}
}
func (c *openstackCloud) ListKeypair(name string) (*keypairs.KeyPair, error) {
var k *keypairs.KeyPair
done, err := vfs.RetryWithBackoff(readBackoff, func() (bool, error) {
rs, err := keypairs.Get(c.novaClient, name).Extract()
if err != nil {
if err.Error() == ErrNotFound {
return true, nil
}
return false, fmt.Errorf("error listing keypair: %v", err)
}
k = rs
return true, nil
})
if err != nil {
return k, err
} else if done {
return k, nil
} else {
return k, wait.ErrWaitTimeout
}
}
func (c *openstackCloud) CreateKeypair(opt keypairs.CreateOptsBuilder) (*keypairs.KeyPair, error) {
var k *keypairs.KeyPair
done, err := vfs.RetryWithBackoff(writeBackoff, func() (bool, error) {
v, err := keypairs.Create(c.novaClient, opt).Extract()
if err != nil {
return false, fmt.Errorf("error creating keypair: %v", err)
}
k = v
return true, nil
})
if err != nil {
return k, err
} else if done {
return k, nil
} else {
return k, wait.ErrWaitTimeout
}
}

View File

@ -10,6 +10,8 @@ go_library(
"securitygroup.go",
"securitygroup_fitask.go",
"securitygrouprule.go",
"sshkey.go",
"sshkey_fitask.go",
"subnet.go",
"subnet_fitask.go",
"volume.go",
@ -18,11 +20,13 @@ go_library(
importpath = "k8s.io/kops/upup/pkg/fi/cloudup/openstacktasks",
visibility = ["//visibility:public"],
deps = [
"//pkg/pki:go_default_library",
"//upup/pkg/fi:go_default_library",
"//upup/pkg/fi/cloudup/openstack:go_default_library",
"//vendor/github.com/golang/glog: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/keypairs:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules:go_default_library",

View File

@ -0,0 +1,132 @@
/*
Copyright 2016 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 openstacktasks
import (
"fmt"
"strings"
"github.com/golang/glog"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"k8s.io/kops/pkg/pki"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack"
)
//go:generate fitask -type=SSHKey
type SSHKey struct {
Name *string
Lifecycle *fi.Lifecycle
PublicKey *fi.ResourceHolder
KeyFingerprint *string
}
var _ fi.CompareWithID = &SSHKey{}
func (e *SSHKey) CompareWithID() *string {
return e.Name
}
func (e *SSHKey) Find(c *fi.Context) (*SSHKey, error) {
cloud := c.Cloud.(openstack.OpenstackCloud)
rs, err := cloud.ListKeypair(openstackKeyPairName(fi.StringValue(e.Name)))
if err != nil {
return nil, err
}
if rs == nil {
return nil, nil
}
actual := &SSHKey{
Name: e.Name,
KeyFingerprint: fi.String(rs.Fingerprint),
}
// Avoid spurious changes
if fi.StringValue(actual.KeyFingerprint) == fi.StringValue(e.KeyFingerprint) {
glog.V(2).Infof("SSH key fingerprints match; assuming public keys match")
actual.PublicKey = e.PublicKey
} else {
glog.V(2).Infof("Computed SSH key fingerprint mismatch: %q %q", fi.StringValue(e.KeyFingerprint), fi.StringValue(actual.KeyFingerprint))
}
actual.Lifecycle = e.Lifecycle
return actual, nil
}
func (e *SSHKey) Run(c *fi.Context) error {
if e.KeyFingerprint == nil && e.PublicKey != nil {
publicKey, err := e.PublicKey.AsString()
if err != nil {
return fmt.Errorf("error reading SSH public key: %v", err)
}
keyFingerprint, err := pki.ComputeAWSKeyFingerprint(publicKey)
if err != nil {
return fmt.Errorf("error computing key fingerprint for SSH key: %v", err)
}
glog.V(2).Infof("Computed SSH key fingerprint as %q", keyFingerprint)
e.KeyFingerprint = &keyFingerprint
}
return fi.DefaultDeltaRunMethod(e, c)
}
func (s *SSHKey) CheckChanges(a, e, changes *SSHKey) error {
if a != nil {
if changes.Name != nil {
return fi.CannotChangeField("Name")
}
}
return nil
}
func openstackKeyPairName(org string) string {
name := strings.Replace(org, ".", "-", -1)
name = strings.Replace(name, ":", "_", -1)
return name
}
func (_ *SSHKey) RenderOpenstack(t *openstack.OpenstackAPITarget, a, e, changes *SSHKey) error {
if a == nil {
glog.V(2).Infof("Creating Keypair with name:%q", fi.StringValue(e.Name))
opt := keypairs.CreateOpts{
Name: openstackKeyPairName(fi.StringValue(e.Name)),
}
if e.PublicKey != nil {
d, err := e.PublicKey.AsString()
if err != nil {
return fmt.Errorf("error rendering SSHKey PublicKey: %v", err)
}
opt.PublicKey = d
}
v, err := t.Cloud.CreateKeypair(opt)
if err != nil {
return fmt.Errorf("Error creating keypair: %v", err)
}
e.KeyFingerprint = fi.String(v.Fingerprint)
glog.V(2).Infof("Creating a new Openstack keypair, id=%s", v.Fingerprint)
return nil
}
e.KeyFingerprint = a.KeyFingerprint
glog.V(2).Infof("Using an existing Openstack keypair, id=%s", fi.StringValue(e.KeyFingerprint))
return nil
}

View File

@ -0,0 +1,75 @@
/*
Copyright 2018 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.
*/
// Code generated by ""fitask" -type=SSHKey"; DO NOT EDIT
package openstacktasks
import (
"encoding/json"
"k8s.io/kops/upup/pkg/fi"
)
// SSHKey
// JSON marshalling boilerplate
type realSSHKey SSHKey
// UnmarshalJSON implements conversion to JSON, supporitng an alternate specification of the object as a string
func (o *SSHKey) UnmarshalJSON(data []byte) error {
var jsonName string
if err := json.Unmarshal(data, &jsonName); err == nil {
o.Name = &jsonName
return nil
}
var r realSSHKey
if err := json.Unmarshal(data, &r); err != nil {
return err
}
*o = SSHKey(r)
return nil
}
var _ fi.HasLifecycle = &SSHKey{}
// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle
func (o *SSHKey) GetLifecycle() *fi.Lifecycle {
return o.Lifecycle
}
// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle
func (o *SSHKey) SetLifecycle(lifecycle fi.Lifecycle) {
o.Lifecycle = &lifecycle
}
var _ fi.HasName = &SSHKey{}
// GetName returns the Name of the object, implementing fi.HasName
func (o *SSHKey) GetName() *string {
return o.Name
}
// SetName sets the Name of the object, implementing fi.SetName
func (o *SSHKey) SetName(name string) {
o.Name = &name
}
// String is the stringer function for the task, producing readable output using fi.TaskAsString
func (o *SSHKey) String() string {
return fi.TaskAsString(o)
}

View File

@ -0,0 +1,19 @@
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 = "vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs",
importpath = "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
],
)

View File

@ -0,0 +1,71 @@
/*
Package keypairs provides the ability to manage key pairs as well as create
servers with a specified key pair.
Example to List Key Pairs
allPages, err := keypairs.List(computeClient).AllPages()
if err != nil {
panic(err)
}
allKeyPairs, err := keypairs.ExtractKeyPairs(allPages)
if err != nil {
panic(err)
}
for _, kp := range allKeyPairs {
fmt.Printf("%+v\n", kp)
}
Example to Create a Key Pair
createOpts := keypairs.CreateOpts{
Name: "keypair-name",
}
keypair, err := keypairs.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v", keypair)
Example to Import a Key Pair
createOpts := keypairs.CreateOpts{
Name: "keypair-name",
PublicKey: "public-key",
}
keypair, err := keypairs.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete a Key Pair
err := keypairs.Delete(computeClient, "keypair-name").ExtractErr()
if err != nil {
panic(err)
}
Example to Create a Server With a Key Pair
serverCreateOpts := servers.CreateOpts{
Name: "server_name",
ImageRef: "image-uuid",
FlavorRef: "flavor-uuid",
}
createOpts := keypairs.CreateOpts{
CreateOptsBuilder: serverCreateOpts,
KeyName: "keypair-name",
}
server, err := servers.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
*/
package keypairs

View File

@ -0,0 +1,86 @@
package keypairs
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/pagination"
)
// CreateOptsExt adds a KeyPair option to the base CreateOpts.
type CreateOptsExt struct {
servers.CreateOptsBuilder
// KeyName is the name of the key pair.
KeyName string `json:"key_name,omitempty"`
}
// ToServerCreateMap adds the key_name 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 opts.KeyName == "" {
return base, nil
}
serverMap := base["server"].(map[string]interface{})
serverMap["key_name"] = opts.KeyName
return base, nil
}
// List returns a Pager that allows you to iterate over a collection of KeyPairs.
func List(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page {
return KeyPairPage{pagination.SinglePageBase(r)}
})
}
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToKeyPairCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies KeyPair creation or import parameters.
type CreateOpts struct {
// Name is a friendly name to refer to this KeyPair in other services.
Name string `json:"name" required:"true"`
// PublicKey [optional] is a pregenerated OpenSSH-formatted public key.
// If provided, this key will be imported and no new key will be created.
PublicKey string `json:"public_key,omitempty"`
}
// ToKeyPairCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToKeyPairCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "keypair")
}
// Create requests the creation of a new KeyPair on the server, or to import a
// pre-existing keypair.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToKeyPairCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Get returns public data about a previously uploaded KeyPair.
func Get(client *gophercloud.ServiceClient, name string) (r GetResult) {
_, r.Err = client.Get(getURL(client, name), &r.Body, nil)
return
}
// Delete requests the deletion of a previous stored KeyPair from the server.
func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, name), nil)
return
}

View File

@ -0,0 +1,91 @@
package keypairs
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// KeyPair is an SSH key known to the OpenStack Cloud that is available to be
// injected into servers.
type KeyPair struct {
// Name is used to refer to this keypair from other services within this
// region.
Name string `json:"name"`
// Fingerprint is a short sequence of bytes that can be used to authenticate
// or validate a longer public key.
Fingerprint string `json:"fingerprint"`
// PublicKey is the public key from this pair, in OpenSSH format.
// "ssh-rsa AAAAB3Nz..."
PublicKey string `json:"public_key"`
// PrivateKey is the private key from this pair, in PEM format.
// "-----BEGIN RSA PRIVATE KEY-----\nMIICXA..."
// It is only present if this KeyPair was just returned from a Create call.
PrivateKey string `json:"private_key"`
// UserID is the user who owns this KeyPair.
UserID string `json:"user_id"`
}
// KeyPairPage stores a single page of all KeyPair results from a List call.
// Use the ExtractKeyPairs function to convert the results to a slice of
// KeyPairs.
type KeyPairPage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a KeyPairPage is empty.
func (page KeyPairPage) IsEmpty() (bool, error) {
ks, err := ExtractKeyPairs(page)
return len(ks) == 0, err
}
// ExtractKeyPairs interprets a page of results as a slice of KeyPairs.
func ExtractKeyPairs(r pagination.Page) ([]KeyPair, error) {
type pair struct {
KeyPair KeyPair `json:"keypair"`
}
var s struct {
KeyPairs []pair `json:"keypairs"`
}
err := (r.(KeyPairPage)).ExtractInto(&s)
results := make([]KeyPair, len(s.KeyPairs))
for i, pair := range s.KeyPairs {
results[i] = pair.KeyPair
}
return results, err
}
type keyPairResult struct {
gophercloud.Result
}
// Extract is a method that attempts to interpret any KeyPair resource response
// as a KeyPair struct.
func (r keyPairResult) Extract() (*KeyPair, error) {
var s struct {
KeyPair *KeyPair `json:"keypair"`
}
err := r.ExtractInto(&s)
return s.KeyPair, err
}
// CreateResult is the response from a Create operation. Call its Extract method
// to interpret it as a KeyPair.
type CreateResult struct {
keyPairResult
}
// GetResult is the response from a Get operation. Call its Extract method to
// interpret it as a KeyPair.
type GetResult struct {
keyPairResult
}
// DeleteResult is the response from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}

View File

@ -0,0 +1,25 @@
package keypairs
import "github.com/gophercloud/gophercloud"
const resourcePath = "os-keypairs"
func resourceURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(resourcePath)
}
func listURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func createURL(c *gophercloud.ServiceClient) string {
return resourceURL(c)
}
func getURL(c *gophercloud.ServiceClient, name string) string {
return c.ServiceURL(resourcePath, name)
}
func deleteURL(c *gophercloud.ServiceClient, name string) string {
return getURL(c, name)
}

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 = "vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors",
importpath = "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
],
)

View File

@ -0,0 +1,137 @@
/*
Package flavors provides information and interaction with the flavor API
in the OpenStack Compute service.
A flavor is an available hardware configuration for a server. Each flavor
has a unique combination of disk space, memory capacity and priority for CPU
time.
Example to List Flavors
listOpts := flavors.ListOpts{
AccessType: flavors.PublicAccess,
}
allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages()
if err != nil {
panic(err)
}
allFlavors, err := flavors.ExtractFlavors(allPages)
if err != nil {
panic(err)
}
for _, flavor := range allFlavors {
fmt.Printf("%+v\n", flavor)
}
Example to Create a Flavor
createOpts := flavors.CreateOpts{
ID: "1",
Name: "m1.tiny",
Disk: gophercloud.IntToPointer(1),
RAM: 512,
VCPUs: 1,
RxTxFactor: 1.0,
}
flavor, err := flavors.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
Example to List Flavor Access
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages()
if err != nil {
panic(err)
}
allAccesses, err := flavors.ExtractAccesses(allPages)
if err != nil {
panic(err)
}
for _, access := range allAccesses {
fmt.Printf("%+v", access)
}
Example to Grant Access to a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
accessOpts := flavors.AddAccessOpts{
Tenant: "15153a0979884b59b0592248ef947921",
}
accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract()
if err != nil {
panic(err)
}
Example to Remove/Revoke Access to a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
accessOpts := flavors.RemoveAccessOpts{
Tenant: "15153a0979884b59b0592248ef947921",
}
accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract()
if err != nil {
panic(err)
}
Example to Create Extra Specs for a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
createOpts := flavors.ExtraSpecsOpts{
"hw:cpu_policy": "CPU-POLICY",
"hw:cpu_thread_policy": "CPU-THREAD-POLICY",
}
createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v", createdExtraSpecs)
Example to Get Extra Specs for a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v", extraSpecs)
Example to Update Extra Specs for a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
updateOpts := flavors.ExtraSpecsOpts{
"hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED",
}
updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v", updatedExtraSpec)
Example to Delete an Extra Spec for a Flavor
flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b"
err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr()
if err != nil {
panic(err)
}
*/
package flavors

View File

@ -0,0 +1,347 @@
package flavors
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToFlavorListQuery() (string, error)
}
/*
AccessType maps to OpenStack's Flavor.is_public field. Although the is_public
field is boolean, the request options are ternary, which is why AccessType is
a string. The following values are allowed:
The AccessType arguement is optional, and if it is not supplied, OpenStack
returns the PublicAccess flavors.
*/
type AccessType string
const (
// PublicAccess returns public flavors and private flavors associated with
// that project.
PublicAccess AccessType = "true"
// PrivateAccess (admin only) returns private flavors, across all projects.
PrivateAccess AccessType = "false"
// AllAccess (admin only) returns public and private flavors across all
// projects.
AllAccess AccessType = "None"
)
/*
ListOpts filters the results returned by the List() function.
For example, a flavor with a minDisk field of 10 will not be returned if you
specify MinDisk set to 20.
Typically, software will use the last ID of the previous call to List to set
the Marker for the current call.
*/
type ListOpts struct {
// ChangesSince, if provided, instructs List to return only those things which
// have changed since the timestamp provided.
ChangesSince string `q:"changes-since"`
// MinDisk and MinRAM, if provided, elides flavors which do not meet your
// criteria.
MinDisk int `q:"minDisk"`
MinRAM int `q:"minRam"`
// Marker and Limit control paging.
// Marker instructs List where to start listing from.
Marker string `q:"marker"`
// Limit instructs List to refrain from sending excessively large lists of
// flavors.
Limit int `q:"limit"`
// AccessType, if provided, instructs List which set of flavors to return.
// If IsPublic not provided, flavors for the current project are returned.
AccessType AccessType `q:"is_public"`
}
// ToFlavorListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToFlavorListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// ListDetail instructs OpenStack to provide a list of flavors.
// You may provide criteria by which List curtails its results for easier
// processing.
func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client)
if opts != nil {
query, err := opts.ToFlavorListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return FlavorPage{pagination.LinkedPageBase{PageResult: r}}
})
}
type CreateOptsBuilder interface {
ToFlavorCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies parameters used for creating a flavor.
type CreateOpts struct {
// Name is the name of the flavor.
Name string `json:"name" required:"true"`
// RAM is the memory of the flavor, measured in MB.
RAM int `json:"ram" required:"true"`
// VCPUs is the number of vcpus for the flavor.
VCPUs int `json:"vcpus" required:"true"`
// Disk the amount of root disk space, measured in GB.
Disk *int `json:"disk" required:"true"`
// ID is a unique ID for the flavor.
ID string `json:"id,omitempty"`
// Swap is the amount of swap space for the flavor, measured in MB.
Swap *int `json:"swap,omitempty"`
// RxTxFactor alters the network bandwidth of a flavor.
RxTxFactor float64 `json:"rxtx_factor,omitempty"`
// IsPublic flags a flavor as being available to all projects or not.
IsPublic *bool `json:"os-flavor-access:is_public,omitempty"`
// Ephemeral is the amount of ephemeral disk space, measured in GB.
Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"`
}
// ToFlavorCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "flavor")
}
// Create requests the creation of a new flavor.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToFlavorCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return
}
// Get retrieves details of a single flavor. Use ExtractFlavor to convert its
// result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// Delete deletes the specified flavor ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// ListAccesses retrieves the tenants which have access to a flavor.
func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager {
url := accessURL(client, id)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return AccessPage{pagination.SinglePageBase(r)}
})
}
// AddAccessOptsBuilder allows extensions to add additional parameters to the
// AddAccess requests.
type AddAccessOptsBuilder interface {
ToFlavorAddAccessMap() (map[string]interface{}, error)
}
// AddAccessOpts represents options for adding access to a flavor.
type AddAccessOpts struct {
// Tenant is the project/tenant ID to grant access.
Tenant string `json:"tenant"`
}
// ToFlavorAddAccessMap constructs a request body from AddAccessOpts.
func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "addTenantAccess")
}
// AddAccess grants a tenant/project access to a flavor.
func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) {
b, err := opts.ToFlavorAddAccessMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// RemoveAccessOptsBuilder allows extensions to add additional parameters to the
// RemoveAccess requests.
type RemoveAccessOptsBuilder interface {
ToFlavorRemoveAccessMap() (map[string]interface{}, error)
}
// RemoveAccessOpts represents options for removing access to a flavor.
type RemoveAccessOpts struct {
// Tenant is the project/tenant ID to grant access.
Tenant string `json:"tenant"`
}
// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts.
func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "removeTenantAccess")
}
// RemoveAccess removes/revokes a tenant/project access to a flavor.
func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) {
b, err := opts.ToFlavorRemoveAccessMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// ExtraSpecs requests all the extra-specs for the given flavor ID.
func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) {
_, r.Err = client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil)
return
}
func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) {
_, r.Err = client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil)
return
}
// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the
// CreateExtraSpecs requests.
type CreateExtraSpecsOptsBuilder interface {
ToExtraSpecsCreateMap() (map[string]interface{}, error)
}
// ExtraSpecsOpts is a map that contains key-value pairs.
type ExtraSpecsOpts map[string]string
// ToExtraSpecsCreateMap assembles a body for a Create request based on the
// contents of a ExtraSpecsOpts
func (opts ExtraSpecsOpts) ToExtraSpecsCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{"extra_specs": opts}, nil
}
// CreateExtraSpecs will create or update the extra-specs key-value pairs for the specified Flavor
func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) {
b, err := opts.ToExtraSpecsCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateExtraSpecOptsBuilder interface {
ToExtraSpecUpdateMap() (map[string]string, string, error)
}
// ToExtraSpecUpdateMap assembles a body for an Update request based on the
// contents of a ExtraSpecOpts.
func (opts ExtraSpecsOpts) ToExtraSpecUpdateMap() (map[string]string, string, error) {
if len(opts) != 1 {
err := gophercloud.ErrInvalidInput{}
err.Argument = "flavors.ExtraSpecOpts"
err.Info = "Must have 1 and only one key-value pair"
return nil, "", err
}
var key string
for k := range opts {
key = k
}
return opts, key, nil
}
// UpdateExtraSpec will updates the value of the specified flavor's extra spec for the key in opts.
func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) {
b, key, err := opts.ToExtraSpecUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// DeleteExtraSpec will delete the key-value pair with the given key for the given
// flavor ID.
func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) {
_, r.Err = client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// IDFromName is a convienience function that returns a flavor's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
allPages, err := ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := ExtractFlavors(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "flavor"
err.Name = name
return "", err
case 1:
return id, nil
default:
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "flavor"
err.Name = name
err.Count = count
return "", err
}
}

View File

@ -0,0 +1,252 @@
package flavors
import (
"encoding/json"
"strconv"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
type commonResult struct {
gophercloud.Result
}
// CreateResult is the response of a Get operations. Call its Extract method to
// interpret it as a Flavor.
type CreateResult struct {
commonResult
}
// GetResult is the response of a Get operations. Call its Extract method to
// interpret it as a Flavor.
type GetResult struct {
commonResult
}
// DeleteResult is the result from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// Extract provides access to the individual Flavor returned by the Get and
// Create functions.
func (r commonResult) Extract() (*Flavor, error) {
var s struct {
Flavor *Flavor `json:"flavor"`
}
err := r.ExtractInto(&s)
return s.Flavor, err
}
// Flavor represent (virtual) hardware configurations for server resources
// in a region.
type Flavor struct {
// ID is the flavor's unique ID.
ID string `json:"id"`
// Disk is the amount of root disk, measured in GB.
Disk int `json:"disk"`
// RAM is the amount of memory, measured in MB.
RAM int `json:"ram"`
// Name is the name of the flavor.
Name string `json:"name"`
// RxTxFactor describes bandwidth alterations of the flavor.
RxTxFactor float64 `json:"rxtx_factor"`
// Swap is the amount of swap space, measured in MB.
Swap int `json:"swap"`
// VCPUs indicates how many (virtual) CPUs are available for this flavor.
VCPUs int `json:"vcpus"`
// IsPublic indicates whether the flavor is public.
IsPublic bool `json:"os-flavor-access:is_public"`
// Ephemeral is the amount of ephemeral disk space, measured in GB.
Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"`
}
func (r *Flavor) UnmarshalJSON(b []byte) error {
type tmp Flavor
var s struct {
tmp
Swap interface{} `json:"swap"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Flavor(s.tmp)
switch t := s.Swap.(type) {
case float64:
r.Swap = int(t)
case string:
switch t {
case "":
r.Swap = 0
default:
swap, err := strconv.ParseFloat(t, 64)
if err != nil {
return err
}
r.Swap = int(swap)
}
}
return nil
}
// FlavorPage contains a single page of all flavors from a ListDetails call.
type FlavorPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines if a FlavorPage contains any results.
func (page FlavorPage) IsEmpty() (bool, error) {
flavors, err := ExtractFlavors(page)
return len(flavors) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (page FlavorPage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"flavors_links"`
}
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractFlavors provides access to the list of flavors in a page acquired
// from the ListDetail operation.
func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
var s struct {
Flavors []Flavor `json:"flavors"`
}
err := (r.(FlavorPage)).ExtractInto(&s)
return s.Flavors, err
}
// AccessPage contains a single page of all FlavorAccess entries for a flavor.
type AccessPage struct {
pagination.SinglePageBase
}
// IsEmpty indicates whether an AccessPage is empty.
func (page AccessPage) IsEmpty() (bool, error) {
v, err := ExtractAccesses(page)
return len(v) == 0, err
}
// ExtractAccesses interprets a page of results as a slice of FlavorAccess.
func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) {
var s struct {
FlavorAccesses []FlavorAccess `json:"flavor_access"`
}
err := (r.(AccessPage)).ExtractInto(&s)
return s.FlavorAccesses, err
}
type accessResult struct {
gophercloud.Result
}
// AddAccessResult is the response of an AddAccess operation. Call its
// Extract method to interpret it as a slice of FlavorAccess.
type AddAccessResult struct {
accessResult
}
// RemoveAccessResult is the response of a RemoveAccess operation. Call its
// Extract method to interpret it as a slice of FlavorAccess.
type RemoveAccessResult struct {
accessResult
}
// Extract provides access to the result of an access create or delete.
// The result will be all accesses that the flavor has.
func (r accessResult) Extract() ([]FlavorAccess, error) {
var s struct {
FlavorAccesses []FlavorAccess `json:"flavor_access"`
}
err := r.ExtractInto(&s)
return s.FlavorAccesses, err
}
// FlavorAccess represents an ACL of tenant access to a specific Flavor.
type FlavorAccess struct {
// FlavorID is the unique ID of the flavor.
FlavorID string `json:"flavor_id"`
// TenantID is the unique ID of the tenant.
TenantID string `json:"tenant_id"`
}
// Extract interprets any extraSpecsResult as ExtraSpecs, if possible.
func (r extraSpecsResult) Extract() (map[string]string, error) {
var s struct {
ExtraSpecs map[string]string `json:"extra_specs"`
}
err := r.ExtractInto(&s)
return s.ExtraSpecs, err
}
// extraSpecsResult contains the result of a call for (potentially) multiple
// key-value pairs. Call its Extract method to interpret it as a
// map[string]interface.
type extraSpecsResult struct {
gophercloud.Result
}
// ListExtraSpecsResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type ListExtraSpecsResult struct {
extraSpecsResult
}
// CreateExtraSpecResult contains the result of a Create operation. Call its
// Extract method to interpret it as a map[string]interface.
type CreateExtraSpecsResult struct {
extraSpecsResult
}
// extraSpecResult contains the result of a call for individual a single
// key-value pair.
type extraSpecResult struct {
gophercloud.Result
}
// GetExtraSpecResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type GetExtraSpecResult struct {
extraSpecResult
}
// UpdateExtraSpecResult contains the result of an Update operation. Call its
// Extract method to interpret it as a map[string]interface.
type UpdateExtraSpecResult struct {
extraSpecResult
}
// DeleteExtraSpecResult contains the result of a Delete operation. Call its
// ExtractErr method to determine if the call succeeded or failed.
type DeleteExtraSpecResult struct {
gophercloud.ErrResult
}
// Extract interprets any extraSpecResult as an ExtraSpec, if possible.
func (r extraSpecResult) Extract() (map[string]string, error) {
var s map[string]string
err := r.ExtractInto(&s)
return s, err
}

View File

@ -0,0 +1,49 @@
package flavors
import (
"github.com/gophercloud/gophercloud"
)
func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id)
}
func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("flavors", "detail")
}
func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("flavors")
}
func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id)
}
func accessURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "os-flavor-access")
}
func accessActionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "action")
}
func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "os-extra_specs")
}
func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string {
return client.ServiceURL("flavors", id, "os-extra_specs", key)
}
func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "os-extra_specs")
}
func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string {
return client.ServiceURL("flavors", id, "os-extra_specs", key)
}
func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string {
return client.ServiceURL("flavors", id, "os-extra_specs", key)
}

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 = "vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images",
importpath = "github.com/gophercloud/gophercloud/openstack/compute/v2/images",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
],
)

View File

@ -0,0 +1,32 @@
/*
Package images provides information and interaction with the images through
the OpenStack Compute service.
This API is deprecated and will be removed from a future version of the Nova
API service.
An image is a collection of files used to create or rebuild a server.
Operators provide a number of pre-built OS images by default. You may also
create custom images from cloud servers you have launched.
Example to List Images
listOpts := images.ListOpts{
Limit: 2,
}
allPages, err := images.ListDetail(computeClient, 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)
}
*/
package images

View File

@ -0,0 +1,109 @@
package images
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// ListDetail request.
type ListOptsBuilder interface {
ToImageListQuery() (string, error)
}
// ListOpts contain options filtering Images returned from a call to ListDetail.
type ListOpts struct {
// ChangesSince filters Images based on the last changed status (in date-time
// format).
ChangesSince string `q:"changes-since"`
// Limit limits the number of Images to return.
Limit int `q:"limit"`
// Mark is an Image UUID at which to set a marker.
Marker string `q:"marker"`
// Name is the name of the Image.
Name string `q:"name"`
// Server is the name of the Server (in URL format).
Server string `q:"server"`
// Status is the current status of the Image.
Status string `q:"status"`
// Type is the type of image (e.g. BASE, SERVER, ALL).
Type string `q:"type"`
}
// ToImageListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToImageListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// ListDetail enumerates the available images.
func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listDetailURL(client)
if opts != nil {
query, err := opts.ToImageListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return ImagePage{pagination.LinkedPageBase{PageResult: r}}
})
}
// Get returns data about a specific image by its ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// Delete deletes the specified image ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// IDFromName is a convienience function that returns an image's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
allPages, err := ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := ExtractImages(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "image"
err.Name = name
return "", err
case 1:
return id, nil
default:
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "image"
err.Name = name
err.Count = count
return "", err
}
}

View File

@ -0,0 +1,95 @@
package images
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult is the response from a Get operation. Call its Extract method to
// interpret it as an Image.
type GetResult struct {
gophercloud.Result
}
// DeleteResult is the result from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// Extract interprets a GetResult as an Image.
func (r GetResult) Extract() (*Image, error) {
var s struct {
Image *Image `json:"image"`
}
err := r.ExtractInto(&s)
return s.Image, err
}
// Image represents an Image returned by the Compute API.
type Image struct {
// ID is the unique ID of an image.
ID string
// Created is the date when the image was created.
Created string
// MinDisk is the minimum amount of disk a flavor must have to be able
// to create a server based on the image, measured in GB.
MinDisk int
// MinRAM is the minimum amount of RAM a flavor must have to be able
// to create a server based on the image, measured in MB.
MinRAM int
// Name provides a human-readable moniker for the OS image.
Name string
// The Progress and Status fields indicate image-creation status.
Progress int
// Status is the current status of the image.
Status string
// Update is the date when the image was updated.
Updated string
// Metadata provides free-form key/value pairs that further describe the
// image.
Metadata map[string]interface{}
}
// ImagePage contains a single page of all Images returne from a ListDetail
// operation. Use ExtractImages to convert it into a slice of usable structs.
type ImagePage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if an ImagePage contains no Image results.
func (page ImagePage) IsEmpty() (bool, error) {
images, err := ExtractImages(page)
return len(images) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (page ImagePage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"images_links"`
}
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractImages converts a page of List results into a slice of usable Image
// structs.
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,15 @@
package images
import "github.com/gophercloud/gophercloud"
func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("images", "detail")
}
func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("images", id)
}
func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("images", id)
}

View File

@ -0,0 +1,22 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "go_default_library",
srcs = [
"doc.go",
"errors.go",
"requests.go",
"results.go",
"urls.go",
"util.go",
],
importmap = "vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers",
importpath = "github.com/gophercloud/gophercloud/openstack/compute/v2/servers",
visibility = ["//visibility:public"],
deps = [
"//vendor/github.com/gophercloud/gophercloud:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/pagination:go_default_library",
],
)

View File

@ -0,0 +1,115 @@
/*
Package servers provides information and interaction with the server API
resource in the OpenStack Compute service.
A server is a virtual machine instance in the compute system. In order for
one to be provisioned, a valid flavor and image are required.
Example to List Servers
listOpts := servers.ListOpts{
AllTenants: true,
}
allPages, err := servers.List(computeClient, listOpts).AllPages()
if err != nil {
panic(err)
}
allServers, err := servers.ExtractServers(allPages)
if err != nil {
panic(err)
}
for _, server := range allServers {
fmt.Printf("%+v\n", server)
}
Example to Create a Server
createOpts := servers.CreateOpts{
Name: "server_name",
ImageRef: "image-uuid",
FlavorRef: "flavor-uuid",
}
server, err := servers.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete a Server
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
err := servers.Delete(computeClient, serverID).ExtractErr()
if err != nil {
panic(err)
}
Example to Force Delete a Server
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
err := servers.ForceDelete(computeClient, serverID).ExtractErr()
if err != nil {
panic(err)
}
Example to Reboot a Server
rebootOpts := servers.RebootOpts{
Type: servers.SoftReboot,
}
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
err := servers.Reboot(computeClient, serverID, rebootOpts).ExtractErr()
if err != nil {
panic(err)
}
Example to Rebuild a Server
rebuildOpts := servers.RebuildOpts{
Name: "new_name",
ImageID: "image-uuid",
}
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
server, err := servers.Rebuilt(computeClient, serverID, rebuildOpts).Extract()
if err != nil {
panic(err)
}
Example to Resize a Server
resizeOpts := servers.ResizeOpts{
FlavorRef: "flavor-uuid",
}
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
err := servers.Resize(computeClient, serverID, resizeOpts).ExtractErr()
if err != nil {
panic(err)
}
err = servers.ConfirmResize(computeClient, serverID).ExtractErr()
if err != nil {
panic(err)
}
Example to Snapshot a Server
snapshotOpts := servers.CreateImageOpts{
Name: "snapshot_name",
}
serverID := "d9072956-1560-487c-97f2-18bdf65ec749"
image, err := servers.CreateImage(computeClient, serverID, snapshotOpts).ExtractImageID()
if err != nil {
panic(err)
}
*/
package servers

View File

@ -0,0 +1,71 @@
package servers
import (
"fmt"
"github.com/gophercloud/gophercloud"
)
// ErrNeitherImageIDNorImageNameProvided is the error when neither the image
// ID nor the image name is provided for a server operation
type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput }
func (e ErrNeitherImageIDNorImageNameProvided) Error() string {
return "One and only one of the image ID and the image name must be provided."
}
// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor
// ID nor the flavor name is provided for a server operation
type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput }
func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string {
return "One and only one of the flavor ID and the flavor name must be provided."
}
type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput }
func (e ErrNoClientProvidedForIDByName) Error() string {
return "A service client must be provided to find a resource ID by name."
}
// ErrInvalidHowParameterProvided is the error when an unknown value is given
// for the `how` argument
type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput }
// ErrNoAdminPassProvided is the error when an administrative password isn't
// provided for a server operation
type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput }
// ErrNoImageIDProvided is the error when an image ID isn't provided for a server
// operation
type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput }
// ErrNoIDProvided is the error when a server ID isn't provided for a server
// operation
type ErrNoIDProvided struct{ gophercloud.ErrMissingInput }
// ErrServer is a generic error type for servers HTTP operations.
type ErrServer struct {
gophercloud.ErrUnexpectedResponseCode
ID string
}
func (se ErrServer) Error() string {
return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID)
}
// Error404 overrides the generic 404 error message.
func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error {
se.ErrUnexpectedResponseCode = e
return &ErrServerNotFound{se}
}
// ErrServerNotFound is the error when a 404 is received during server HTTP
// operations.
type ErrServerNotFound struct {
ErrServer
}
func (e ErrServerNotFound) Error() string {
return fmt.Sprintf("I couldn't find server [%s]", e.ID)
}

View File

@ -0,0 +1,791 @@
package servers
import (
"encoding/base64"
"encoding/json"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/pagination"
)
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToServerListQuery() (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.
type ListOpts struct {
// ChangesSince is a time/date stamp for when the server last changed status.
ChangesSince string `q:"changes-since"`
// Image is the name of the image in URL format.
Image string `q:"image"`
// Flavor is the name of the flavor in URL format.
Flavor string `q:"flavor"`
// Name of the server as a string; can be queried with regular expressions.
// Realize that ?name=bob returns both bob and bobb. If you need to match bob
// only, you can use a regular expression matching the syntax of the
// underlying database server implemented for Compute.
Name string `q:"name"`
// Status is the value of the status of the server so that you can filter on
// "ACTIVE" for example.
Status string `q:"status"`
// Host is the name of the host as a string.
Host string `q:"host"`
// Marker is a UUID of the server at which you want to set a marker.
Marker string `q:"marker"`
// Limit is an integer value for the limit of values to return.
Limit int `q:"limit"`
// AllTenants is a bool to show all tenants.
AllTenants bool `q:"all_tenants"`
// TenantID lists servers for a particular tenant.
// Setting "AllTenants = true" is required.
TenantID string `q:"tenant_id"`
}
// ToServerListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToServerListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// List makes a request against the API to list servers accessible to you.
func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listDetailURL(client)
if opts != nil {
query, err := opts.ToServerListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
}
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return ServerPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToServerCreateMap() (map[string]interface{}, error)
}
// Network is used within CreateOpts to control a new server's network
// attachments.
type Network struct {
// UUID of a network to attach to the newly provisioned server.
// Required unless Port is provided.
UUID string
// Port of a neutron network to attach to the newly provisioned server.
// Required unless UUID is provided.
Port string
// FixedIP specifies a fixed IPv4 address to be used on this network.
FixedIP string
}
// Personality is an array of files that are injected into the server at launch.
type Personality []*File
// File is used within CreateOpts and RebuildOpts to inject a file into the
// server at launch.
// File implements the json.Marshaler interface, so when a Create or Rebuild
// operation is requested, json.Marshal will call File's MarshalJSON method.
type File struct {
// Path of the file.
Path string
// Contents of the file. Maximum content size is 255 bytes.
Contents []byte
}
// MarshalJSON marshals the escaped file, base64 encoding the contents.
func (f *File) MarshalJSON() ([]byte, error) {
file := struct {
Path string `json:"path"`
Contents string `json:"contents"`
}{
Path: f.Path,
Contents: base64.StdEncoding.EncodeToString(f.Contents),
}
return json.Marshal(file)
}
// CreateOpts specifies server creation parameters.
type CreateOpts struct {
// Name is the name to assign to the newly launched server.
Name string `json:"name" required:"true"`
// ImageRef [optional; required if ImageName is not provided] is the ID or
// full URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string `json:"imageRef"`
// ImageName [optional; required if ImageRef is not provided] is the name of
// the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageName string `json:"-"`
// FlavorRef [optional; required if FlavorName is not provided] is the ID or
// full URL to the flavor that describes the server's specs.
FlavorRef string `json:"flavorRef"`
// FlavorName [optional; required if FlavorRef is not provided] is the name of
// the flavor that describes the server's specs.
FlavorName string `json:"-"`
// SecurityGroups lists the names of the security groups to which this server
// should belong.
SecurityGroups []string `json:"-"`
// UserData contains configuration information or scripts to use upon launch.
// Create will base64-encode it for you, if it isn't already.
UserData []byte `json:"-"`
// AvailabilityZone in which to launch the server.
AvailabilityZone string `json:"availability_zone,omitempty"`
// Networks dictates how this server will be attached to available networks.
// By default, the server will be attached to all isolated networks for the
// tenant.
Networks []Network `json:"-"`
// Metadata contains key-value pairs (up to 255 bytes each) to attach to the
// server.
Metadata map[string]string `json:"metadata,omitempty"`
// Personality includes files to inject into the server at launch.
// Create will base64-encode file contents for you.
Personality Personality `json:"personality,omitempty"`
// ConfigDrive enables metadata injection through a configuration drive.
ConfigDrive *bool `json:"config_drive,omitempty"`
// AdminPass sets the root user password. If not set, a randomly-generated
// password will be created and returned in the response.
AdminPass string `json:"adminPass,omitempty"`
// AccessIPv4 specifies an IPv4 address for the instance.
AccessIPv4 string `json:"accessIPv4,omitempty"`
// AccessIPv6 pecifies an IPv6 address for the instance.
AccessIPv6 string `json:"accessIPv6,omitempty"`
// ServiceClient will allow calls to be made to retrieve an image or
// flavor ID by name.
ServiceClient *gophercloud.ServiceClient `json:"-"`
}
// ToServerCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
sc := opts.ServiceClient
opts.ServiceClient = nil
b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil {
return nil, err
}
if opts.UserData != nil {
var userData string
if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil {
userData = base64.StdEncoding.EncodeToString(opts.UserData)
} else {
userData = string(opts.UserData)
}
b["user_data"] = &userData
}
if len(opts.SecurityGroups) > 0 {
securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups))
for i, groupName := range opts.SecurityGroups {
securityGroups[i] = map[string]interface{}{"name": groupName}
}
b["security_groups"] = securityGroups
}
if len(opts.Networks) > 0 {
networks := make([]map[string]interface{}, len(opts.Networks))
for i, net := range opts.Networks {
networks[i] = make(map[string]interface{})
if net.UUID != "" {
networks[i]["uuid"] = net.UUID
}
if net.Port != "" {
networks[i]["port"] = net.Port
}
if net.FixedIP != "" {
networks[i]["fixed_ip"] = net.FixedIP
}
}
b["networks"] = networks
}
// If ImageRef isn't provided, check if ImageName was provided to ascertain
// the image ID.
if opts.ImageRef == "" {
if opts.ImageName != "" {
if sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
imageID, err := images.IDFromName(sc, opts.ImageName)
if err != nil {
return nil, err
}
b["imageRef"] = imageID
}
}
// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
if opts.FlavorRef == "" {
if opts.FlavorName == "" {
err := ErrNeitherFlavorIDNorFlavorNameProvided{}
err.Argument = "FlavorRef/FlavorName"
return nil, err
}
if sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
flavorID, err := flavors.IDFromName(sc, opts.FlavorName)
if err != nil {
return nil, err
}
b["flavorRef"] = flavorID
}
return map[string]interface{}{"server": b}, nil
}
// Create requests a server to be provisioned to the user in the current tenant.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
reqBody, err := opts.ToServerCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil)
return
}
// Delete requests that a server previously provisioned be removed from your
// account.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}
// ForceDelete forces the deletion of a server.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
return
}
// Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return
}
// UpdateOptsBuilder allows extensions to add additional attributes to the
// Update request.
type UpdateOptsBuilder interface {
ToServerUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts specifies the base attributes that may be updated on an existing
// server.
type UpdateOpts struct {
// Name changes the displayed name of the server.
// The server host name will *not* change.
// Server names are not constrained to be unique, even within the same tenant.
Name string `json:"name,omitempty"`
// AccessIPv4 provides a new IPv4 address for the instance.
AccessIPv4 string `json:"accessIPv4,omitempty"`
// AccessIPv6 provides a new IPv6 address for the instance.
AccessIPv6 string `json:"accessIPv6,omitempty"`
}
// ToServerUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "server")
}
// Update requests that various attributes of the indicated server be changed.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToServerUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// ChangeAdminPassword alters the administrator or root password for a specified
// server.
func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) {
b := map[string]interface{}{
"changePassword": map[string]string{
"adminPass": newPassword,
},
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
return
}
// RebootMethod describes the mechanisms by which a server reboot can be requested.
type RebootMethod string
// These constants determine how a server should be rebooted.
// See the Reboot() function for further details.
const (
SoftReboot RebootMethod = "SOFT"
HardReboot RebootMethod = "HARD"
OSReboot = SoftReboot
PowerCycle = HardReboot
)
// RebootOptsBuilder allows extensions to add additional parameters to the
// reboot request.
type RebootOptsBuilder interface {
ToServerRebootMap() (map[string]interface{}, error)
}
// RebootOpts provides options to the reboot request.
type RebootOpts struct {
// Type is the type of reboot to perform on the server.
Type RebootMethod `json:"type" required:"true"`
}
// ToServerRebootMap builds a body for the reboot request.
func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "reboot")
}
/*
Reboot requests that a given server reboot.
Two methods exist for rebooting a server:
HardReboot (aka PowerCycle) starts the server instance by physically cutting
power to the machine, or if a VM, terminating it at the hypervisor level.
It's done. Caput. Full stop.
Then, after a brief while, power is rtored or the VM instance restarted.
SoftReboot (aka OSReboot) simply tells the OS to restart under its own
procedure.
E.g., in Linux, asking it to enter runlevel 6, or executing
"sudo shutdown -r now", or by asking Windows to rtart the machine.
*/
func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) {
b, err := opts.ToServerRebootMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
return
}
// RebuildOptsBuilder allows extensions to provide additional parameters to the
// rebuild request.
type RebuildOptsBuilder interface {
ToServerRebuildMap() (map[string]interface{}, error)
}
// RebuildOpts represents the configuration options used in a server rebuild
// operation.
type RebuildOpts struct {
// AdminPass is the server's admin password
AdminPass string `json:"adminPass,omitempty"`
// ImageID is the ID of the image you want your server to be provisioned on.
ImageID string `json:"imageRef"`
// ImageName is readable name of an image.
ImageName string `json:"-"`
// Name to set the server to
Name string `json:"name,omitempty"`
// AccessIPv4 [optional] provides a new IPv4 address for the instance.
AccessIPv4 string `json:"accessIPv4,omitempty"`
// AccessIPv6 [optional] provides a new IPv6 address for the instance.
AccessIPv6 string `json:"accessIPv6,omitempty"`
// Metadata [optional] contains key-value pairs (up to 255 bytes each)
// to attach to the server.
Metadata map[string]string `json:"metadata,omitempty"`
// Personality [optional] includes files to inject into the server at launch.
// Rebuild will base64-encode file contents for you.
Personality Personality `json:"personality,omitempty"`
// ServiceClient will allow calls to be made to retrieve an image or
// flavor ID by name.
ServiceClient *gophercloud.ServiceClient `json:"-"`
}
// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON
func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil {
return nil, err
}
// If ImageRef isn't provided, check if ImageName was provided to ascertain
// the image ID.
if opts.ImageID == "" {
if opts.ImageName != "" {
if opts.ServiceClient == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName)
if err != nil {
return nil, err
}
b["imageRef"] = imageID
}
}
return map[string]interface{}{"rebuild": b}, nil
}
// Rebuild will reprovision the server according to the configuration options
// provided in the RebuildOpts struct.
func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) {
b, err := opts.ToServerRebuildMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil)
return
}
// ResizeOptsBuilder allows extensions to add additional parameters to the
// resize request.
type ResizeOptsBuilder interface {
ToServerResizeMap() (map[string]interface{}, error)
}
// ResizeOpts represents the configuration options used to control a Resize
// operation.
type ResizeOpts struct {
// FlavorRef is the ID of the flavor you wish your server to become.
FlavorRef string `json:"flavorRef" required:"true"`
}
// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON
// request body for the Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "resize")
}
// Resize instructs the provider to change the flavor of the server.
//
// Note that this implies rebuilding it.
//
// Unfortunately, one cannot pass rebuild parameters to the resize function.
// When the resize completes, the server will be in VERIFY_RESIZE state.
// While in this state, you can explore the use of the new server's
// configuration. If you like it, call ConfirmResize() to commit the resize
// permanently. Otherwise, call RevertResize() to restore the old configuration.
func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) {
b, err := opts.ToServerResizeMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
return
}
// ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details.
func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{
OkCodes: []int{201, 202, 204},
})
return
}
// RevertResize cancels a previous resize operation on a server.
// See Resize() for more details.
func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil)
return
}
// RescueOptsBuilder is an interface that allows extensions to override the
// default structure of a Rescue request.
type RescueOptsBuilder interface {
ToServerRescueMap() (map[string]interface{}, error)
}
// RescueOpts represents the configuration options used to control a Rescue
// option.
type RescueOpts struct {
// AdminPass is the desired administrative password for the instance in
// RESCUE mode. If it's left blank, the server will generate a password.
AdminPass string `json:"adminPass,omitempty"`
}
// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON
// request body for the Rescue request.
func (opts RescueOpts) ToServerRescueMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "rescue")
}
// Rescue instructs the provider to place the server into RESCUE mode.
func Rescue(client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) {
b, err := opts.ToServerRescueMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// ResetMetadataOptsBuilder allows extensions to add additional parameters to
// the Reset request.
type ResetMetadataOptsBuilder interface {
ToMetadataResetMap() (map[string]interface{}, error)
}
// MetadataOpts is a map that contains key-value pairs.
type MetadataOpts map[string]string
// ToMetadataResetMap assembles a body for a Reset request based on the contents
// of a MetadataOpts.
func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil
}
// ToMetadataUpdateMap assembles a body for an Update request based on the
// contents of a MetadataOpts.
func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil
}
// ResetMetadata will create multiple new key-value pairs for the given server
// ID.
// Note: Using this operation will erase any already-existing metadata and
// create the new metadata provided. To keep any already-existing metadata,
// use the UpdateMetadatas or UpdateMetadata function.
func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) {
b, err := opts.ToMetadataResetMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Metadata requests all the metadata for the given server ID.
func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) {
_, r.Err = client.Get(metadataURL(client, id), &r.Body, nil)
return
}
// UpdateMetadataOptsBuilder allows extensions to add additional parameters to
// the Create request.
type UpdateMetadataOptsBuilder interface {
ToMetadataUpdateMap() (map[string]interface{}, error)
}
// UpdateMetadata updates (or creates) all the metadata specified by opts for
// the given server ID. This operation does not affect already-existing metadata
// that is not specified by opts.
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) {
b, err := opts.ToMetadataUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// MetadatumOptsBuilder allows extensions to add additional parameters to the
// Create request.
type MetadatumOptsBuilder interface {
ToMetadatumCreateMap() (map[string]interface{}, string, error)
}
// MetadatumOpts is a map of length one that contains a key-value pair.
type MetadatumOpts map[string]string
// ToMetadatumCreateMap assembles a body for a Create request based on the
// contents of a MetadataumOpts.
func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
if len(opts) != 1 {
err := gophercloud.ErrInvalidInput{}
err.Argument = "servers.MetadatumOpts"
err.Info = "Must have 1 and only 1 key-value pair"
return nil, "", err
}
metadatum := map[string]interface{}{"meta": opts}
var key string
for k := range metadatum["meta"].(MetadatumOpts) {
key = k
}
return metadatum, key, nil
}
// CreateMetadatum will create or update the key-value pair with the given key
// for the given server ID.
func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) {
b, key, err := opts.ToMetadatumCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Metadatum requests the key-value pair with the given key for the given
// server ID.
func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) {
_, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil)
return
}
// DeleteMetadatum will delete the key-value pair with the given key for the
// given server ID.
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) {
_, r.Err = client.Delete(metadatumURL(client, id, key), nil)
return
}
// ListAddresses makes a request against the API to list the servers IP
// addresses.
func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page {
return AddressPage{pagination.SinglePageBase(r)}
})
}
// ListAddressesByNetwork makes a request against the API to list the servers IP
// addresses for the given network.
func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager {
return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page {
return NetworkAddressPage{pagination.SinglePageBase(r)}
})
}
// CreateImageOptsBuilder allows extensions to add additional parameters to the
// CreateImage request.
type CreateImageOptsBuilder interface {
ToServerCreateImageMap() (map[string]interface{}, error)
}
// CreateImageOpts provides options to pass to the CreateImage request.
type CreateImageOpts struct {
// Name of the image/snapshot.
Name string `json:"name" required:"true"`
// Metadata contains key-value pairs (up to 255 bytes each) to attach to
// the created image.
Metadata map[string]string `json:"metadata,omitempty"`
}
// ToServerCreateImageMap formats a CreateImageOpts structure into a request
// body.
func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "createImage")
}
// CreateImage makes a request against the nova API to schedule an image to be
// created of the server
func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) {
b, err := opts.ToServerCreateImageMap()
if err != nil {
r.Err = err
return
}
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
r.Err = err
r.Header = resp.Header
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 := ""
allPages, err := List(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := ExtractServers(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "server"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"}
}
}
// GetPassword makes a request against the nova API to get the encrypted
// administrative password.
func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
return
}

View File

@ -0,0 +1,414 @@
package servers
import (
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/url"
"path"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
type serverResult struct {
gophercloud.Result
}
// Extract interprets any serverResult as a Server, if possible.
func (r serverResult) Extract() (*Server, error) {
var s Server
err := r.ExtractInto(&s)
return &s, err
}
func (r serverResult) ExtractInto(v interface{}) error {
return r.Result.ExtractIntoStructPtr(v, "server")
}
func ExtractServersInto(r pagination.Page, v interface{}) error {
return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers")
}
// CreateResult is the response from a Create operation. Call its Extract
// method to interpret it as a Server.
type CreateResult struct {
serverResult
}
// GetResult is the response from a Get operation. Call its Extract
// method to interpret it as a Server.
type GetResult struct {
serverResult
}
// UpdateResult is the response from an Update operation. Call its Extract
// method to interpret it as a Server.
type UpdateResult struct {
serverResult
}
// DeleteResult is the response from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// RebuildResult is the response from a Rebuild operation. Call its Extract
// method to interpret it as a Server.
type RebuildResult struct {
serverResult
}
// ActionResult represents the result of server action operations, like reboot.
// Call its ExtractErr method to determine if the action succeeded or failed.
type ActionResult struct {
gophercloud.ErrResult
}
// RescueResult is the response from a Rescue operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type RescueResult struct {
ActionResult
}
// CreateImageResult is the response from a CreateImage operation. Call its
// ExtractImageID method to retrieve the ID of the newly created image.
type CreateImageResult struct {
gophercloud.Result
}
// GetPasswordResult represent the result of a get os-server-password operation.
// Call its ExtractPassword method to retrieve the password.
type GetPasswordResult struct {
gophercloud.Result
}
// ExtractPassword gets the encrypted password.
// If privateKey != nil the password is decrypted with the private key.
// If privateKey == nil the encrypted password is returned and can be decrypted
// with:
// echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
var s struct {
Password string `json:"password"`
}
err := r.ExtractInto(&s)
if err == nil && privateKey != nil && s.Password != "" {
return decryptPassword(s.Password, privateKey)
}
return s.Password, err
}
func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) {
b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword)))
n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword))
if err != nil {
return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err)
}
password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n])
if err != nil {
return "", fmt.Errorf("Failed to decrypt password: %s", err)
}
return string(password), nil
}
// ExtractImageID gets the ID of the newly created server image from the header.
func (r CreateImageResult) ExtractImageID() (string, error) {
if r.Err != nil {
return "", r.Err
}
// Get the image id from the header
u, err := url.ParseRequestURI(r.Header.Get("Location"))
if err != nil {
return "", err
}
imageID := path.Base(u.Path)
if imageID == "." || imageID == "/" {
return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u)
}
return imageID, nil
}
// Extract interprets any RescueResult as an AdminPass, if possible.
func (r RescueResult) Extract() (string, error) {
var s struct {
AdminPass string `json:"adminPass"`
}
err := r.ExtractInto(&s)
return s.AdminPass, err
}
// Server represents a server/instance in the OpenStack cloud.
type Server struct {
// ID uniquely identifies this server amongst all other servers,
// including those not accessible to the current tenant.
ID string `json:"id"`
// TenantID identifies the tenant owning this server resource.
TenantID string `json:"tenant_id"`
// UserID uniquely identifies the user account owning the tenant.
UserID string `json:"user_id"`
// Name contains the human-readable name for the server.
Name string `json:"name"`
// Updated and Created contain ISO-8601 timestamps of when the state of the
// server last changed, and when it was created.
Updated time.Time `json:"updated"`
Created time.Time `json:"created"`
// HostID is the host where the server is located in the cloud.
HostID string `json:"hostid"`
// Status contains the current operational status of the server,
// such as IN_PROGRESS or ACTIVE.
Status string `json:"status"`
// Progress ranges from 0..100.
// A request made against the server completes only once Progress reaches 100.
Progress int `json:"progress"`
// AccessIPv4 and AccessIPv6 contain the IP addresses of the server,
// suitable for remote access for administration.
AccessIPv4 string `json:"accessIPv4"`
AccessIPv6 string `json:"accessIPv6"`
// Image refers to a JSON object, which itself indicates the OS image used to
// deploy the server.
Image map[string]interface{} `json:"-"`
// Flavor refers to a JSON object, which itself indicates the hardware
// configuration of the deployed server.
Flavor map[string]interface{} `json:"flavor"`
// Addresses includes a list of all IP addresses assigned to the server,
// keyed by pool.
Addresses map[string]interface{} `json:"addresses"`
// Metadata includes a list of all user-specified key-value pairs attached
// to the server.
Metadata map[string]string `json:"metadata"`
// Links includes HTTP references to the itself, useful for passing along to
// other APIs that might want a server reference.
Links []interface{} `json:"links"`
// KeyName indicates which public key was injected into the server on launch.
KeyName string `json:"key_name"`
// AdminPass will generally be empty (""). However, it will contain the
// administrative password chosen when provisioning a new server without a
// set AdminPass setting in the first place.
// Note that this is the ONLY time this field will be valid.
AdminPass string `json:"adminPass"`
// SecurityGroups includes the security groups that this instance has applied
// to it.
SecurityGroups []map[string]interface{} `json:"security_groups"`
// Fault contains failure information about a server.
Fault Fault `json:"fault"`
}
type Fault struct {
Code int `json:"code"`
Created time.Time `json:"created"`
Details string `json:"details"`
Message string `json:"message"`
}
func (r *Server) UnmarshalJSON(b []byte) error {
type tmp Server
var s struct {
tmp
Image interface{} `json:"image"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Server(s.tmp)
switch t := s.Image.(type) {
case map[string]interface{}:
r.Image = t
case string:
switch t {
case "":
r.Image = nil
}
}
return err
}
// ServerPage abstracts the raw results of making a List() request against
// the API. As OpenStack extensions may freely alter the response bodies of
// structures returned to the client, you may only safely access the data
// provided through the ExtractServers call.
type ServerPage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if a page contains no Server results.
func (r ServerPage) IsEmpty() (bool, error) {
s, err := ExtractServers(r)
return len(s) == 0, err
}
// NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (r ServerPage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"servers_links"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractServers interprets the results of a single page from a List() call,
// producing a slice of Server entities.
func ExtractServers(r pagination.Page) ([]Server, error) {
var s []Server
err := ExtractServersInto(r, &s)
return s, err
}
// MetadataResult contains the result of a call for (potentially) multiple
// key-value pairs. Call its Extract method to interpret it as a
// map[string]interface.
type MetadataResult struct {
gophercloud.Result
}
// GetMetadataResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type GetMetadataResult struct {
MetadataResult
}
// ResetMetadataResult contains the result of a Reset operation. Call its
// Extract method to interpret it as a map[string]interface.
type ResetMetadataResult struct {
MetadataResult
}
// UpdateMetadataResult contains the result of an Update operation. Call its
// Extract method to interpret it as a map[string]interface.
type UpdateMetadataResult struct {
MetadataResult
}
// MetadatumResult contains the result of a call for individual a single
// key-value pair.
type MetadatumResult struct {
gophercloud.Result
}
// GetMetadatumResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type GetMetadatumResult struct {
MetadatumResult
}
// CreateMetadatumResult contains the result of a Create operation. Call its
// Extract method to interpret it as a map[string]interface.
type CreateMetadatumResult struct {
MetadatumResult
}
// DeleteMetadatumResult contains the result of a Delete operation. Call its
// ExtractErr method to determine if the call succeeded or failed.
type DeleteMetadatumResult struct {
gophercloud.ErrResult
}
// Extract interprets any MetadataResult as a Metadata, if possible.
func (r MetadataResult) Extract() (map[string]string, error) {
var s struct {
Metadata map[string]string `json:"metadata"`
}
err := r.ExtractInto(&s)
return s.Metadata, err
}
// Extract interprets any MetadatumResult as a Metadatum, if possible.
func (r MetadatumResult) Extract() (map[string]string, error) {
var s struct {
Metadatum map[string]string `json:"meta"`
}
err := r.ExtractInto(&s)
return s.Metadatum, err
}
// Address represents an IP address.
type Address struct {
Version int `json:"version"`
Address string `json:"addr"`
}
// AddressPage abstracts the raw results of making a ListAddresses() request
// against the API. As OpenStack extensions may freely alter the response bodies
// of structures returned to the client, you may only safely access the data
// provided through the ExtractAddresses call.
type AddressPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if an AddressPage contains no networks.
func (r AddressPage) IsEmpty() (bool, error) {
addresses, err := ExtractAddresses(r)
return len(addresses) == 0, err
}
// ExtractAddresses interprets the results of a single page from a
// ListAddresses() call, producing a map of addresses.
func ExtractAddresses(r pagination.Page) (map[string][]Address, error) {
var s struct {
Addresses map[string][]Address `json:"addresses"`
}
err := (r.(AddressPage)).ExtractInto(&s)
return s.Addresses, err
}
// NetworkAddressPage abstracts the raw results of making a
// ListAddressesByNetwork() request against the API.
// As OpenStack extensions may freely alter the response bodies of structures
// returned to the client, you may only safely access the data provided through
// the ExtractAddresses call.
type NetworkAddressPage struct {
pagination.SinglePageBase
}
// IsEmpty returns true if a NetworkAddressPage contains no addresses.
func (r NetworkAddressPage) IsEmpty() (bool, error) {
addresses, err := ExtractNetworkAddresses(r)
return len(addresses) == 0, err
}
// ExtractNetworkAddresses interprets the results of a single page from a
// ListAddressesByNetwork() call, producing a slice of addresses.
func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) {
var s map[string][]Address
err := (r.(NetworkAddressPage)).ExtractInto(&s)
if err != nil {
return nil, err
}
var key string
for k := range s {
key = k
}
return s[key], err
}

View File

@ -0,0 +1,51 @@
package servers
import "github.com/gophercloud/gophercloud"
func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers")
}
func listURL(client *gophercloud.ServiceClient) string {
return createURL(client)
}
func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("servers", "detail")
}
func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id)
}
func getURL(client *gophercloud.ServiceClient, id string) string {
return deleteURL(client, id)
}
func updateURL(client *gophercloud.ServiceClient, id string) string {
return deleteURL(client, id)
}
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
func metadatumURL(client *gophercloud.ServiceClient, id, key string) string {
return client.ServiceURL("servers", id, "metadata", key)
}
func metadataURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "metadata")
}
func listAddressesURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "ips")
}
func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string {
return client.ServiceURL("servers", id, "ips", network)
}
func passwordURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "os-server-password")
}

View File

@ -0,0 +1,21 @@
package servers
import "github.com/gophercloud/gophercloud"
// WaitForStatus will continually poll a server until it successfully
// transitions to a specified status. It will do this for at most the number
// of seconds specified.
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
})
}