mirror of
https://github.com/linuxkit/linuxkit.git
synced 2025-07-19 09:16:29 +00:00
Merge pull request #2338 from yankcrime/run_openstack
Initial support for launching instances on OpenStack
This commit is contained in:
commit
66b81a5205
@ -22,6 +22,7 @@ func runUsage() {
|
|||||||
fmt.Printf(" gcp\n")
|
fmt.Printf(" gcp\n")
|
||||||
fmt.Printf(" hyperkit [macOS]\n")
|
fmt.Printf(" hyperkit [macOS]\n")
|
||||||
fmt.Printf(" hyperv [Windows]\n")
|
fmt.Printf(" hyperv [Windows]\n")
|
||||||
|
fmt.Printf(" openstack\n")
|
||||||
fmt.Printf(" packet\n")
|
fmt.Printf(" packet\n")
|
||||||
fmt.Printf(" qemu [linux]\n")
|
fmt.Printf(" qemu [linux]\n")
|
||||||
fmt.Printf(" vcenter\n")
|
fmt.Printf(" vcenter\n")
|
||||||
@ -54,6 +55,8 @@ func run(args []string) {
|
|||||||
runHyperKit(args[1:])
|
runHyperKit(args[1:])
|
||||||
case "hyperv":
|
case "hyperv":
|
||||||
runHyperV(args[1:])
|
runHyperV(args[1:])
|
||||||
|
case "openstack":
|
||||||
|
runOpenStack(args[1:])
|
||||||
case "packet":
|
case "packet":
|
||||||
runPacket(args[1:])
|
runPacket(args[1:])
|
||||||
case "qemu":
|
case "qemu":
|
||||||
|
88
src/cmd/linuxkit/run_openstack.go
Normal file
88
src/cmd/linuxkit/run_openstack.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
log "github.com/Sirupsen/logrus"
|
||||||
|
"github.com/gophercloud/gophercloud"
|
||||||
|
"github.com/gophercloud/gophercloud/openstack"
|
||||||
|
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultOSFlavor = "m1.tiny"
|
||||||
|
authurlVar = "OS_AUTH_URL"
|
||||||
|
usernameVar = "OS_USERNAME"
|
||||||
|
passwordVar = "OS_PASSWORD"
|
||||||
|
projectNameVar = "OS_PROJECT_NAME"
|
||||||
|
userDomainVar = "OS_USER_DOMAIN_NAME"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runOpenStack(args []string) {
|
||||||
|
flags := flag.NewFlagSet("openstack", flag.ExitOnError)
|
||||||
|
invoked := filepath.Base(os.Args[0])
|
||||||
|
flags.Usage = func() {
|
||||||
|
fmt.Printf("USAGE: %s run openstack [options]\n\n", invoked)
|
||||||
|
flags.PrintDefaults()
|
||||||
|
}
|
||||||
|
authurlFlag := flags.String("authurl", "", "The URL of the OpenStack identity service, i.e https://keystone.example.com:5000/v3")
|
||||||
|
usernameFlag := flags.String("username", "", "Username with permissions to create an instance")
|
||||||
|
passwordFlag := flags.String("password", "", "Password for the specified username")
|
||||||
|
projectNameFlag := flags.String("project", "", "Name of the Project (aka Tenant) to be used")
|
||||||
|
userDomainFlag := flags.String("domain", "Default", "Domain name")
|
||||||
|
imageID := flags.String("img-ID", "", "The ID of the image to boot the instance from")
|
||||||
|
networkID := flags.String("network", "", "The ID of the network to attach the instance to")
|
||||||
|
flavorName := flags.String("flavor", defaultOSFlavor, "Instance size (flavor)")
|
||||||
|
name := flags.String("name", "", "Name of the instance")
|
||||||
|
|
||||||
|
if err := flags.Parse(args); err != nil {
|
||||||
|
log.Fatal("Unable to parse args")
|
||||||
|
}
|
||||||
|
|
||||||
|
authurl := getStringValue(authurlVar, *authurlFlag, "")
|
||||||
|
username := getStringValue(usernameVar, *usernameFlag, "")
|
||||||
|
password := getStringValue(passwordVar, *passwordFlag, "")
|
||||||
|
projectName := getStringValue(projectNameVar, *projectNameFlag, "")
|
||||||
|
userDomain := getStringValue(userDomainVar, *userDomainFlag, "")
|
||||||
|
|
||||||
|
authOpts := gophercloud.AuthOptions{
|
||||||
|
IdentityEndpoint: authurl,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
DomainName: userDomain,
|
||||||
|
TenantName: projectName,
|
||||||
|
}
|
||||||
|
provider, err := openstack.AuthenticatedClient(authOpts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to create Compute V2 client, %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
network := servers.Network{
|
||||||
|
UUID: *networkID,
|
||||||
|
}
|
||||||
|
|
||||||
|
serverOpts := &servers.CreateOpts{
|
||||||
|
FlavorName: *flavorName,
|
||||||
|
ImageRef: *imageID,
|
||||||
|
Name: *name,
|
||||||
|
Networks: []servers.Network{network},
|
||||||
|
ServiceClient: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := servers.Create(client, serverOpts).Extract()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Unable to create server: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
servers.WaitForStatus(client, server.ID, "ACTIVE", 600)
|
||||||
|
fmt.Printf("Server created, UUID is %s", server.ID)
|
||||||
|
|
||||||
|
}
|
7
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go
generated
vendored
Normal file
7
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Package flavors provides information and interaction with the flavor API
|
||||||
|
// resource 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.
|
||||||
|
package flavors
|
163
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go
generated
vendored
Normal file
163
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go
generated
vendored
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
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:
|
||||||
|
//
|
||||||
|
// PublicAccess (the default): Returns public flavors and private flavors associated with that project.
|
||||||
|
// PrivateAccess (admin only): Returns private flavors, across all projects.
|
||||||
|
// AllAccess (admin only): Returns public and private flavors across all projects.
|
||||||
|
//
|
||||||
|
// The AccessType arguement is optional, and if it is not supplied, OpenStack returns the PublicAccess
|
||||||
|
// flavors.
|
||||||
|
type AccessType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PublicAccess AccessType = "true"
|
||||||
|
PrivateAccess AccessType = "false"
|
||||||
|
AllAccess AccessType = "None"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListOpts helps control 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.
|
||||||
|
// See ListOpts for more details.
|
||||||
|
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 is passed to Create to create a flavor
|
||||||
|
// Source:
|
||||||
|
// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20
|
||||||
|
type CreateOpts struct {
|
||||||
|
Name string `json:"name" required:"true"`
|
||||||
|
// memory size, in MBs
|
||||||
|
RAM int `json:"ram" required:"true"`
|
||||||
|
VCPUs int `json:"vcpus" required:"true"`
|
||||||
|
// disk size, in GBs
|
||||||
|
Disk *int `json:"disk" required:"true"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
// non-zero, positive
|
||||||
|
Swap *int `json:"swap,omitempty"`
|
||||||
|
RxTxFactor float64 `json:"rxtx_factor,omitempty"`
|
||||||
|
IsPublic *bool `json:"os-flavor-access:is_public,omitempty"`
|
||||||
|
// ephemeral disk size, in GBs, non-zero, positive
|
||||||
|
Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFlavorCreateMap satisfies the CreateOptsBuilder interface
|
||||||
|
func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) {
|
||||||
|
return gophercloud.BuildRequestBody(opts, "flavor")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a 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 instructs OpenStack to provide details on a single flavor, identified by its ID.
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
115
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go
generated
vendored
Normal file
115
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go
generated
vendored
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
package flavors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gophercloud/gophercloud"
|
||||||
|
"github.com/gophercloud/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
type commonResult struct {
|
||||||
|
gophercloud.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateResult struct {
|
||||||
|
commonResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult temporarily holds the response from a Get call.
|
||||||
|
type GetResult struct {
|
||||||
|
commonResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 records represent (virtual) hardware configurations for server resources in a region.
|
||||||
|
type Flavor struct {
|
||||||
|
// The Id field contains the flavor's unique identifier.
|
||||||
|
// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// The Disk and RA< fields provide a measure of storage space offered by the flavor, in GB and MB, respectively.
|
||||||
|
Disk int `json:"disk"`
|
||||||
|
RAM int `json:"ram"`
|
||||||
|
// The Name field provides a human-readable moniker for the flavor.
|
||||||
|
Name string `json:"name"`
|
||||||
|
RxTxFactor float64 `json:"rxtx_factor"`
|
||||||
|
// Swap indicates how much space is reserved for swap.
|
||||||
|
// If not provided, this field will be set to 0.
|
||||||
|
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:"is_public"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 the response from a List call.
|
||||||
|
type FlavorPage struct {
|
||||||
|
pagination.LinkedPageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty determines if a page 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 List operation.
|
||||||
|
func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
|
||||||
|
var s struct {
|
||||||
|
Flavors []Flavor `json:"flavors"`
|
||||||
|
}
|
||||||
|
err := (r.(FlavorPage)).ExtractInto(&s)
|
||||||
|
return s.Flavors, err
|
||||||
|
}
|
17
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go
generated
vendored
Normal file
17
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go
generated
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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")
|
||||||
|
}
|
7
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go
generated
vendored
Normal file
7
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/doc.go
generated
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// Package images provides information and interaction with the image API
|
||||||
|
// resource in the OpenStack Compute 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.
|
||||||
|
package images
|
102
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go
generated
vendored
Normal file
102
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/requests.go
generated
vendored
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gophercloud/gophercloud"
|
||||||
|
"github.com/gophercloud/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListOptsBuilder allows extensions to add additional parameters to the
|
||||||
|
// List request.
|
||||||
|
type ListOptsBuilder interface {
|
||||||
|
ToImageListQuery() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListOpts contain options for limiting the number of Images returned from a call to ListDetail.
|
||||||
|
type ListOpts struct {
|
||||||
|
// When the image last changed status (in date-time format).
|
||||||
|
ChangesSince string `q:"changes-since"`
|
||||||
|
// The number of Images to return.
|
||||||
|
Limit int `q:"limit"`
|
||||||
|
// UUID of the Image at which to set a marker.
|
||||||
|
Marker string `q:"marker"`
|
||||||
|
// The name of the Image.
|
||||||
|
Name string `q:"name"`
|
||||||
|
// The name of the Server (in URL format).
|
||||||
|
Server string `q:"server"`
|
||||||
|
// The current status of the Image.
|
||||||
|
Status string `q:"status"`
|
||||||
|
// The value of 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 acquires additional detail about a specific image by ID.
|
||||||
|
// Use ExtractImage() to interpret the result as an openstack Image.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
83
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go
generated
vendored
Normal file
83
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/results.go
generated
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gophercloud/gophercloud"
|
||||||
|
"github.com/gophercloud/gophercloud/pagination"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetResult temporarily stores a Get response.
|
||||||
|
type GetResult struct {
|
||||||
|
gophercloud.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResult represents the result of an image.Delete operation.
|
||||||
|
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 is used for JSON (un)marshalling.
|
||||||
|
// It provides a description of an OS image.
|
||||||
|
type Image struct {
|
||||||
|
// ID contains the image's unique identifier.
|
||||||
|
ID string
|
||||||
|
|
||||||
|
Created string
|
||||||
|
|
||||||
|
// MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image.
|
||||||
|
MinDisk int
|
||||||
|
MinRAM int
|
||||||
|
|
||||||
|
// Name provides a human-readable moniker for the OS image.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// The Progress and Status fields indicate image-creation status.
|
||||||
|
// Any usable image will have 100% progress.
|
||||||
|
Progress int
|
||||||
|
Status string
|
||||||
|
|
||||||
|
Updated string
|
||||||
|
|
||||||
|
Metadata map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImagePage contains a single page of results from a List operation.
|
||||||
|
// Use ExtractImages to convert it into a slice of usable structs.
|
||||||
|
type ImagePage struct {
|
||||||
|
pagination.LinkedPageBase
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if a page 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
|
||||||
|
}
|
15
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go
generated
vendored
Normal file
15
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/images/urls.go
generated
vendored
Normal 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)
|
||||||
|
}
|
6
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go
generated
vendored
Normal file
6
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go
generated
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// 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.
|
||||||
|
package servers
|
71
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go
generated
vendored
Normal file
71
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go
generated
vendored
Normal 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)
|
||||||
|
}
|
744
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go
generated
vendored
Normal file
744
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go
generated
vendored
Normal file
@ -0,0 +1,744 @@
|
|||||||
|
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 {
|
||||||
|
// A time/date stamp for when the server last changed status.
|
||||||
|
ChangesSince string `q:"changes-since"`
|
||||||
|
|
||||||
|
// Name of the image in URL format.
|
||||||
|
Image string `q:"image"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
// Value of the status of the server so that you can filter on "ACTIVE" for example.
|
||||||
|
Status string `q:"status"`
|
||||||
|
|
||||||
|
// Name of the host as a string.
|
||||||
|
Host string `q:"host"`
|
||||||
|
|
||||||
|
// UUID of the server at which you want to set a marker.
|
||||||
|
Marker string `q:"marker"`
|
||||||
|
|
||||||
|
// Integer value for the limit of values to return.
|
||||||
|
Limit int `q:"limit"`
|
||||||
|
|
||||||
|
// Bool to show all tenants
|
||||||
|
AllTenants bool `q:"all_tenants"`
|
||||||
|
|
||||||
|
// List 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 describes struct types that can be accepted by the Create call.
|
||||||
|
// The CreateOpts struct in this package does.
|
||||||
|
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 nova-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 [optional] 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 rponse.
|
||||||
|
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 is an interface that options must satisfy in order to be
|
||||||
|
// used when rebooting a server instance
|
||||||
|
type RebootOptsBuilder interface {
|
||||||
|
ToServerRebootMap() (map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RebootOpts satisfies the RebootOptsBuilder interface
|
||||||
|
type RebootOpts struct {
|
||||||
|
Type RebootMethod `json:"type" required:"true"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToServerRebootMap allows RebootOpts to satisfiy the RebootOptsBuilder
|
||||||
|
// interface
|
||||||
|
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 rtarted.
|
||||||
|
//
|
||||||
|
// SoftReboot (aka OSReboot) simply tells the OS to rtart under its own procedur.
|
||||||
|
// 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 is an interface that allows extensions to override the
|
||||||
|
// default behaviour of rebuild options
|
||||||
|
type RebuildOptsBuilder interface {
|
||||||
|
ToServerRebuildMap() (map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RebuildOpts represents the configuration options used in a server rebuild
|
||||||
|
// operation
|
||||||
|
type RebuildOpts struct {
|
||||||
|
// The server's admin password
|
||||||
|
AdminPass string `json:"adminPass,omitempty"`
|
||||||
|
// The ID of the image you want your server to be provisioned on
|
||||||
|
ImageID string `json:"imageRef"`
|
||||||
|
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 *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 is an interface that allows extensions to override the default structure of
|
||||||
|
// a 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 RESIZE_VERIFY 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 is the interface types must satisfy in order to be
|
||||||
|
// used as CreateImage options
|
||||||
|
type CreateImageOptsBuilder interface {
|
||||||
|
ToServerCreateImageMap() (map[string]interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateImageOpts satisfies the CreateImageOptsBuilder
|
||||||
|
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
|
||||||
|
}
|
350
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go
generated
vendored
Normal file
350
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go
generated
vendored
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
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 temporarily contains the response from a Create call.
|
||||||
|
type CreateResult struct {
|
||||||
|
serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResult temporarily contains the response from a Get call.
|
||||||
|
type GetResult struct {
|
||||||
|
serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateResult temporarily contains the response from an Update call.
|
||||||
|
type UpdateResult struct {
|
||||||
|
serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteResult temporarily contains the response from a Delete call.
|
||||||
|
type DeleteResult struct {
|
||||||
|
gophercloud.ErrResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// RebuildResult temporarily contains the response from a Rebuild call.
|
||||||
|
type RebuildResult struct {
|
||||||
|
serverResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActionResult represents the result of server action operations, like reboot
|
||||||
|
type ActionResult struct {
|
||||||
|
gophercloud.ErrResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// RescueResult represents the result of a server rescue operation
|
||||||
|
type RescueResult struct {
|
||||||
|
ActionResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateImageResult represents the result of an image creation operation
|
||||||
|
type CreateImageResult struct {
|
||||||
|
gophercloud.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPasswordResult represent the result of a get os-server-password operation.
|
||||||
|
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 exposes only the standard OpenStack fields corresponding to a given server on the user's account.
|
||||||
|
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 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
type MetadataResult struct {
|
||||||
|
gophercloud.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadataResult temporarily contains the response from a metadata Get call.
|
||||||
|
type GetMetadataResult struct {
|
||||||
|
MetadataResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetMetadataResult temporarily contains the response from a metadata Reset call.
|
||||||
|
type ResetMetadataResult struct {
|
||||||
|
MetadataResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMetadataResult temporarily contains the response from a metadata Update call.
|
||||||
|
type UpdateMetadataResult struct {
|
||||||
|
MetadataResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadatumResult contains the result of a call for individual a single key-value pair.
|
||||||
|
type MetadatumResult struct {
|
||||||
|
gophercloud.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetadatumResult temporarily contains the response from a metadatum Get call.
|
||||||
|
type GetMetadatumResult struct {
|
||||||
|
MetadatumResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMetadatumResult temporarily contains the response from a metadatum Create call.
|
||||||
|
type CreateMetadatumResult struct {
|
||||||
|
MetadatumResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call.
|
||||||
|
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
|
||||||
|
}
|
51
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go
generated
vendored
Normal file
51
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go
generated
vendored
Normal 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")
|
||||||
|
}
|
20
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go
generated
vendored
Normal file
20
src/cmd/linuxkit/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go
generated
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user