openstack_sd: Supporting application credential for authentication. (#4968)

* openstack_sd: Support application credentials for authentication.
Updated gophercloud

Signed-off-by: Kevin Bulebush <kmbulebu@gmail.com>
This commit is contained in:
Kevin Bulebush 2019-01-09 10:18:58 -05:00 committed by Brian Brazil
parent b8ede99767
commit 718344434c
56 changed files with 2981 additions and 991 deletions

View file

@ -50,20 +50,23 @@ var (
// SDConfig is the configuration for OpenStack based service discovery. // SDConfig is the configuration for OpenStack based service discovery.
type SDConfig struct { type SDConfig struct {
IdentityEndpoint string `yaml:"identity_endpoint"` IdentityEndpoint string `yaml:"identity_endpoint"`
Username string `yaml:"username"` Username string `yaml:"username"`
UserID string `yaml:"userid"` UserID string `yaml:"userid"`
Password config_util.Secret `yaml:"password"` Password config_util.Secret `yaml:"password"`
ProjectName string `yaml:"project_name"` ProjectName string `yaml:"project_name"`
ProjectID string `yaml:"project_id"` ProjectID string `yaml:"project_id"`
DomainName string `yaml:"domain_name"` DomainName string `yaml:"domain_name"`
DomainID string `yaml:"domain_id"` DomainID string `yaml:"domain_id"`
Role Role `yaml:"role"` ApplicationCredentialName string `yaml:"application_credential_name"`
Region string `yaml:"region"` ApplicationCredentialID string `yaml:"application_credential_id"`
RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"` ApplicationCredentialSecret config_util.Secret `yaml:"application_credential_secret"`
Port int `yaml:"port"` Role Role `yaml:"role"`
AllTenants bool `yaml:"all_tenants,omitempty"` Region string `yaml:"region"`
TLSConfig config_util.TLSConfig `yaml:"tls_config,omitempty"` RefreshInterval model.Duration `yaml:"refresh_interval,omitempty"`
Port int `yaml:"port"`
AllTenants bool `yaml:"all_tenants,omitempty"`
TLSConfig config_util.TLSConfig `yaml:"tls_config,omitempty"`
} }
// OpenStackRole is role of the target in OpenStack. // OpenStackRole is role of the target in OpenStack.
@ -132,14 +135,17 @@ func NewDiscovery(conf *SDConfig, l log.Logger) (Discovery, error) {
} }
} else { } else {
opts = gophercloud.AuthOptions{ opts = gophercloud.AuthOptions{
IdentityEndpoint: conf.IdentityEndpoint, IdentityEndpoint: conf.IdentityEndpoint,
Username: conf.Username, Username: conf.Username,
UserID: conf.UserID, UserID: conf.UserID,
Password: string(conf.Password), Password: string(conf.Password),
TenantName: conf.ProjectName, TenantName: conf.ProjectName,
TenantID: conf.ProjectID, TenantID: conf.ProjectID,
DomainName: conf.DomainName, DomainName: conf.DomainName,
DomainID: conf.DomainID, DomainID: conf.DomainID,
ApplicationCredentialID: conf.ApplicationCredentialID,
ApplicationCredentialName: conf.ApplicationCredentialName,
ApplicationCredentialSecret: string(conf.ApplicationCredentialSecret),
} }
} }
client, err := openstack.NewClient(opts.IdentityEndpoint) client, err := openstack.NewClient(opts.IdentityEndpoint)

View file

@ -540,6 +540,17 @@ region: <string>
[ project_name: <string> ] [ project_name: <string> ]
[ project_id: <string> ] [ project_id: <string> ]
# The application_credential_id or application_credential_name fields are
# required if using an application credential to authenticate. Some providers
# allow you to create an application credential to authenticate rather than a
# password.
[ application_credential_name: <string> ]
[ application_credential_id: <string> ]
# The application_credential_secret field is required if using an application
# credential to authenticate.
[ application_credential_secret: <secret> ]
# Whether the service discovery should list all instances for all projects. # Whether the service discovery should list all instances for all projects.
# It is only relevant for the 'instance' role and usually requires admin permissions. # It is only relevant for the 'instance' role and usually requires admin permissions.
[ all_tenants: <boolean> | default: false ] [ all_tenants: <boolean> | default: false ]
@ -978,7 +989,7 @@ endpoint: <string>
# A list of groups for which targets are retrieved. If omitted, all containers # A list of groups for which targets are retrieved. If omitted, all containers
# available to the requesting account are scraped. # available to the requesting account are scraped.
groups: groups:
[ - <string> ... ] [ - <string> ... ]
# The port to use for discovery and metric scraping. # The port to use for discovery and metric scraping.

2
go.mod
View file

@ -37,7 +37,7 @@ require (
github.com/google/gofuzz v0.0.0-20150304233714-bbcb9da2d746 // indirect github.com/google/gofuzz v0.0.0-20150304233714-bbcb9da2d746 // indirect
github.com/google/pprof v0.0.0-20180605153948-8b03ce837f34 github.com/google/pprof v0.0.0-20180605153948-8b03ce837f34
github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4 // indirect github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4 // indirect
github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602 github.com/gophercloud/gophercloud v0.0.0-20181206160319-9d88c34913a9
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20171126203511-e4b8a938efae github.com/grpc-ecosystem/grpc-gateway v0.0.0-20171126203511-e4b8a938efae

4
go.sum
View file

@ -87,8 +87,8 @@ github.com/google/pprof v0.0.0-20180605153948-8b03ce837f34 h1:mGdRet4qWdrDnNidFr
github.com/google/pprof v0.0.0-20180605153948-8b03ce837f34/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20180605153948-8b03ce837f34/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4 h1:yxHFSapGMUoyn+3v6LiJJxoJhvbDqIq8me0gAWehnSU= github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4 h1:yxHFSapGMUoyn+3v6LiJJxoJhvbDqIq8me0gAWehnSU=
github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.0.0-20180520015035-48a0ecefe2e4/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602 h1:Acc1d6mIuURCyYN6nkm1d7+Gycfq1+jUWdnBbTyGb6E= github.com/gophercloud/gophercloud v0.0.0-20181206160319-9d88c34913a9 h1:7TRGugCPfA2Mll6QT7cbhD1GXZwk7+1PUz8tYrOWXgQ=
github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gophercloud/gophercloud v0.0.0-20181206160319-9d88c34913a9/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=

View file

@ -1 +1,3 @@
**/*.swp **/*.swp
.idea
.vscode

View file

@ -7,11 +7,13 @@ install:
- go get github.com/mattn/goveralls - go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/goimports - go get golang.org/x/tools/cmd/goimports
go: go:
- 1.8 - "1.10"
- tip - "tip"
env: env:
global: global:
- secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ=" - secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ="
before_script:
- go vet ./...
script: script:
- ./script/coverage - ./script/coverage
- ./script/format - ./script/format

98
vendor/github.com/gophercloud/gophercloud/.zuul.yaml generated vendored Normal file
View file

@ -0,0 +1,98 @@
- job:
name: gophercloud-unittest
parent: golang-test
description: |
Run gophercloud unit test
run: .zuul/playbooks/gophercloud-unittest/run.yaml
nodeset: ubuntu-xenial-ut
- job:
name: gophercloud-acceptance-test
parent: golang-test
description: |
Run gophercloud acceptance test on master branch
run: .zuul/playbooks/gophercloud-acceptance-test/run.yaml
- job:
name: gophercloud-acceptance-test-queens
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on queens branch
vars:
global_env:
OS_BRANCH: stable/queens
- job:
name: gophercloud-acceptance-test-rocky
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on rocky branch
vars:
global_env:
OS_BRANCH: stable/rocky
- job:
name: gophercloud-acceptance-test-pike
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on pike branch
vars:
global_env:
OS_BRANCH: stable/pike
- job:
name: gophercloud-acceptance-test-ocata
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on ocata branch
vars:
global_env:
OS_BRANCH: stable/ocata
- job:
name: gophercloud-acceptance-test-newton
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on newton branch
vars:
global_env:
OS_BRANCH: stable/newton
- job:
name: gophercloud-acceptance-test-mitaka
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on mitaka branch
vars:
global_env:
OS_BRANCH: stable/mitaka
nodeset: ubuntu-trusty
- project:
name: gophercloud/gophercloud
check:
jobs:
- gophercloud-unittest
- gophercloud-acceptance-test
recheck-mitaka:
jobs:
- gophercloud-acceptance-test-mitaka
recheck-newton:
jobs:
- gophercloud-acceptance-test-newton
recheck-ocata:
jobs:
- gophercloud-acceptance-test-ocata
recheck-pike:
jobs:
- gophercloud-acceptance-test-pike
recheck-queens:
jobs:
- gophercloud-acceptance-test-queens
recheck-rocky:
jobs:
- gophercloud-acceptance-test-rocky
periodic:
jobs:
- gophercloud-unittest
- gophercloud-acceptance-test

View file

@ -1,148 +0,0 @@
# Tips
## Implementing default logging and re-authentication attempts
You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client
like the following and setting it as the provider client's HTTP Client (via the
`gophercloud.ProviderClient.HTTPClient` field):
```go
//...
// LogRoundTripper satisfies the http.RoundTripper interface and is used to
// customize the default Gophercloud RoundTripper to allow for logging.
type LogRoundTripper struct {
rt http.RoundTripper
numReauthAttempts int
}
// newHTTPClient return a custom HTTP client that allows for logging relevant
// information before and after the HTTP request.
func newHTTPClient() http.Client {
return http.Client{
Transport: &LogRoundTripper{
rt: http.DefaultTransport,
},
}
}
// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
glog.Infof("Request URL: %s\n", request.URL)
response, err := lrt.rt.RoundTrip(request)
if response == nil {
return nil, err
}
if response.StatusCode == http.StatusUnauthorized {
if lrt.numReauthAttempts == 3 {
return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.")
}
lrt.numReauthAttempts++
}
glog.Debugf("Response Status: %s\n", response.Status)
return response, nil
}
endpoint := "https://127.0.0.1/auth"
pc := openstack.NewClient(endpoint)
pc.HTTPClient = newHTTPClient()
//...
```
## Implementing custom objects
OpenStack request/response objects may differ among variable names or types.
### Custom request objects
To pass custom options to a request, implement the desired `<ACTION>OptsBuilder` interface. For
example, to pass in
```go
type MyCreateServerOpts struct {
Name string
Size int
}
```
to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface:
```go
func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{
"name": o.Name,
"size": o.Size,
}, nil
}
```
create an instance of your custom options object, and pass it to `servers.Create`:
```go
// ...
myOpts := MyCreateServerOpts{
Name: "s1",
Size: "100",
}
server, err := servers.Create(computeClient, myOpts).Extract()
// ...
```
### Custom response objects
Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be
combined to create a custom object:
```go
// ...
type MyVolume struct {
volumes.Volume
tenantattr.VolumeExt
}
var v struct {
MyVolume `json:"volume"`
}
err := volumes.Get(client, volID).ExtractInto(&v)
// ...
```
## Overriding default `UnmarshalJSON` method
For some response objects, a field may be a custom type or may be allowed to take on
different types. In these cases, overriding the default `UnmarshalJSON` method may be
necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON`
method on the type:
```go
// ...
type MyVolume struct {
ID string `json: "id"`
TimeCreated time.Time `json: "-"`
}
func (r *MyVolume) UnmarshalJSON(b []byte) error {
type tmp MyVolume
var s struct {
tmp
TimeCreated gophercloud.JSONRFC3339MilliNoZ `json:"created_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = Volume(s.tmp)
r.TimeCreated = time.Time(s.CreatedAt)
return err
}
// ...
```

View file

@ -1,32 +0,0 @@
# Compute
## Floating IPs
* `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingip` is now `github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips`
* `floatingips.Associate` and `floatingips.Disassociate` have been removed.
* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP.
## Security Groups
* `secgroups.AddServerToGroup` is now `secgroups.AddServer`.
* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`.
## Servers
* `servers.Reboot` now requires a `servers.RebootOpts` struct:
```golang
rebootOpts := &servers.RebootOpts{
Type: servers.SoftReboot,
}
res := servers.Reboot(client, server.ID, rebootOpts)
```
# Identity
## V3
### Tokens
* `Token.ExpiresAt` is now of type `gophercloud.JSONRFC3339Milli` instead of
`time.Time`

View file

@ -127,7 +127,7 @@ new resource in the `server` variable (a
## Advanced Usage ## Advanced Usage
Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Gophercloud works. Have a look at the [FAQ](./docs/FAQ.md) for some tips on customizing the way Gophercloud works.
## Backwards-Compatibility Guarantees ## Backwards-Compatibility Guarantees
@ -140,4 +140,20 @@ See the [contributing guide](./.github/CONTRIBUTING.md).
## Help and feedback ## Help and feedback
If you're struggling with something or have spotted a potential bug, feel free If you're struggling with something or have spotted a potential bug, feel free
to submit an issue to our [bug tracker](/issues). to submit an issue to our [bug tracker](https://github.com/gophercloud/gophercloud/issues).
## Thank You
We'd like to extend special thanks and appreciation to the following:
### OpenLab
<a href="http://openlabtesting.org/"><img src="./docs/assets/openlab.png" width="600px"></a>
OpenLab is providing a full CI environment to test each PR and merge for a variety of OpenStack releases.
### VEXXHOST
<a href="https://vexxhost.com/"><img src="./docs/assets/vexxhost.png" width="600px"></a>
VEXXHOST is providing their services to assist with the development and testing of Gophercloud.

View file

@ -1,74 +0,0 @@
## On Pull Requests
- Before you start a PR there needs to be a Github issue and a discussion about it
on that issue with a core contributor, even if it's just a 'SGTM'.
- A PR's description must reference the issue it closes with a `For <ISSUE NUMBER>` (e.g. For #293).
- A PR's description must contain link(s) to the line(s) in the OpenStack
source code (on Github) that prove(s) the PR code to be valid. Links to documentation
are not good enough. The link(s) should be to a non-`master` branch. For example,
a pull request implementing the creation of a Neutron v2 subnet might put the
following link in the description:
https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749
From that link, a reviewer (or user) can verify the fields in the request/response
objects in the PR.
- A PR that is in-progress should have `[wip]` in front of the PR's title. When
ready for review, remove the `[wip]` and ping a core contributor with an `@`.
- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with
one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM]
prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the
[Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will
let reviewers know it is ready to review.
- A PR should be small. Even if you intend on implementing an entire
service, a PR should only be one route of that service
(e.g. create server or get server, but not both).
- Unless explicitly asked, do not squash commits in the middle of a review; only
append. It makes it difficult for the reviewer to see what's changed from one
review to the next.
## On Code
- In re design: follow as closely as is reasonable the code already in the library.
Most operations (e.g. create, delete) admit the same design.
- Unit tests and acceptance (integration) tests must be written to cover each PR.
Tests for operations with several options (e.g. list, create) should include all
the options in the tests. This will allow users to verify an operation on their
own infrastructure and see an example of usage.
- If in doubt, ask in-line on the PR.
### File Structure
- The following should be used in most cases:
- `requests.go`: contains all the functions that make HTTP requests and the
types associated with the HTTP request (parameters for URL, body, etc)
- `results.go`: contains all the response objects and their methods
- `urls.go`: contains the endpoints to which the requests are made
### Naming
- For methods on a type in `results.go`, the receiver should be named `r` and the
variable into which it will be unmarshalled `s`.
- Functions in `requests.go`, with the exception of functions that return a
`pagination.Pager`, should be named returns of the name `r`.
- Functions in `requests.go` that accept request bodies should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `CreateOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Map`
(eg `ToPortCreateMap`).
- Functions in `requests.go` that accept query strings should accept as their
last parameter an `interface` named `<Action>OptsBuilder` (eg `ListOptsBuilder`).
This `interface` should have at the least a method named `To<Resource><Action>Query`
(eg `ToServerListQuery`).

View file

@ -9,25 +9,45 @@ ProviderClient representing an active session on that provider.
Its fields are the union of those recognized by each identity implementation and Its fields are the union of those recognized by each identity implementation and
provider. provider.
An example of manually providing authentication information:
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
Username: "{username}",
Password: "{password}",
TenantID: "{tenant_id}",
}
provider, err := openstack.AuthenticatedClient(opts)
An example of using AuthOptionsFromEnv(), where the environment variables can
be read from a file, such as a standard openrc file:
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
*/ */
type AuthOptions struct { type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with // IdentityEndpoint specifies the HTTP endpoint that is required to work with
// the Identity API of the appropriate version. While it's ultimately needed by // the Identity API of the appropriate version. While it's ultimately needed by
// all of the identity services, it will often be populated by a provider-level // all of the identity services, it will often be populated by a provider-level
// function. // function.
//
// The IdentityEndpoint is typically referred to as the "auth_url" or
// "OS_AUTH_URL" in the information provided by the cloud operator.
IdentityEndpoint string `json:"-"` IdentityEndpoint string `json:"-"`
// Username is required if using Identity V2 API. Consult with your provider's // Username is required if using Identity V2 API. Consult with your provider's
// control panel to discover your account's username. In Identity V3, either // control panel to discover your account's username. In Identity V3, either
// UserID or a combination of Username and DomainID or DomainName are needed. // UserID or a combination of Username and DomainID or DomainName are needed.
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
UserID string `json:"id,omitempty"` UserID string `json:"-"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
// At most one of DomainID and DomainName must be provided if using Username // At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional. // with Identity V3. Otherwise, either are optional.
DomainID string `json:"id,omitempty"` DomainID string `json:"-"`
DomainName string `json:"name,omitempty"` DomainName string `json:"name,omitempty"`
// The TenantID and TenantName fields are optional for the Identity V2 API. // The TenantID and TenantName fields are optional for the Identity V2 API.
@ -39,7 +59,7 @@ type AuthOptions struct {
// If DomainID or DomainName are provided, they will also apply to TenantName. // If DomainID or DomainName are provided, they will also apply to TenantName.
// It is not currently possible to authenticate with Username and a Domain // It is not currently possible to authenticate with Username and a Domain
// and scope to a Project in a different Domain by using TenantName. To // and scope to a Project in a different Domain by using TenantName. To
// accomplish that, the ProjectID will need to be provided to the TenantID // accomplish that, the ProjectID will need to be provided as the TenantID
// option. // option.
TenantID string `json:"tenantId,omitempty"` TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"` TenantName string `json:"tenantName,omitempty"`
@ -50,15 +70,34 @@ type AuthOptions struct {
// false, it will not cache these settings, but re-authentication will not be // false, it will not cache these settings, but re-authentication will not be
// possible. This setting defaults to false. // possible. This setting defaults to false.
// //
// NOTE: The reauth function will try to re-authenticate endlessly if left unchecked. // NOTE: The reauth function will try to re-authenticate endlessly if left
// The way to limit the number of attempts is to provide a custom HTTP client to the provider client // unchecked. The way to limit the number of attempts is to provide a custom
// and provide a transport that implements the RoundTripper interface and stores the number of failed retries. // HTTP client to the provider client and provide a transport that implements
// For an example of this, see here: https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 // the RoundTripper interface and stores the number of failed retries. For an
// example of this, see here:
// https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
AllowReauth bool `json:"-"` AllowReauth bool `json:"-"`
// TokenID allows users to authenticate (possibly as another user) with an // TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID. // authentication token ID.
TokenID string `json:"-"` TokenID string `json:"-"`
// Scope determines the scoping of the authentication request.
Scope *AuthScope `json:"-"`
// Authentication through Application Credentials requires supplying name, project and secret
// For project we can use TenantID
ApplicationCredentialID string `json:"-"`
ApplicationCredentialName string `json:"-"`
ApplicationCredentialSecret string `json:"-"`
}
// AuthScope allows a created token to be limited to a specific domain or project.
type AuthScope struct {
ProjectID string
ProjectName string
DomainID string
DomainName string
} }
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder // ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
@ -109,7 +148,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
type userReq struct { type userReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Password string `json:"password"` Password string `json:"password,omitempty"`
Domain *domainReq `json:"domain,omitempty"` Domain *domainReq `json:"domain,omitempty"`
} }
@ -121,10 +160,18 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
ID string `json:"id"` ID string `json:"id"`
} }
type applicationCredentialReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
User *userReq `json:"user,omitempty"`
Secret *string `json:"secret,omitempty"`
}
type identityReq struct { type identityReq struct {
Methods []string `json:"methods"` Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"` Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"` Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
} }
type authReq struct { type authReq struct {
@ -138,6 +185,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// Populate the request structure based on the provided arguments. Create and return an error // Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present. // if insufficient or incompatible information is present.
var req request var req request
var userRequest userReq
if opts.Password == "" { if opts.Password == "" {
if opts.TokenID != "" { if opts.TokenID != "" {
@ -161,8 +209,49 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
req.Auth.Identity.Token = &tokenReq{ req.Auth.Identity.Token = &tokenReq{
ID: opts.TokenID, ID: opts.TokenID,
} }
} else if opts.ApplicationCredentialID != "" {
// Configure the request for ApplicationCredentialID authentication.
// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
// There are three kinds of possible application_credential requests
// 1. application_credential id + secret
// 2. application_credential name + secret + user_id
// 3. application_credential name + secret + username + domain_id / domain_name
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
ID: &opts.ApplicationCredentialID,
Secret: &opts.ApplicationCredentialSecret,
}
} else if opts.ApplicationCredentialName != "" {
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
// make sure that only one of DomainName or DomainID were provided
if opts.DomainID == "" && opts.DomainName == "" {
return nil, ErrDomainIDOrDomainName{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
if opts.DomainID != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{ID: &opts.DomainID},
}
} else if opts.DomainName != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{Name: &opts.DomainName},
}
}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
Name: &opts.ApplicationCredentialName,
User: &userRequest,
Secret: &opts.ApplicationCredentialSecret,
}
} else { } else {
// If no password or token ID are available, authentication can't continue. // If no password or token ID or ApplicationCredential are available, authentication can't continue.
return nil, ErrMissingPassword{} return nil, ErrMissingPassword{}
} }
} else { } else {
@ -241,82 +330,85 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
} }
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
// For backwards compatibility.
var scope struct { // If AuthOptions.Scope was not set, try to determine it.
ProjectID string // This works well for common scenarios.
ProjectName string if opts.Scope == nil {
DomainID string opts.Scope = new(AuthScope)
DomainName string if opts.TenantID != "" {
} opts.Scope.ProjectID = opts.TenantID
} else {
if opts.TenantID != "" { if opts.TenantName != "" {
scope.ProjectID = opts.TenantID opts.Scope.ProjectName = opts.TenantName
} else { opts.Scope.DomainID = opts.DomainID
if opts.TenantName != "" { opts.Scope.DomainName = opts.DomainName
scope.ProjectName = opts.TenantName }
scope.DomainID = opts.DomainID
scope.DomainName = opts.DomainName
} }
} }
if scope.ProjectName != "" { if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied. // ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied. // ProjectID may not be supplied.
if scope.DomainID == "" && scope.DomainName == "" { if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
return nil, ErrScopeDomainIDOrDomainName{} return nil, ErrScopeDomainIDOrDomainName{}
} }
if scope.ProjectID != "" { if opts.Scope.ProjectID != "" {
return nil, ErrScopeProjectIDOrProjectName{} return nil, ErrScopeProjectIDOrProjectName{}
} }
if scope.DomainID != "" { if opts.Scope.DomainID != "" {
// ProjectName + DomainID // ProjectName + DomainID
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"name": &scope.ProjectName, "name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"id": &scope.DomainID}, "domain": map[string]interface{}{"id": &opts.Scope.DomainID},
}, },
}, nil }, nil
} }
if scope.DomainName != "" { if opts.Scope.DomainName != "" {
// ProjectName + DomainName // ProjectName + DomainName
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"name": &scope.ProjectName, "name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"name": &scope.DomainName}, "domain": map[string]interface{}{"name": &opts.Scope.DomainName},
}, },
}, nil }, nil
} }
} else if scope.ProjectID != "" { } else if opts.Scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if scope.DomainID != "" { if opts.Scope.DomainID != "" {
return nil, ErrScopeProjectIDAlone{} return nil, ErrScopeProjectIDAlone{}
} }
if scope.DomainName != "" { if opts.Scope.DomainName != "" {
return nil, ErrScopeProjectIDAlone{} return nil, ErrScopeProjectIDAlone{}
} }
// ProjectID // ProjectID
return map[string]interface{}{ return map[string]interface{}{
"project": map[string]interface{}{ "project": map[string]interface{}{
"id": &scope.ProjectID, "id": &opts.Scope.ProjectID,
}, },
}, nil }, nil
} else if scope.DomainID != "" { } else if opts.Scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if scope.DomainName != "" { if opts.Scope.DomainName != "" {
return nil, ErrScopeDomainIDOrDomainName{} return nil, ErrScopeDomainIDOrDomainName{}
} }
// DomainID // DomainID
return map[string]interface{}{ return map[string]interface{}{
"domain": map[string]interface{}{ "domain": map[string]interface{}{
"id": &scope.DomainID, "id": &opts.Scope.DomainID,
},
}, nil
} else if opts.Scope.DomainName != "" {
// DomainName
return map[string]interface{}{
"domain": map[string]interface{}{
"name": &opts.Scope.DomainName,
}, },
}, nil }, nil
} else if scope.DomainName != "" {
return nil, ErrScopeDomainName{}
} }
return nil, nil return nil, nil

View file

@ -3,11 +3,17 @@ Package gophercloud provides a multi-vendor interface to OpenStack-compatible
clouds. The library has a three-level hierarchy: providers, services, and clouds. The library has a three-level hierarchy: providers, services, and
resources. resources.
Provider structs represent the service providers that offer and manage a Authenticating with Providers
collection of services. The IdentityEndpoint is typically refered to as
"auth_url" in information provided by the cloud operator. Additionally, Provider structs represent the cloud providers that offer and manage a
the cloud may refer to TenantID or TenantName as project_id and project_name. collection of services. You will generally want to create one Provider
These are defined like so: client per OpenStack cloud.
Use your OpenStack credentials to create a Provider client. The
IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in
information provided by the cloud operator. Additionally, the cloud may refer to
TenantID or TenantName as project_id and project_name. Credentials are
specified like so:
opts := gophercloud.AuthOptions{ opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0", IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
@ -18,6 +24,16 @@ These are defined like so:
provider, err := openstack.AuthenticatedClient(opts) provider, err := openstack.AuthenticatedClient(opts)
You may also use the openstack.AuthOptionsFromEnv() helper function. This
function reads in standard environment variables frequently found in an
OpenStack `openrc` file. Again note that Gophercloud currently uses "tenant"
instead of "project".
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
Service Clients
Service structs are specific to a provider and handle all of the logic and Service structs are specific to a provider and handle all of the logic and
operations for a particular OpenStack service. Examples of services include: operations for a particular OpenStack service. Examples of services include:
Compute, Object Storage, Block Storage. In order to define one, you need to Compute, Object Storage, Block Storage. In order to define one, you need to
@ -27,6 +43,8 @@ pass in the parent provider, like so:
client := openstack.NewComputeV2(provider, opts) client := openstack.NewComputeV2(provider, opts)
Resources
Resource structs are the domain models that services make use of in order Resource structs are the domain models that services make use of in order
to work with and represent the state of API resources: to work with and represent the state of API resources:
@ -62,6 +80,12 @@ of results:
return true, nil return true, nil
}) })
If you want to obtain the entire collection of pages without doing any
intermediary processing on each page, you can use the AllPages method:
allPages, err := servers.List(client, nil).AllPages()
allServers, err := servers.ExtractServers(allPages)
This top-level package contains utility functions and data types that are used This top-level package contains utility functions and data types that are used
throughout the provider and service packages. Of particular note for end users throughout the provider and service packages. Of particular note for end users
are the AuthOptions and EndpointOpts structs. are the AuthOptions and EndpointOpts structs.

View file

@ -27,7 +27,7 @@ const (
// unambiguously identify one, and only one, endpoint within the catalog. // unambiguously identify one, and only one, endpoint within the catalog.
// //
// Usually, these are passed to service client factory functions in a provider // Usually, these are passed to service client factory functions in a provider
// package, like "rackspace.NewComputeV2()". // package, like "openstack.NewComputeV2()".
type EndpointOpts struct { type EndpointOpts struct {
// Type [required] is the service type for the client (e.g., "compute", // Type [required] is the service type for the client (e.g., "compute",
// "object-store"). Generally, this will be supplied by the service client // "object-store"). Generally, this will be supplied by the service client

View file

@ -1,6 +1,9 @@
package gophercloud package gophercloud
import "fmt" import (
"fmt"
"strings"
)
// BaseError is an error type that all other error types embed. // BaseError is an error type that all other error types embed.
type BaseError struct { type BaseError struct {
@ -43,6 +46,33 @@ func (e ErrInvalidInput) Error() string {
return e.choseErrString() return e.choseErrString()
} }
// ErrMissingEnvironmentVariable is the error when environment variable is required
// in a particular situation but not provided by the user
type ErrMissingEnvironmentVariable struct {
BaseError
EnvironmentVariable string
}
func (e ErrMissingEnvironmentVariable) Error() string {
e.DefaultErrString = fmt.Sprintf("Missing environment variable [%s]", e.EnvironmentVariable)
return e.choseErrString()
}
// ErrMissingAnyoneOfEnvironmentVariables is the error when anyone of the environment variables
// is required in a particular situation but not provided by the user
type ErrMissingAnyoneOfEnvironmentVariables struct {
BaseError
EnvironmentVariables []string
}
func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string {
e.DefaultErrString = fmt.Sprintf(
"Missing one of the following environment variables [%s]",
strings.Join(e.EnvironmentVariables, ", "),
)
return e.choseErrString()
}
// ErrUnexpectedResponseCode is returned by the Request method when a response code other than // ErrUnexpectedResponseCode is returned by the Request method when a response code other than
// those listed in OkCodes is encountered. // those listed in OkCodes is encountered.
type ErrUnexpectedResponseCode struct { type ErrUnexpectedResponseCode struct {
@ -72,6 +102,11 @@ type ErrDefault401 struct {
ErrUnexpectedResponseCode ErrUnexpectedResponseCode
} }
// ErrDefault403 is the default error type returned on a 403 HTTP response code.
type ErrDefault403 struct {
ErrUnexpectedResponseCode
}
// ErrDefault404 is the default error type returned on a 404 HTTP response code. // ErrDefault404 is the default error type returned on a 404 HTTP response code.
type ErrDefault404 struct { type ErrDefault404 struct {
ErrUnexpectedResponseCode ErrUnexpectedResponseCode
@ -103,11 +138,22 @@ type ErrDefault503 struct {
} }
func (e ErrDefault400) Error() string { func (e ErrDefault400) Error() string {
return "Invalid request due to incorrect syntax or missing required parameters." e.DefaultErrString = fmt.Sprintf(
"Bad request with: [%s %s], error message: %s",
e.Method, e.URL, e.Body,
)
return e.choseErrString()
} }
func (e ErrDefault401) Error() string { func (e ErrDefault401) Error() string {
return "Authentication failed" return "Authentication failed"
} }
func (e ErrDefault403) Error() string {
e.DefaultErrString = fmt.Sprintf(
"Request forbidden: [%s %s], error message: %s",
e.Method, e.URL, e.Body,
)
return e.choseErrString()
}
func (e ErrDefault404) Error() string { func (e ErrDefault404) Error() string {
return "Resource not found" return "Resource not found"
} }
@ -141,6 +187,12 @@ type Err401er interface {
Error401(ErrUnexpectedResponseCode) error Error401(ErrUnexpectedResponseCode) error
} }
// Err403er is the interface resource error types implement to override the error message
// from a 403 error.
type Err403er interface {
Error403(ErrUnexpectedResponseCode) error
}
// Err404er is the interface resource error types implement to override the error message // Err404er is the interface resource error types implement to override the error message
// from a 404 error. // from a 404 error.
type Err404er interface { type Err404er interface {
@ -393,16 +445,16 @@ func (e ErrScopeProjectIDAlone) Error() string {
return "ProjectID must be supplied alone in a Scope" return "ProjectID must be supplied alone in a Scope"
} }
// ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
type ErrScopeDomainName struct{ BaseError }
func (e ErrScopeDomainName) Error() string {
return "DomainName must be supplied with a ProjectName or ProjectID in a Scope"
}
// ErrScopeEmpty indicates that no credentials were provided in a Scope. // ErrScopeEmpty indicates that no credentials were provided in a Scope.
type ErrScopeEmpty struct{ BaseError } type ErrScopeEmpty struct{ BaseError }
func (e ErrScopeEmpty) Error() string { func (e ErrScopeEmpty) Error() string {
return "You must provide either a Project or Domain in a Scope" return "You must provide either a Project or Domain in a Scope"
} }
// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name
type ErrAppCredMissingSecret struct{ BaseError }
func (e ErrAppCredMissingSecret) Error() string {
return "You must provide an Application Credential Secret"
}

View file

@ -8,10 +8,27 @@ import (
var nilOptions = gophercloud.AuthOptions{} var nilOptions = gophercloud.AuthOptions{}
// AuthOptionsFromEnv fills out an identity.AuthOptions structure with the settings found on the various OpenStack /*
// OS_* environment variables. The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, AuthOptionsFromEnv fills out an identity.AuthOptions structure with the
// OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must settings found on the various OpenStack OS_* environment variables.
// have settings, or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional.
The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME.
Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings,
or an error will result. OS_TENANT_ID, OS_TENANT_NAME, OS_PROJECT_ID, and
OS_PROJECT_NAME are optional.
OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and
OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will
still be referred as "tenant" in Gophercloud.
To use this function, first set the OS_* environment variables (for example,
by sourcing an `openrc` file), then:
opts, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(opts)
*/
func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
authURL := os.Getenv("OS_AUTH_URL") authURL := os.Getenv("OS_AUTH_URL")
username := os.Getenv("OS_USERNAME") username := os.Getenv("OS_USERNAME")
@ -21,31 +38,60 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
tenantName := os.Getenv("OS_TENANT_NAME") tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID") domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME") domainName := os.Getenv("OS_DOMAIN_NAME")
applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID")
applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME")
applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET")
// If OS_PROJECT_ID is set, overwrite tenantID with the value.
if v := os.Getenv("OS_PROJECT_ID"); v != "" {
tenantID = v
}
// If OS_PROJECT_NAME is set, overwrite tenantName with the value.
if v := os.Getenv("OS_PROJECT_NAME"); v != "" {
tenantName = v
}
if authURL == "" { if authURL == "" {
err := gophercloud.ErrMissingInput{Argument: "authURL"} err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_AUTH_URL",
}
return nilOptions, err return nilOptions, err
} }
if username == "" && userID == "" { if username == "" && userID == "" {
err := gophercloud.ErrMissingInput{Argument: "username"} err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{
EnvironmentVariables: []string{"OS_USERNAME", "OS_USERID"},
}
return nilOptions, err return nilOptions, err
} }
if password == "" { if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
err := gophercloud.ErrMissingInput{Argument: "password"} err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_PASSWORD",
}
return nilOptions, err
}
if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" {
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET",
}
return nilOptions, err return nilOptions, err
} }
ao := gophercloud.AuthOptions{ ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL, IdentityEndpoint: authURL,
UserID: userID, UserID: userID,
Username: username, Username: username,
Password: password, Password: password,
TenantID: tenantID, TenantID: tenantID,
TenantName: tenantName, TenantName: tenantName,
DomainID: domainID, DomainID: domainID,
DomainName: domainName, DomainName: domainName,
ApplicationCredentialID: applicationCredentialID,
ApplicationCredentialName: applicationCredentialName,
ApplicationCredentialSecret: applicationCredentialSecret,
} }
return ao, nil return ao, nil

View file

@ -2,7 +2,6 @@ package openstack
import ( import (
"fmt" "fmt"
"net/url"
"reflect" "reflect"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
@ -12,43 +11,66 @@ import (
) )
const ( const (
v20 = "v2.0" // v2 represents Keystone v2.
v30 = "v3.0" // It should never increase beyond 2.0.
v2 = "v2.0"
// v3 represents Keystone v3.
// The version can be anything from v3 to v3.x.
v3 = "v3"
) )
// NewClient prepares an unauthenticated ProviderClient instance. /*
// Most users will probably prefer using the AuthenticatedClient function instead. NewClient prepares an unauthenticated ProviderClient instance.
// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly, Most users will probably prefer using the AuthenticatedClient function
// for example. instead.
This is useful if you wish to explicitly control the version of the identity
service that's used for authentication explicitly, for example.
A basic example of using this would be:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.NewClient(ao.IdentityEndpoint)
client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
*/
func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
u, err := url.Parse(endpoint) base, err := utils.BaseEndpoint(endpoint)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hadPath := u.Path != ""
u.Path, u.RawQuery, u.Fragment = "", "", ""
base := u.String()
endpoint = gophercloud.NormalizeURL(endpoint) endpoint = gophercloud.NormalizeURL(endpoint)
base = gophercloud.NormalizeURL(base) base = gophercloud.NormalizeURL(base)
if hadPath { p := new(gophercloud.ProviderClient)
return &gophercloud.ProviderClient{ p.IdentityBase = base
IdentityBase: base, p.IdentityEndpoint = endpoint
IdentityEndpoint: endpoint, p.UseTokenLock()
}, nil
}
return &gophercloud.ProviderClient{ return p, nil
IdentityBase: base,
IdentityEndpoint: "",
}, nil
} }
// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and /*
// returns a Client instance that's ready to operate. AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint
// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses specified by the options, acquires a token, and returns a Provider Client
// the most recent identity service available to proceed. instance that's ready to operate.
If the full path to a versioned identity endpoint was specified (example:
http://example.com:5000/v3), that path will be used as the endpoint to query.
If a versionless endpoint was specified (example: http://example.com:5000/),
the endpoint will be queried to determine which versions of the identity service
are available, then chooses the most recent or most supported version.
Example:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
client, err := openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
*/
func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
client, err := NewClient(options.IdentityEndpoint) client, err := NewClient(options.IdentityEndpoint)
if err != nil { if err != nil {
@ -62,11 +84,12 @@ func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.Provider
return client, nil return client, nil
} }
// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint. // Authenticate or re-authenticate against the most recent identity service
// supported at the provided endpoint.
func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
versions := []*utils.Version{ versions := []*utils.Version{
{ID: v20, Priority: 20, Suffix: "/v2.0/"}, {ID: v2, Priority: 20, Suffix: "/v2.0/"},
{ID: v30, Priority: 30, Suffix: "/v3/"}, {ID: v3, Priority: 30, Suffix: "/v3/"},
} }
chosen, endpoint, err := utils.ChooseVersion(client, versions) chosen, endpoint, err := utils.ChooseVersion(client, versions)
@ -75,9 +98,9 @@ func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOp
} }
switch chosen.ID { switch chosen.ID {
case v20: case v2:
return v2auth(client, endpoint, options, gophercloud.EndpointOpts{}) return v2auth(client, endpoint, options, gophercloud.EndpointOpts{})
case v30: case v3:
return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{}) return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
default: default:
// The switch statement must be out of date from the versions list. // The switch statement must be out of date from the versions list.
@ -123,9 +146,21 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
} }
if options.AllowReauth { if options.AllowReauth {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once
tac := *client
tac.ReauthFunc = nil
tac.TokenID = ""
tao := options
tao.AllowReauth = false
client.ReauthFunc = func() error { client.ReauthFunc = func() error {
client.TokenID = "" err := v2auth(&tac, endpoint, tao, eo)
return v2auth(client, endpoint, options, eo) if err != nil {
return err
}
client.TokenID = tac.TokenID
return nil
} }
} }
client.TokenID = token.ID client.TokenID = token.ID
@ -167,9 +202,32 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
client.TokenID = token.ID client.TokenID = token.ID
if opts.CanReauth() { if opts.CanReauth() {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once
tac := *client
tac.ReauthFunc = nil
tac.TokenID = ""
var tao tokens3.AuthOptionsBuilder
switch ot := opts.(type) {
case *gophercloud.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
case *tokens3.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
default:
tao = opts
}
client.ReauthFunc = func() error { client.ReauthFunc = func() error {
client.TokenID = "" err := v3auth(&tac, endpoint, tao, eo)
return v3auth(client, endpoint, opts, eo) if err != nil {
return err
}
client.TokenID = tac.TokenID
return nil
} }
} }
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
@ -179,7 +237,8 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
return nil return nil
} }
// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service. // NewIdentityV2 creates a ServiceClient that may be used to interact with the
// v2 identity service.
func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v2.0/" endpoint := client.IdentityBase + "v2.0/"
clientType := "identity" clientType := "identity"
@ -199,7 +258,8 @@ func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
}, nil }, nil
} }
// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service. // NewIdentityV3 creates a ServiceClient that may be used to access the v3
// identity service.
func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
endpoint := client.IdentityBase + "v3/" endpoint := client.IdentityBase + "v3/"
clientType := "identity" clientType := "identity"
@ -212,6 +272,19 @@ func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp
} }
} }
// Ensure endpoint still has a suffix of v3.
// This is because EndpointLocator might have found a versionless
// endpoint or the published endpoint is still /v2.0. In both
// cases, we need to fix the endpoint to point to /v3.
base, err := utils.BaseEndpoint(endpoint)
if err != nil {
return nil, err
}
base = gophercloud.NormalizeURL(base)
endpoint = base + "v3/"
return &gophercloud.ServiceClient{ return &gophercloud.ServiceClient{
ProviderClient: client, ProviderClient: client,
Endpoint: endpoint, Endpoint: endpoint,
@ -232,33 +305,43 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
return sc, nil return sc, nil
} }
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 object storage package. // NewObjectStorageV1 creates a ServiceClient that may be used with the v1
// object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "object-store") return initClientOpts(client, eo, "object-store")
} }
// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package. // NewComputeV2 creates a ServiceClient that may be used with the v2 compute
// package.
func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "compute") return initClientOpts(client, eo, "compute")
} }
// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package. // NewNetworkV2 creates a ServiceClient that may be used with the v2 network
// package.
func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "network") sc, err := initClientOpts(client, eo, "network")
sc.ResourceBase = sc.Endpoint + "v2.0/" sc.ResourceBase = sc.Endpoint + "v2.0/"
return sc, err return sc, err
} }
// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service. // NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
// block storage service.
func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volume") return initClientOpts(client, eo, "volume")
} }
// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 block storage service. // NewBlockStorageV2 creates a ServiceClient that may be used to access the v2
// block storage service.
func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volumev2") return initClientOpts(client, eo, "volumev2")
} }
// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service.
func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "volumev3")
}
// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. // NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "sharev2") return initClientOpts(client, eo, "sharev2")
@ -270,7 +353,8 @@ func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (
return initClientOpts(client, eo, "cdn") return initClientOpts(client, eo, "cdn")
} }
// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 orchestration service. // NewOrchestrationV1 creates a ServiceClient that may be used to access the v1
// orchestration service.
func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "orchestration") return initClientOpts(client, eo, "orchestration")
} }
@ -280,16 +364,64 @@ func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*
return initClientOpts(client, eo, "database") return initClientOpts(client, eo, "database")
} }
// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS service. // NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS
// service.
func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "dns") sc, err := initClientOpts(client, eo, "dns")
sc.ResourceBase = sc.Endpoint + "v2/" sc.ResourceBase = sc.Endpoint + "v2/"
return sc, err return sc, err
} }
// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 image service. // NewImageServiceV2 creates a ServiceClient that may be used to access the v2
// image service.
func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "image") sc, err := initClientOpts(client, eo, "image")
sc.ResourceBase = sc.Endpoint + "v2/" sc.ResourceBase = sc.Endpoint + "v2/"
return sc, err return sc, err
} }
// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2
// load balancer service.
func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "load-balancer")
sc.ResourceBase = sc.Endpoint + "v2.0/"
return sc, err
}
// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering
// package.
func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "clustering")
}
// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
// service.
func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "messaging")
sc.MoreHeaders = map[string]string{"Client-ID": clientID}
return sc, err
}
// NewContainerV1 creates a ServiceClient that may be used with v1 container package
func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "container")
}
// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key
// manager service.
func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "key-manager")
sc.ResourceBase = sc.Endpoint + "v1/"
return sc, err
}
// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management
// package.
func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "container-infra")
}
// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package.
func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "workflowv2")
}

View file

@ -1,3 +1,68 @@
// Package floatingips provides the ability to manage floating ips through /*
// nova-network Package floatingips provides the ability to manage floating ips through the
Nova API.
This API has been deprecated and will be removed from a future release of the
Nova API service.
For environements that support this extension, this package can be used
regardless of if either Neutron or nova-network is used as the cloud's network
service.
Example to List Floating IPs
allPages, err := floatingips.List(computeClient).AllPages()
if err != nil {
panic(err)
}
allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages)
if err != nil {
panic(err)
}
for _, fip := range allFloatingIPs {
fmt.Printf("%+v\n", fip)
}
Example to Create a Floating IP
createOpts := floatingips.CreateOpts{
Pool: "nova",
}
fip, err := floatingips.Create(computeClient, createOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete a Floating IP
err := floatingips.Delete(computeClient, "floatingip-id").ExtractErr()
if err != nil {
panic(err)
}
Example to Associate a Floating IP With a Server
associateOpts := floatingips.AssociateOpts{
FloatingIP: "10.10.10.2",
}
err := floatingips.AssociateInstance(computeClient, "server-id", associateOpts).ExtractErr()
if err != nil {
panic(err)
}
Example to Disassociate a Floating IP From a Server
disassociateOpts := floatingips.DisassociateOpts{
FloatingIP: "10.10.10.2",
}
err := floatingips.DisassociateInstance(computeClient, "server-id", disassociateOpts).ExtractErr()
if err != nil {
panic(err)
}
*/
package floatingips package floatingips

View file

@ -12,15 +12,15 @@ func List(client *gophercloud.ServiceClient) pagination.Pager {
}) })
} }
// CreateOptsBuilder describes struct types that can be accepted by the Create call. Notable, the // CreateOptsBuilder allows extensions to add additional parameters to the
// CreateOpts struct in this package does. // Create request.
type CreateOptsBuilder interface { type CreateOptsBuilder interface {
ToFloatingIPCreateMap() (map[string]interface{}, error) ToFloatingIPCreateMap() (map[string]interface{}, error)
} }
// CreateOpts specifies a Floating IP allocation request // CreateOpts specifies a Floating IP allocation request.
type CreateOpts struct { type CreateOpts struct {
// Pool is the pool of floating IPs to allocate one from // Pool is the pool of Floating IPs to allocate one from.
Pool string `json:"pool" required:"true"` Pool string `json:"pool" required:"true"`
} }
@ -29,7 +29,7 @@ func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "") return gophercloud.BuildRequestBody(opts, "")
} }
// Create requests the creation of a new floating IP // Create requests the creation of a new Floating IP.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToFloatingIPCreateMap() b, err := opts.ToFloatingIPCreateMap()
if err != nil { if err != nil {
@ -42,29 +42,30 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
// Get returns data about a previously created FloatingIP. // Get returns data about a previously created Floating IP.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) _, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return return
} }
// Delete requests the deletion of a previous allocated FloatingIP. // Delete requests the deletion of a previous allocated Floating IP.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) _, r.Err = client.Delete(deleteURL(client, id), nil)
return return
} }
// AssociateOptsBuilder is the interface types must satfisfy to be used as // AssociateOptsBuilder allows extensions to add additional parameters to the
// Associate options // Associate request.
type AssociateOptsBuilder interface { type AssociateOptsBuilder interface {
ToFloatingIPAssociateMap() (map[string]interface{}, error) ToFloatingIPAssociateMap() (map[string]interface{}, error)
} }
// AssociateOpts specifies the required information to associate a floating IP with an instance // AssociateOpts specifies the required information to associate a Floating IP with an instance
type AssociateOpts struct { type AssociateOpts struct {
// FloatingIP is the floating IP to associate with an instance // FloatingIP is the Floating IP to associate with an instance.
FloatingIP string `json:"address" required:"true"` FloatingIP string `json:"address" required:"true"`
// FixedIP is an optional fixed IP address of the server
// FixedIP is an optional fixed IP address of the server.
FixedIP string `json:"fixed_address,omitempty"` FixedIP string `json:"fixed_address,omitempty"`
} }
@ -73,7 +74,7 @@ func (opts AssociateOpts) ToFloatingIPAssociateMap() (map[string]interface{}, er
return gophercloud.BuildRequestBody(opts, "addFloatingIp") return gophercloud.BuildRequestBody(opts, "addFloatingIp")
} }
// AssociateInstance pairs an allocated floating IP with an instance. // AssociateInstance pairs an allocated Floating IP with a server.
func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) { func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts AssociateOptsBuilder) (r AssociateResult) {
b, err := opts.ToFloatingIPAssociateMap() b, err := opts.ToFloatingIPAssociateMap()
if err != nil { if err != nil {
@ -84,23 +85,24 @@ func AssociateInstance(client *gophercloud.ServiceClient, serverID string, opts
return return
} }
// DisassociateOptsBuilder is the interface types must satfisfy to be used as // DisassociateOptsBuilder allows extensions to add additional parameters to
// Disassociate options // the Disassociate request.
type DisassociateOptsBuilder interface { type DisassociateOptsBuilder interface {
ToFloatingIPDisassociateMap() (map[string]interface{}, error) ToFloatingIPDisassociateMap() (map[string]interface{}, error)
} }
// DisassociateOpts specifies the required information to disassociate a floating IP with an instance // DisassociateOpts specifies the required information to disassociate a
// Floating IP with a server.
type DisassociateOpts struct { type DisassociateOpts struct {
FloatingIP string `json:"address" required:"true"` FloatingIP string `json:"address" required:"true"`
} }
// ToFloatingIPDisassociateMap constructs a request body from AssociateOpts. // ToFloatingIPDisassociateMap constructs a request body from DisassociateOpts.
func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) { func (opts DisassociateOpts) ToFloatingIPDisassociateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "removeFloatingIp") return gophercloud.BuildRequestBody(opts, "removeFloatingIp")
} }
// DisassociateInstance decouples an allocated floating IP from an instance // DisassociateInstance decouples an allocated Floating IP from an instance
func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) { func DisassociateInstance(client *gophercloud.ServiceClient, serverID string, opts DisassociateOptsBuilder) (r DisassociateResult) {
b, err := opts.ToFloatingIPDisassociateMap() b, err := opts.ToFloatingIPDisassociateMap()
if err != nil { if err != nil {

View file

@ -8,21 +8,21 @@ import (
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
// A FloatingIP is an IP that can be associated with an instance // A FloatingIP is an IP that can be associated with a server.
type FloatingIP struct { type FloatingIP struct {
// ID is a unique ID of the Floating IP // ID is a unique ID of the Floating IP
ID string `json:"-"` ID string `json:"-"`
// FixedIP is the IP of the instance related to the Floating IP // FixedIP is a specific IP on the server to pair the Floating IP with.
FixedIP string `json:"fixed_ip,omitempty"` FixedIP string `json:"fixed_ip,omitempty"`
// InstanceID is the ID of the instance that is using the Floating IP // InstanceID is the ID of the server that is using the Floating IP.
InstanceID string `json:"instance_id"` InstanceID string `json:"instance_id"`
// IP is the actual Floating IP // IP is the actual Floating IP.
IP string `json:"ip"` IP string `json:"ip"`
// Pool is the pool of floating IPs that this floating IP belongs to // Pool is the pool of Floating IPs that this Floating IP belongs to.
Pool string `json:"pool"` Pool string `json:"pool"`
} }
@ -49,8 +49,7 @@ func (r *FloatingIP) UnmarshalJSON(b []byte) error {
return err return err
} }
// FloatingIPPage stores a single, only page of FloatingIPs // FloatingIPPage stores a single page of FloatingIPs from a List call.
// results from a List call.
type FloatingIPPage struct { type FloatingIPPage struct {
pagination.SinglePageBase pagination.SinglePageBase
} }
@ -61,8 +60,7 @@ func (page FloatingIPPage) IsEmpty() (bool, error) {
return len(va) == 0, err return len(va) == 0, err
} }
// ExtractFloatingIPs interprets a page of results as a slice of // ExtractFloatingIPs interprets a page of results as a slice of FloatingIPs.
// FloatingIPs.
func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) {
var s struct { var s struct {
FloatingIPs []FloatingIP `json:"floating_ips"` FloatingIPs []FloatingIP `json:"floating_ips"`
@ -86,32 +84,32 @@ func (r FloatingIPResult) Extract() (*FloatingIP, error) {
return s.FloatingIP, err return s.FloatingIP, err
} }
// CreateResult is the response from a Create operation. Call its Extract method to interpret it // CreateResult is the response from a Create operation. Call its Extract method
// as a FloatingIP. // to interpret it as a FloatingIP.
type CreateResult struct { type CreateResult struct {
FloatingIPResult FloatingIPResult
} }
// GetResult is the response from a Get operation. Call its Extract method to interpret it // GetResult is the response from a Get operation. Call its Extract method to
// as a FloatingIP. // interpret it as a FloatingIP.
type GetResult struct { type GetResult struct {
FloatingIPResult FloatingIPResult
} }
// DeleteResult is the response from a Delete operation. Call its Extract method to determine if // DeleteResult is the response from a Delete operation. Call its ExtractErr
// the call succeeded or failed. // method to determine if the call succeeded or failed.
type DeleteResult struct { type DeleteResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
// AssociateResult is the response from a Delete operation. Call its Extract method to determine if // AssociateResult is the response from a Delete operation. Call its ExtractErr
// the call succeeded or failed. // method to determine if the call succeeded or failed.
type AssociateResult struct { type AssociateResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
// DisassociateResult is the response from a Delete operation. Call its Extract method to determine if // DisassociateResult is the response from a Delete operation. Call its
// the call succeeded or failed. // ExtractErr method to determine if the call succeeded or failed.
type DisassociateResult struct { type DisassociateResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }

View file

@ -1,3 +1,51 @@
// Package hypervisors gives information and control of the os-hypervisors /*
// portion of the compute API Package hypervisors returns details about list of hypervisors, shows details for a hypervisor
and shows summary statistics for all hypervisors over all compute nodes in the OpenStack cloud.
Example of Show Hypervisor Details
hypervisorID := 42
hypervisor, err := hypervisors.Get(computeClient, 42).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", hypervisor)
Example of Retrieving Details of All Hypervisors
allPages, err := hypervisors.List(computeClient).AllPages()
if err != nil {
panic(err)
}
allHypervisors, err := hypervisors.ExtractHypervisors(allPages)
if err != nil {
panic(err)
}
for _, hypervisor := range allHypervisors {
fmt.Printf("%+v\n", hypervisor)
}
Example of Show Hypervisor Statistics
hypervisorsStatistics, err := hypervisors.GetStatistics(computeClient).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", hypervisorsStatistics)
Example of Show Hypervisor Uptime
hypervisorID := 42
hypervisorUptime, err := hypervisors.GetUptime(computeClient, hypervisorID).Extract()
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", hypervisorUptime)
*/
package hypervisors package hypervisors

View file

@ -1,6 +1,8 @@
package hypervisors package hypervisors
import ( import (
"strconv"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
@ -11,3 +13,29 @@ func List(client *gophercloud.ServiceClient) pagination.Pager {
return HypervisorPage{pagination.SinglePageBase(r)} return HypervisorPage{pagination.SinglePageBase(r)}
}) })
} }
// Statistics makes a request against the API to get hypervisors statistics.
func GetStatistics(client *gophercloud.ServiceClient) (r StatisticsResult) {
_, r.Err = client.Get(hypervisorsStatisticsURL(client), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Get makes a request against the API to get details for specific hypervisor.
func Get(client *gophercloud.ServiceClient, hypervisorID int) (r HypervisorResult) {
v := strconv.Itoa(hypervisorID)
_, r.Err = client.Get(hypervisorsGetURL(client, v), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// GetUptime makes a request against the API to get uptime for specific hypervisor.
func GetUptime(client *gophercloud.ServiceClient, hypervisorID int) (r UptimeResult) {
v := strconv.Itoa(hypervisorID)
_, r.Err = client.Get(hypervisorsUptimeURL(client, v), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}

View file

@ -4,15 +4,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
// Topology represents a CPU Topology.
type Topology struct { type Topology struct {
Sockets int `json:"sockets"` Sockets int `json:"sockets"`
Cores int `json:"cores"` Cores int `json:"cores"`
Threads int `json:"threads"` Threads int `json:"threads"`
} }
// CPUInfo represents CPU information of the hypervisor.
type CPUInfo struct { type CPUInfo struct {
Vendor string `json:"vendor"` Vendor string `json:"vendor"`
Arch string `json:"arch"` Arch string `json:"arch"`
@ -21,59 +24,82 @@ type CPUInfo struct {
Topology Topology `json:"topology"` Topology Topology `json:"topology"`
} }
// Service represents a Compute service running on the hypervisor.
type Service struct { type Service struct {
Host string `json:"host"` Host string `json:"host"`
ID int `json:"id"` ID int `json:"id"`
DisabledReason string `json:"disabled_reason"` DisabledReason string `json:"disabled_reason"`
} }
// Hypervisor represents a hypervisor in the OpenStack cloud.
type Hypervisor struct { type Hypervisor struct {
// A structure that contains cpu information like arch, model, vendor, features and topology // A structure that contains cpu information like arch, model, vendor,
// features and topology.
CPUInfo CPUInfo `json:"-"` CPUInfo CPUInfo `json:"-"`
// The current_workload is the number of tasks the hypervisor is responsible for.
// This will be equal or greater than the number of active VMs on the system // The current_workload is the number of tasks the hypervisor is responsible
// (it can be greater when VMs are being deleted and the hypervisor is still cleaning up). // for. This will be equal or greater than the number of active VMs on the
// system (it can be greater when VMs are being deleted and the hypervisor is
// still cleaning up).
CurrentWorkload int `json:"current_workload"` CurrentWorkload int `json:"current_workload"`
// Status of the hypervisor, either "enabled" or "disabled"
// Status of the hypervisor, either "enabled" or "disabled".
Status string `json:"status"` Status string `json:"status"`
// State of the hypervisor, either "up" or "down"
// State of the hypervisor, either "up" or "down".
State string `json:"state"` State string `json:"state"`
// Actual free disk on this hypervisor in GB
// DiskAvailableLeast is the actual free disk on this hypervisor,
// measured in GB.
DiskAvailableLeast int `json:"disk_available_least"` DiskAvailableLeast int `json:"disk_available_least"`
// The hypervisor's IP address
// HostIP is the hypervisor's IP address.
HostIP string `json:"host_ip"` HostIP string `json:"host_ip"`
// The free disk remaining on this hypervisor in GB
// FreeDiskGB is the free disk remaining on the hypervisor, measured in GB.
FreeDiskGB int `json:"-"` FreeDiskGB int `json:"-"`
// The free RAM in this hypervisor in MB
// FreeRAMMB is the free RAM in the hypervisor, measured in MB.
FreeRamMB int `json:"free_ram_mb"` FreeRamMB int `json:"free_ram_mb"`
// The hypervisor host name
// HypervisorHostname is the hostname of the hypervisor.
HypervisorHostname string `json:"hypervisor_hostname"` HypervisorHostname string `json:"hypervisor_hostname"`
// The hypervisor type
// HypervisorType is the type of hypervisor.
HypervisorType string `json:"hypervisor_type"` HypervisorType string `json:"hypervisor_type"`
// The hypervisor version
// HypervisorVersion is the version of the hypervisor.
HypervisorVersion int `json:"-"` HypervisorVersion int `json:"-"`
// Unique ID of the hypervisor
// ID is the unique ID of the hypervisor.
ID int `json:"id"` ID int `json:"id"`
// The disk in this hypervisor in GB
// LocalGB is the disk space in the hypervisor, measured in GB.
LocalGB int `json:"-"` LocalGB int `json:"-"`
// The disk used in this hypervisor in GB
// LocalGBUsed is the used disk space of the hypervisor, measured in GB.
LocalGBUsed int `json:"local_gb_used"` LocalGBUsed int `json:"local_gb_used"`
// The memory of this hypervisor in MB
// MemoryMB is the total memory of the hypervisor, measured in MB.
MemoryMB int `json:"memory_mb"` MemoryMB int `json:"memory_mb"`
// The memory used in this hypervisor in MB
// MemoryMBUsed is the used memory of the hypervisor, measured in MB.
MemoryMBUsed int `json:"memory_mb_used"` MemoryMBUsed int `json:"memory_mb_used"`
// The number of running vms on this hypervisor
// RunningVMs is the The number of running vms on the hypervisor.
RunningVMs int `json:"running_vms"` RunningVMs int `json:"running_vms"`
// The hypervisor service object
// Service is the service this hypervisor represents.
Service Service `json:"service"` Service Service `json:"service"`
// The number of vcpu in this hypervisor
// VCPUs is the total number of vcpus on the hypervisor.
VCPUs int `json:"vcpus"` VCPUs int `json:"vcpus"`
// The number of vcpu used in this hypervisor
// VCPUsUsed is the number of used vcpus on the hypervisor.
VCPUsUsed int `json:"vcpus_used"` VCPUsUsed int `json:"vcpus_used"`
} }
func (r *Hypervisor) UnmarshalJSON(b []byte) error { func (r *Hypervisor) UnmarshalJSON(b []byte) error {
type tmp Hypervisor type tmp Hypervisor
var s struct { var s struct {
tmp tmp
@ -90,9 +116,9 @@ func (r *Hypervisor) UnmarshalJSON(b []byte) error {
*r = Hypervisor(s.tmp) *r = Hypervisor(s.tmp)
// Newer versions pass the CPU into around as the correct types, this just needs // Newer versions return the CPU info as the correct type.
// converting and copying into place. Older versions pass CPU info around as a string // Older versions return the CPU info as a string and need to be
// and can simply be unmarshalled by the json parser // unmarshalled by the json parser.
var tmpb []byte var tmpb []byte
switch t := s.CPUInfo.(type) { switch t := s.CPUInfo.(type) {
@ -112,7 +138,8 @@ func (r *Hypervisor) UnmarshalJSON(b []byte) error {
return err return err
} }
// These fields may be passed in in scientific notation // These fields may be returned as a scientific notation, so they need
// converted to int.
switch t := s.HypervisorVersion.(type) { switch t := s.HypervisorVersion.(type) {
case int: case int:
r.HypervisorVersion = t r.HypervisorVersion = t
@ -143,15 +170,19 @@ func (r *Hypervisor) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// HypervisorPage represents a single page of all Hypervisors from a List
// request.
type HypervisorPage struct { type HypervisorPage struct {
pagination.SinglePageBase pagination.SinglePageBase
} }
// IsEmpty determines whether or not a HypervisorPage is empty.
func (page HypervisorPage) IsEmpty() (bool, error) { func (page HypervisorPage) IsEmpty() (bool, error) {
va, err := ExtractHypervisors(page) va, err := ExtractHypervisors(page)
return len(va) == 0, err return len(va) == 0, err
} }
// ExtractHypervisors interprets a page of results as a slice of Hypervisors.
func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) { func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) {
var h struct { var h struct {
Hypervisors []Hypervisor `json:"hypervisors"` Hypervisors []Hypervisor `json:"hypervisors"`
@ -159,3 +190,101 @@ func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) {
err := (p.(HypervisorPage)).ExtractInto(&h) err := (p.(HypervisorPage)).ExtractInto(&h)
return h.Hypervisors, err return h.Hypervisors, err
} }
type HypervisorResult struct {
gophercloud.Result
}
// Extract interprets any HypervisorResult as a Hypervisor, if possible.
func (r HypervisorResult) Extract() (*Hypervisor, error) {
var s struct {
Hypervisor Hypervisor `json:"hypervisor"`
}
err := r.ExtractInto(&s)
return &s.Hypervisor, err
}
// Statistics represents a summary statistics for all enabled
// hypervisors over all compute nodes in the OpenStack cloud.
type Statistics struct {
// The number of hypervisors.
Count int `json:"count"`
// The current_workload is the number of tasks the hypervisor is responsible for
CurrentWorkload int `json:"current_workload"`
// The actual free disk on this hypervisor(in GB).
DiskAvailableLeast int `json:"disk_available_least"`
// The free disk remaining on this hypervisor(in GB).
FreeDiskGB int `json:"free_disk_gb"`
// The free RAM in this hypervisor(in MB).
FreeRamMB int `json:"free_ram_mb"`
// The disk in this hypervisor(in GB).
LocalGB int `json:"local_gb"`
// The disk used in this hypervisor(in GB).
LocalGBUsed int `json:"local_gb_used"`
// The memory of this hypervisor(in MB).
MemoryMB int `json:"memory_mb"`
// The memory used in this hypervisor(in MB).
MemoryMBUsed int `json:"memory_mb_used"`
// The total number of running vms on all hypervisors.
RunningVMs int `json:"running_vms"`
// The number of vcpu in this hypervisor.
VCPUs int `json:"vcpus"`
// The number of vcpu used in this hypervisor.
VCPUsUsed int `json:"vcpus_used"`
}
type StatisticsResult struct {
gophercloud.Result
}
// Extract interprets any StatisticsResult as a Statistics, if possible.
func (r StatisticsResult) Extract() (*Statistics, error) {
var s struct {
Stats Statistics `json:"hypervisor_statistics"`
}
err := r.ExtractInto(&s)
return &s.Stats, err
}
// Uptime represents uptime and additional info for a specific hypervisor.
type Uptime struct {
// The hypervisor host name provided by the Nova virt driver.
// For the Ironic driver, it is the Ironic node uuid.
HypervisorHostname string `json:"hypervisor_hostname"`
// The id of the hypervisor.
ID int `json:"id"`
// The state of the hypervisor. One of up or down.
State string `json:"state"`
// The status of the hypervisor. One of enabled or disabled.
Status string `json:"status"`
// The total uptime of the hypervisor and information about average load.
Uptime string `json:"uptime"`
}
type UptimeResult struct {
gophercloud.Result
}
// Extract interprets any UptimeResult as a Uptime, if possible.
func (r UptimeResult) Extract() (*Uptime, error) {
var s struct {
Uptime Uptime `json:"hypervisor"`
}
err := r.ExtractInto(&s)
return &s.Uptime, err
}

View file

@ -5,3 +5,15 @@ import "github.com/gophercloud/gophercloud"
func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string { func hypervisorsListDetailURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-hypervisors", "detail") return c.ServiceURL("os-hypervisors", "detail")
} }
func hypervisorsStatisticsURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-hypervisors", "statistics")
}
func hypervisorsGetURL(c *gophercloud.ServiceClient, hypervisorID string) string {
return c.ServiceURL("os-hypervisors", hypervisorID)
}
func hypervisorsUptimeURL(c *gophercloud.ServiceClient, hypervisorID string) string {
return c.ServiceURL("os-hypervisors", hypervisorID, "uptime")
}

View file

@ -1,7 +1,137 @@
// Package flavors provides information and interaction with the flavor API /*
// resource in the OpenStack Compute service. 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 A flavor is an available hardware configuration for a server. Each flavor
// time. 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 package flavors

View file

@ -11,45 +11,65 @@ type ListOptsBuilder interface {
ToFlavorListQuery() (string, error) 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 AccessType maps to OpenStack's Flavor.is_public field. Although the is_public
// allowed: 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. The AccessType arguement is optional, and if it is not supplied, OpenStack
// AllAccess (admin only): Returns public and private flavors across all projects. returns the PublicAccess flavors.
// */
// The AccessType arguement is optional, and if it is not supplied, OpenStack returns the PublicAccess
// flavors.
type AccessType string type AccessType string
const ( const (
PublicAccess AccessType = "true" // 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" PrivateAccess AccessType = "false"
AllAccess AccessType = "None"
// AllAccess (admin only) returns public and private flavors across all
// projects.
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. ListOpts filters the results returned by the List() function.
// Typically, software will use the last ID of the previous call to List to set the Marker for the current call. For example, a flavor with a minDisk field of 10 will not be returned if you
type ListOpts struct { specify MinDisk set to 20.
// ChangesSince, if provided, instructs List to return only those things which have changed since the timestamp provided. 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"` ChangesSince string `q:"changes-since"`
// MinDisk and MinRAM, if provided, elides flavors which do not meet your criteria. // MinDisk and MinRAM, if provided, elides flavors which do not meet your
// criteria.
MinDisk int `q:"minDisk"` MinDisk int `q:"minDisk"`
MinRAM int `q:"minRam"` MinRAM int `q:"minRam"`
// SortDir allows to select sort direction.
// It can be "asc" or "desc" (default).
SortDir string `q:"sort_dir"`
// SortKey allows to sort by one of the flavors attributes.
// Default is flavorid.
SortKey string `q:"sort_key"`
// Marker and Limit control paging. // Marker and Limit control paging.
// Marker instructs List where to start listing from. // Marker instructs List where to start listing from.
Marker string `q:"marker"` Marker string `q:"marker"`
// Limit instructs List to refrain from sending excessively large lists of flavors. // Limit instructs List to refrain from sending excessively large lists of
// flavors.
Limit int `q:"limit"` Limit int `q:"limit"`
// AccessType, if provided, instructs List which set of flavors to return. If IsPublic not provided, // AccessType, if provided, instructs List which set of flavors to return.
// flavors for the current project are returned. // If IsPublic not provided, flavors for the current project are returned.
AccessType AccessType `q:"is_public"` AccessType AccessType `q:"is_public"`
} }
@ -60,8 +80,8 @@ func (opts ListOpts) ToFlavorListQuery() (string, error) {
} }
// ListDetail instructs OpenStack to provide a list of flavors. // ListDetail instructs OpenStack to provide a list of flavors.
// You may provide criteria by which List curtails its results for easier processing. // You may provide criteria by which List curtails its results for easier
// See ListOpts for more details. // processing.
func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
url := listURL(client) url := listURL(client)
if opts != nil { if opts != nil {
@ -80,31 +100,42 @@ type CreateOptsBuilder interface {
ToFlavorCreateMap() (map[string]interface{}, error) ToFlavorCreateMap() (map[string]interface{}, error)
} }
// CreateOpts is passed to Create to create a flavor // CreateOpts specifies parameters used for creating a flavor.
// Source:
// https://github.com/openstack/nova/blob/stable/newton/nova/api/openstack/compute/schemas/flavor_manage.py#L20
type CreateOpts struct { type CreateOpts struct {
// Name is the name of the flavor.
Name string `json:"name" required:"true"` Name string `json:"name" required:"true"`
// memory size, in MBs
RAM int `json:"ram" 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"` VCPUs int `json:"vcpus" required:"true"`
// disk size, in GBs
Disk *int `json:"disk" required:"true"` // Disk the amount of root disk space, measured in GB.
ID string `json:"id,omitempty"` Disk *int `json:"disk" required:"true"`
// non-zero, positive
Swap *int `json:"swap,omitempty"` // 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"` RxTxFactor float64 `json:"rxtx_factor,omitempty"`
IsPublic *bool `json:"os-flavor-access:is_public,omitempty"`
// ephemeral disk size, in GBs, non-zero, positive // 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"` Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"`
} }
// ToFlavorCreateMap satisfies the CreateOptsBuilder interface // ToFlavorCreateMap constructs a request body from CreateOpts.
func (opts *CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "flavor") return gophercloud.BuildRequestBody(opts, "flavor")
} }
// Create a flavor // Create requests the creation of a new flavor.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToFlavorCreateMap() b, err := opts.ToFlavorCreateMap()
if err != nil { if err != nil {
@ -117,14 +148,177 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
// Get instructs OpenStack to provide details on a single flavor, identified by its ID. // Get retrieves details of a single flavor. Use ExtractFlavor to convert its
// Use ExtractFlavor to convert its result into a Flavor. // result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) _, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return return
} }
// IDFromName is a convienience function that returns a flavor's ID given its name. // 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 {
ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error)
}
// ExtraSpecsOpts is a map that contains key-value pairs.
type ExtraSpecsOpts map[string]string
// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on
// the contents of ExtraSpecsOpts.
func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (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.ToFlavorExtraSpecsCreateMap()
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 {
ToFlavorExtraSpecUpdateMap() (map[string]string, string, error)
}
// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on
// the contents of a ExtraSpecOpts.
func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (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.ToFlavorExtraSpecUpdateMap()
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) { func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0 count := 0
id := "" id := ""

View file

@ -12,16 +12,26 @@ type commonResult struct {
gophercloud.Result gophercloud.Result
} }
// CreateResult is the response of a Get operations. Call its Extract method to
// interpret it as a Flavor.
type CreateResult struct { type CreateResult struct {
commonResult commonResult
} }
// GetResult temporarily holds the response from a Get call. // GetResult is the response of a Get operations. Call its Extract method to
// interpret it as a Flavor.
type GetResult struct { type GetResult struct {
commonResult commonResult
} }
// Extract provides access to the individual Flavor returned by the Get and Create functions. // 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) { func (r commonResult) Extract() (*Flavor, error) {
var s struct { var s struct {
Flavor *Flavor `json:"flavor"` Flavor *Flavor `json:"flavor"`
@ -30,24 +40,35 @@ func (r commonResult) Extract() (*Flavor, error) {
return s.Flavor, err return s.Flavor, err
} }
// Flavor records represent (virtual) hardware configurations for server resources in a region. // Flavor represent (virtual) hardware configurations for server resources
// in a region.
type Flavor struct { type Flavor struct {
// The Id field contains the flavor's unique identifier. // ID is the flavor's unique ID.
// For example, this identifier will be useful when specifying which hardware configuration to use for a new server instance.
ID string `json:"id"` 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 is the amount of root disk, measured in GB.
Disk int `json:"disk"` Disk int `json:"disk"`
RAM int `json:"ram"`
// The Name field provides a human-readable moniker for the flavor. // RAM is the amount of memory, measured in MB.
Name string `json:"name"` 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"` 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 is the amount of swap space, measured in MB.
Swap int `json:"swap"` Swap int `json:"-"`
// VCPUs indicates how many (virtual) CPUs are available for this flavor. // VCPUs indicates how many (virtual) CPUs are available for this flavor.
VCPUs int `json:"vcpus"` VCPUs int `json:"vcpus"`
// IsPublic indicates whether the flavor is public. // IsPublic indicates whether the flavor is public.
IsPublic bool `json:"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 { func (r *Flavor) UnmarshalJSON(b []byte) error {
@ -82,18 +103,19 @@ func (r *Flavor) UnmarshalJSON(b []byte) error {
return nil return nil
} }
// FlavorPage contains a single page of the response from a List call. // FlavorPage contains a single page of all flavors from a ListDetails call.
type FlavorPage struct { type FlavorPage struct {
pagination.LinkedPageBase pagination.LinkedPageBase
} }
// IsEmpty determines if a page contains any results. // IsEmpty determines if a FlavorPage contains any results.
func (page FlavorPage) IsEmpty() (bool, error) { func (page FlavorPage) IsEmpty() (bool, error) {
flavors, err := ExtractFlavors(page) flavors, err := ExtractFlavors(page)
return len(flavors) == 0, err return len(flavors) == 0, err
} }
// NextPageURL uses the response's embedded link reference to navigate to the next page of results. // NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (page FlavorPage) NextPageURL() (string, error) { func (page FlavorPage) NextPageURL() (string, error) {
var s struct { var s struct {
Links []gophercloud.Link `json:"flavors_links"` Links []gophercloud.Link `json:"flavors_links"`
@ -105,7 +127,8 @@ func (page FlavorPage) NextPageURL() (string, error) {
return gophercloud.ExtractNextURL(s.Links) return gophercloud.ExtractNextURL(s.Links)
} }
// ExtractFlavors provides access to the list of flavors in a page acquired from the List operation. // ExtractFlavors provides access to the list of flavors in a page acquired
// from the ListDetail operation.
func ExtractFlavors(r pagination.Page) ([]Flavor, error) { func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
var s struct { var s struct {
Flavors []Flavor `json:"flavors"` Flavors []Flavor `json:"flavors"`
@ -113,3 +136,117 @@ func ExtractFlavors(r pagination.Page) ([]Flavor, error) {
err := (r.(FlavorPage)).ExtractInto(&s) err := (r.(FlavorPage)).ExtractInto(&s)
return s.Flavors, err 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

@ -15,3 +15,35 @@ func listURL(client *gophercloud.ServiceClient) string {
func createURL(client *gophercloud.ServiceClient) string { func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("flavors") 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

@ -1,7 +1,32 @@
// Package images provides information and interaction with the image API /*
// resource in the OpenStack Compute service. Package images provides information and interaction with the images through
// 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 This API is deprecated and will be removed from a future version of the Nova
// create custom images from cloud servers you have launched. 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 package images

View file

@ -6,26 +6,33 @@ import (
) )
// ListOptsBuilder allows extensions to add additional parameters to the // ListOptsBuilder allows extensions to add additional parameters to the
// List request. // ListDetail request.
type ListOptsBuilder interface { type ListOptsBuilder interface {
ToImageListQuery() (string, error) ToImageListQuery() (string, error)
} }
// ListOpts contain options for limiting the number of Images returned from a call to ListDetail. // ListOpts contain options filtering Images returned from a call to ListDetail.
type ListOpts struct { type ListOpts struct {
// When the image last changed status (in date-time format). // ChangesSince filters Images based on the last changed status (in date-time
// format).
ChangesSince string `q:"changes-since"` ChangesSince string `q:"changes-since"`
// The number of Images to return.
// Limit limits the number of Images to return.
Limit int `q:"limit"` Limit int `q:"limit"`
// UUID of the Image at which to set a marker.
// Mark is an Image UUID at which to set a marker.
Marker string `q:"marker"` Marker string `q:"marker"`
// The name of the Image.
// Name is the name of the Image.
Name string `q:"name"` Name string `q:"name"`
// The name of the Server (in URL format).
// Server is the name of the Server (in URL format).
Server string `q:"server"` Server string `q:"server"`
// The current status of the Image.
// Status is the current status of the Image.
Status string `q:"status"` Status string `q:"status"`
// The value of the type of image (e.g. BASE, SERVER, ALL)
// Type is the type of image (e.g. BASE, SERVER, ALL).
Type string `q:"type"` Type string `q:"type"`
} }
@ -50,8 +57,7 @@ func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) paginat
}) })
} }
// Get acquires additional detail about a specific image by ID. // Get returns data about a specific image by its ID.
// Use ExtractImage() to interpret the result as an openstack Image.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) _, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return return
@ -63,7 +69,8 @@ func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
return return
} }
// IDFromName is a convienience function that returns an image's ID given its name. // IDFromName is a convienience function that returns an image's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0 count := 0
id := "" id := ""

View file

@ -5,12 +5,14 @@ import (
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
// GetResult temporarily stores a Get response. // GetResult is the response from a Get operation. Call its Extract method to
// interpret it as an Image.
type GetResult struct { type GetResult struct {
gophercloud.Result gophercloud.Result
} }
// DeleteResult represents the result of an image.Delete operation. // DeleteResult is the result from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct { type DeleteResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
@ -24,44 +26,53 @@ func (r GetResult) Extract() (*Image, error) {
return s.Image, err return s.Image, err
} }
// Image is used for JSON (un)marshalling. // Image represents an Image returned by the Compute API.
// It provides a description of an OS image.
type Image struct { type Image struct {
// ID contains the image's unique identifier. // ID is the unique ID of an image.
ID string ID string
// Created is the date when the image was created.
Created string Created string
// MinDisk and MinRAM specify the minimum resources a server must provide to be able to install the image. // 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 MinDisk int
MinRAM 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 provides a human-readable moniker for the OS image.
Name string Name string
// The Progress and Status fields indicate image-creation status. // The Progress and Status fields indicate image-creation status.
// Any usable image will have 100% progress.
Progress int Progress int
Status string
// Status is the current status of the image.
Status string
// Update is the date when the image was updated.
Updated string Updated string
// Metadata provides free-form key/value pairs that further describe the
// image.
Metadata map[string]interface{} Metadata map[string]interface{}
} }
// ImagePage contains a single page of results from a List operation. // ImagePage contains a single page of all Images returne from a ListDetail
// Use ExtractImages to convert it into a slice of usable structs. // operation. Use ExtractImages to convert it into a slice of usable structs.
type ImagePage struct { type ImagePage struct {
pagination.LinkedPageBase pagination.LinkedPageBase
} }
// IsEmpty returns true if a page contains no Image results. // IsEmpty returns true if an ImagePage contains no Image results.
func (page ImagePage) IsEmpty() (bool, error) { func (page ImagePage) IsEmpty() (bool, error) {
images, err := ExtractImages(page) images, err := ExtractImages(page)
return len(images) == 0, err return len(images) == 0, err
} }
// NextPageURL uses the response's embedded link reference to navigate to the next page of results. // NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (page ImagePage) NextPageURL() (string, error) { func (page ImagePage) NextPageURL() (string, error) {
var s struct { var s struct {
Links []gophercloud.Link `json:"images_links"` Links []gophercloud.Link `json:"images_links"`
@ -73,7 +84,8 @@ func (page ImagePage) NextPageURL() (string, error) {
return gophercloud.ExtractNextURL(s.Links) return gophercloud.ExtractNextURL(s.Links)
} }
// ExtractImages converts a page of List results into a slice of usable Image structs. // ExtractImages converts a page of List results into a slice of usable Image
// structs.
func ExtractImages(r pagination.Page) ([]Image, error) { func ExtractImages(r pagination.Page) ([]Image, error) {
var s struct { var s struct {
Images []Image `json:"images"` Images []Image `json:"images"`

View file

@ -1,6 +1,115 @@
// Package servers provides information and interaction with the server API /*
// resource in the OpenStack Compute service. 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. 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 package servers

View file

@ -21,13 +21,13 @@ type ListOptsBuilder interface {
// the server attributes you want to see returned. Marker and Limit are used // the server attributes you want to see returned. Marker and Limit are used
// for pagination. // for pagination.
type ListOpts struct { type ListOpts struct {
// A time/date stamp for when the server last changed status. // ChangesSince is a time/date stamp for when the server last changed status.
ChangesSince string `q:"changes-since"` ChangesSince string `q:"changes-since"`
// Name of the image in URL format. // Image is the name of the image in URL format.
Image string `q:"image"` Image string `q:"image"`
// Name of the flavor in URL format. // Flavor is the name of the flavor in URL format.
Flavor string `q:"flavor"` Flavor string `q:"flavor"`
// Name of the server as a string; can be queried with regular expressions. // Name of the server as a string; can be queried with regular expressions.
@ -36,20 +36,25 @@ type ListOpts struct {
// underlying database server implemented for Compute. // underlying database server implemented for Compute.
Name string `q:"name"` Name string `q:"name"`
// Value of the status of the server so that you can filter on "ACTIVE" for example. // Status is the value of the status of the server so that you can filter on
// "ACTIVE" for example.
Status string `q:"status"` Status string `q:"status"`
// Name of the host as a string. // Host is the name of the host as a string.
Host string `q:"host"` Host string `q:"host"`
// UUID of the server at which you want to set a marker. // Marker is a UUID of the server at which you want to set a marker.
Marker string `q:"marker"` Marker string `q:"marker"`
// Integer value for the limit of values to return. // Limit is an integer value for the limit of values to return.
Limit int `q:"limit"` Limit int `q:"limit"`
// Bool to show all tenants // AllTenants is a bool to show all tenants.
AllTenants bool `q:"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. // ToServerListQuery formats a ListOpts into a query string.
@ -73,15 +78,16 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa
}) })
} }
// CreateOptsBuilder describes struct types that can be accepted by the Create call. // CreateOptsBuilder allows extensions to add additional parameters to the
// The CreateOpts struct in this package does. // Create request.
type CreateOptsBuilder interface { type CreateOptsBuilder interface {
ToServerCreateMap() (map[string]interface{}, error) ToServerCreateMap() (map[string]interface{}, error)
} }
// Network is used within CreateOpts to control a new server's network attachments. // Network is used within CreateOpts to control a new server's network
// attachments.
type Network struct { type Network struct {
// UUID of a nova-network to attach to the newly provisioned server. // UUID of a network to attach to the newly provisioned server.
// Required unless Port is provided. // Required unless Port is provided.
UUID string UUID string
@ -89,19 +95,21 @@ type Network struct {
// Required unless UUID is provided. // Required unless UUID is provided.
Port string Port string
// FixedIP [optional] specifies a fixed IPv4 address to be used on this network. // FixedIP specifies a fixed IPv4 address to be used on this network.
FixedIP string FixedIP string
} }
// Personality is an array of files that are injected into the server at launch. // Personality is an array of files that are injected into the server at launch.
type Personality []*File type Personality []*File
// File is used within CreateOpts and RebuildOpts to inject a file into the server at launch. // File is used within CreateOpts and RebuildOpts to inject a file into the
// File implements the json.Marshaler interface, so when a Create or Rebuild operation is requested, // server at launch.
// json.Marshal will call File's MarshalJSON method. // 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 { type File struct {
// Path of the file // Path of the file.
Path string Path string
// Contents of the file. Maximum content size is 255 bytes. // Contents of the file. Maximum content size is 255 bytes.
Contents []byte Contents []byte
} }
@ -123,13 +131,13 @@ type CreateOpts struct {
// Name is the name to assign to the newly launched server. // Name is the name to assign to the newly launched server.
Name string `json:"name" required:"true"` Name string `json:"name" required:"true"`
// ImageRef [optional; required if ImageName is not provided] is the ID or full // ImageRef [optional; required if ImageName is not provided] is the ID or
// URL to the image that contains the server's OS and initial state. // full URL to the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension. // Also optional if using the boot-from-volume extension.
ImageRef string `json:"imageRef"` ImageRef string `json:"imageRef"`
// ImageName [optional; required if ImageRef is not provided] is the name of the // ImageName [optional; required if ImageRef is not provided] is the name of
// image that contains the server's OS and initial state. // the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension. // Also optional if using the boot-from-volume extension.
ImageName string `json:"-"` ImageName string `json:"-"`
@ -141,7 +149,8 @@ type CreateOpts struct {
// the flavor that describes the server's specs. // the flavor that describes the server's specs.
FlavorName string `json:"-"` FlavorName string `json:"-"`
// SecurityGroups lists the names of the security groups to which this server should belong. // SecurityGroups lists the names of the security groups to which this server
// should belong.
SecurityGroups []string `json:"-"` SecurityGroups []string `json:"-"`
// UserData contains configuration information or scripts to use upon launch. // UserData contains configuration information or scripts to use upon launch.
@ -152,10 +161,12 @@ type CreateOpts struct {
AvailabilityZone string `json:"availability_zone,omitempty"` AvailabilityZone string `json:"availability_zone,omitempty"`
// Networks dictates how this server will be attached to available networks. // 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. // By default, the server will be attached to all isolated networks for the
// tenant.
Networks []Network `json:"-"` Networks []Network `json:"-"`
// Metadata contains key-value pairs (up to 255 bytes each) to attach to the server. // Metadata contains key-value pairs (up to 255 bytes each) to attach to the
// server.
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
// Personality includes files to inject into the server at launch. // Personality includes files to inject into the server at launch.
@ -166,7 +177,7 @@ type CreateOpts struct {
ConfigDrive *bool `json:"config_drive,omitempty"` ConfigDrive *bool `json:"config_drive,omitempty"`
// AdminPass sets the root user password. If not set, a randomly-generated // AdminPass sets the root user password. If not set, a randomly-generated
// password will be created and returned in the rponse. // password will be created and returned in the response.
AdminPass string `json:"adminPass,omitempty"` AdminPass string `json:"adminPass,omitempty"`
// AccessIPv4 specifies an IPv4 address for the instance. // AccessIPv4 specifies an IPv4 address for the instance.
@ -180,7 +191,8 @@ type CreateOpts struct {
ServiceClient *gophercloud.ServiceClient `json:"-"` ServiceClient *gophercloud.ServiceClient `json:"-"`
} }
// ToServerCreateMap assembles a request body based on the contents of a CreateOpts. // ToServerCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
sc := opts.ServiceClient sc := opts.ServiceClient
opts.ServiceClient = nil opts.ServiceClient = nil
@ -274,13 +286,14 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
// Delete requests that a server previously provisioned be removed from your account. // Delete requests that a server previously provisioned be removed from your
// account.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) _, r.Err = client.Delete(deleteURL(client, id), nil)
return return
} }
// ForceDelete forces the deletion of a server // ForceDelete forces the deletion of a server.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
return return
@ -294,12 +307,14 @@ func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
return return
} }
// UpdateOptsBuilder allows extensions to add additional attributes to the Update request. // UpdateOptsBuilder allows extensions to add additional attributes to the
// Update request.
type UpdateOptsBuilder interface { type UpdateOptsBuilder interface {
ToServerUpdateMap() (map[string]interface{}, error) ToServerUpdateMap() (map[string]interface{}, error)
} }
// UpdateOpts specifies the base attributes that may be updated on an existing server. // UpdateOpts specifies the base attributes that may be updated on an existing
// server.
type UpdateOpts struct { type UpdateOpts struct {
// Name changes the displayed name of the server. // Name changes the displayed name of the server.
// The server host name will *not* change. // The server host name will *not* change.
@ -331,7 +346,8 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
return return
} }
// ChangeAdminPassword alters the administrator or root password for a specified server. // ChangeAdminPassword alters the administrator or root password for a specified
// server.
func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) {
b := map[string]interface{}{ b := map[string]interface{}{
"changePassword": map[string]string{ "changePassword": map[string]string{
@ -354,33 +370,38 @@ const (
PowerCycle = HardReboot PowerCycle = HardReboot
) )
// RebootOptsBuilder is an interface that options must satisfy in order to be // RebootOptsBuilder allows extensions to add additional parameters to the
// used when rebooting a server instance // reboot request.
type RebootOptsBuilder interface { type RebootOptsBuilder interface {
ToServerRebootMap() (map[string]interface{}, error) ToServerRebootMap() (map[string]interface{}, error)
} }
// RebootOpts satisfies the RebootOptsBuilder interface // RebootOpts provides options to the reboot request.
type RebootOpts struct { type RebootOpts struct {
// Type is the type of reboot to perform on the server.
Type RebootMethod `json:"type" required:"true"` Type RebootMethod `json:"type" required:"true"`
} }
// ToServerRebootMap allows RebootOpts to satisfiy the RebootOptsBuilder // ToServerRebootMap builds a body for the reboot request.
// interface func (opts RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
func (opts *RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "reboot") return gophercloud.BuildRequestBody(opts, "reboot")
} }
// Reboot requests that a given server reboot. /*
// Two methods exist for rebooting a server: Reboot requests that a given server reboot.
//
// HardReboot (aka PowerCycle) starts the server instance by physically cutting power to the machine, or if a VM, Two methods exist for rebooting a server:
// terminating it at the hypervisor level.
// It's done. Caput. Full stop. HardReboot (aka PowerCycle) starts the server instance by physically cutting
// Then, after a brief while, power is rtored or the VM instance rtarted. power to the machine, or if a VM, terminating it at the hypervisor level.
// It's done. Caput. Full stop.
// SoftReboot (aka OSReboot) simply tells the OS to rtart under its own procedur. Then, after a brief while, power is rtored or the VM instance restarted.
// E.g., in Linux, asking it to enter runlevel 6, or executing "sudo shutdown -r now", or by asking Windows to rtart the machine.
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) { func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) {
b, err := opts.ToServerRebootMap() b, err := opts.ToServerRebootMap()
if err != nil { if err != nil {
@ -391,31 +412,43 @@ func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder
return return
} }
// RebuildOptsBuilder is an interface that allows extensions to override the // RebuildOptsBuilder allows extensions to provide additional parameters to the
// default behaviour of rebuild options // rebuild request.
type RebuildOptsBuilder interface { type RebuildOptsBuilder interface {
ToServerRebuildMap() (map[string]interface{}, error) ToServerRebuildMap() (map[string]interface{}, error)
} }
// RebuildOpts represents the configuration options used in a server rebuild // RebuildOpts represents the configuration options used in a server rebuild
// operation // operation.
type RebuildOpts struct { type RebuildOpts struct {
// The server's admin password // AdminPass is the server's admin password
AdminPass string `json:"adminPass,omitempty"` AdminPass string `json:"adminPass,omitempty"`
// The ID of the image you want your server to be provisioned on
ImageID string `json:"imageRef"` // 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:"-"` ImageName string `json:"-"`
// Name to set the server to // Name to set the server to
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// AccessIPv4 [optional] provides a new IPv4 address for the instance. // AccessIPv4 [optional] provides a new IPv4 address for the instance.
AccessIPv4 string `json:"accessIPv4,omitempty"` AccessIPv4 string `json:"accessIPv4,omitempty"`
// AccessIPv6 [optional] provides a new IPv6 address for the instance. // AccessIPv6 [optional] provides a new IPv6 address for the instance.
AccessIPv6 string `json:"accessIPv6,omitempty"` AccessIPv6 string `json:"accessIPv6,omitempty"`
// Metadata [optional] contains key-value pairs (up to 255 bytes each) to attach to the server.
// Metadata [optional] contains key-value pairs (up to 255 bytes each)
// to attach to the server.
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
// Personality [optional] includes files to inject into the server at launch. // Personality [optional] includes files to inject into the server at launch.
// Rebuild will base64-encode file contents for you. // Rebuild will base64-encode file contents for you.
Personality Personality `json:"personality,omitempty"` 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:"-"` ServiceClient *gophercloud.ServiceClient `json:"-"`
} }
@ -458,31 +491,34 @@ func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuild
return return
} }
// ResizeOptsBuilder is an interface that allows extensions to override the default structure of // ResizeOptsBuilder allows extensions to add additional parameters to the
// a Resize request. // resize request.
type ResizeOptsBuilder interface { type ResizeOptsBuilder interface {
ToServerResizeMap() (map[string]interface{}, error) ToServerResizeMap() (map[string]interface{}, error)
} }
// ResizeOpts represents the configuration options used to control a Resize operation. // ResizeOpts represents the configuration options used to control a Resize
// operation.
type ResizeOpts struct { type ResizeOpts struct {
// FlavorRef is the ID of the flavor you wish your server to become. // FlavorRef is the ID of the flavor you wish your server to become.
FlavorRef string `json:"flavorRef" required:"true"` FlavorRef string `json:"flavorRef" required:"true"`
} }
// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON request body for the // ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON
// Resize request. // request body for the Resize request.
func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "resize") return gophercloud.BuildRequestBody(opts, "resize")
} }
// Resize instructs the provider to change the flavor of the server. // Resize instructs the provider to change the flavor of the server.
//
// Note that this implies rebuilding it. // Note that this implies rebuilding it.
//
// Unfortunately, one cannot pass rebuild parameters to the resize function. // Unfortunately, one cannot pass rebuild parameters to the resize function.
// When the resize completes, the server will be in RESIZE_VERIFY state. // 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. // While in this state, you can explore the use of the new server's
// If you like it, call ConfirmResize() to commit the resize permanently. // configuration. If you like it, call ConfirmResize() to commit the resize
// Otherwise, call RevertResize() to restore the old configuration. // permanently. Otherwise, call RevertResize() to restore the old configuration.
func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) {
b, err := opts.ToServerResizeMap() b, err := opts.ToServerResizeMap()
if err != nil { if err != nil {
@ -509,41 +545,8 @@ func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult)
return return
} }
// RescueOptsBuilder is an interface that allows extensions to override the // ResetMetadataOptsBuilder allows extensions to add additional parameters to
// default structure of a Rescue request. // the Reset 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 { type ResetMetadataOptsBuilder interface {
ToMetadataResetMap() (map[string]interface{}, error) ToMetadataResetMap() (map[string]interface{}, error)
} }
@ -551,20 +554,23 @@ type ResetMetadataOptsBuilder interface {
// MetadataOpts is a map that contains key-value pairs. // MetadataOpts is a map that contains key-value pairs.
type MetadataOpts map[string]string type MetadataOpts map[string]string
// ToMetadataResetMap assembles a body for a Reset request based on the contents of a MetadataOpts. // ToMetadataResetMap assembles a body for a Reset request based on the contents
// of a MetadataOpts.
func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil return map[string]interface{}{"metadata": opts}, nil
} }
// ToMetadataUpdateMap assembles a body for an Update request based on the contents of a MetadataOpts. // ToMetadataUpdateMap assembles a body for an Update request based on the
// contents of a MetadataOpts.
func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) {
return map[string]interface{}{"metadata": opts}, nil return map[string]interface{}{"metadata": opts}, nil
} }
// ResetMetadata will create multiple new key-value pairs for the given server ID. // ResetMetadata will create multiple new key-value pairs for the given server
// Note: Using this operation will erase any already-existing metadata and create // ID.
// the new metadata provided. To keep any already-existing metadata, use the // Note: Using this operation will erase any already-existing metadata and
// UpdateMetadatas or UpdateMetadata function. // 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) { func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) {
b, err := opts.ToMetadataResetMap() b, err := opts.ToMetadataResetMap()
if err != nil { if err != nil {
@ -583,15 +589,15 @@ func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult
return return
} }
// UpdateMetadataOptsBuilder allows extensions to add additional parameters to the // UpdateMetadataOptsBuilder allows extensions to add additional parameters to
// Create request. // the Create request.
type UpdateMetadataOptsBuilder interface { type UpdateMetadataOptsBuilder interface {
ToMetadataUpdateMap() (map[string]interface{}, error) ToMetadataUpdateMap() (map[string]interface{}, error)
} }
// UpdateMetadata updates (or creates) all the metadata specified by opts for the given server ID. // UpdateMetadata updates (or creates) all the metadata specified by opts for
// This operation does not affect already-existing metadata that is not specified // the given server ID. This operation does not affect already-existing metadata
// by opts. // that is not specified by opts.
func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) {
b, err := opts.ToMetadataUpdateMap() b, err := opts.ToMetadataUpdateMap()
if err != nil { if err != nil {
@ -613,7 +619,8 @@ type MetadatumOptsBuilder interface {
// MetadatumOpts is a map of length one that contains a key-value pair. // MetadatumOpts is a map of length one that contains a key-value pair.
type MetadatumOpts map[string]string type MetadatumOpts map[string]string
// ToMetadatumCreateMap assembles a body for a Create request based on the contents of a MetadataumOpts. // ToMetadatumCreateMap assembles a body for a Create request based on the
// contents of a MetadataumOpts.
func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) {
if len(opts) != 1 { if len(opts) != 1 {
err := gophercloud.ErrInvalidInput{} err := gophercloud.ErrInvalidInput{}
@ -629,7 +636,8 @@ func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string
return metadatum, key, nil return metadatum, key, nil
} }
// CreateMetadatum will create or update the key-value pair with the given key for the given server ID. // 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) { func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) {
b, key, err := opts.ToMetadatumCreateMap() b, key, err := opts.ToMetadatumCreateMap()
if err != nil { if err != nil {
@ -642,53 +650,60 @@ func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts Metadatu
return return
} }
// Metadatum requests the key-value pair with the given key for the given server ID. // 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) { func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) {
_, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) _, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil)
return return
} }
// DeleteMetadatum will delete the key-value pair with the given key for the given server ID. // 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) { func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) {
_, r.Err = client.Delete(metadatumURL(client, id, key), nil) _, r.Err = client.Delete(metadatumURL(client, id, key), nil)
return return
} }
// ListAddresses makes a request against the API to list the servers IP addresses. // ListAddresses makes a request against the API to list the servers IP
// addresses.
func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager {
return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page {
return AddressPage{pagination.SinglePageBase(r)} return AddressPage{pagination.SinglePageBase(r)}
}) })
} }
// ListAddressesByNetwork makes a request against the API to list the servers IP addresses // ListAddressesByNetwork makes a request against the API to list the servers IP
// for the given network. // addresses for the given network.
func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { 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 pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page {
return NetworkAddressPage{pagination.SinglePageBase(r)} return NetworkAddressPage{pagination.SinglePageBase(r)}
}) })
} }
// CreateImageOptsBuilder is the interface types must satisfy in order to be // CreateImageOptsBuilder allows extensions to add additional parameters to the
// used as CreateImage options // CreateImage request.
type CreateImageOptsBuilder interface { type CreateImageOptsBuilder interface {
ToServerCreateImageMap() (map[string]interface{}, error) ToServerCreateImageMap() (map[string]interface{}, error)
} }
// CreateImageOpts satisfies the CreateImageOptsBuilder // CreateImageOpts provides options to pass to the CreateImage request.
type CreateImageOpts struct { type CreateImageOpts struct {
// Name of the image/snapshot // Name of the image/snapshot.
Name string `json:"name" required:"true"` Name string `json:"name" required:"true"`
// Metadata contains key-value pairs (up to 255 bytes each) to attach to the created image.
// Metadata contains key-value pairs (up to 255 bytes each) to attach to
// the created image.
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
} }
// ToServerCreateImageMap formats a CreateImageOpts structure into a request body. // ToServerCreateImageMap formats a CreateImageOpts structure into a request
// body.
func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "createImage") return gophercloud.BuildRequestBody(opts, "createImage")
} }
// CreateImage makes a request against the nova API to schedule an image to be created of the server // 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) { func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) {
b, err := opts.ToServerCreateImageMap() b, err := opts.ToServerCreateImageMap()
if err != nil { if err != nil {
@ -703,11 +718,17 @@ func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageO
return return
} }
// IDFromName is a convienience function that returns a server's ID given its name. // IDFromName is a convienience function that returns a server's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0 count := 0
id := "" id := ""
allPages, err := List(client, nil).AllPages()
listOpts := ListOpts{
Name: name,
}
allPages, err := List(client, listOpts).AllPages()
if err != nil { if err != nil {
return "", err return "", err
} }
@ -734,8 +755,40 @@ func IDFromName(client *gophercloud.ServiceClient, name string) (string, error)
} }
} }
// GetPassword makes a request against the nova API to get the encrypted administrative password. // GetPassword makes a request against the nova API to get the encrypted
// administrative password.
func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil) _, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
return return
} }
// ShowConsoleOutputOptsBuilder is the interface types must satisfy in order to be
// used as ShowConsoleOutput options
type ShowConsoleOutputOptsBuilder interface {
ToServerShowConsoleOutputMap() (map[string]interface{}, error)
}
// ShowConsoleOutputOpts satisfies the ShowConsoleOutputOptsBuilder
type ShowConsoleOutputOpts struct {
// The number of lines to fetch from the end of console log.
// All lines will be returned if this is not specified.
Length int `json:"length,omitempty"`
}
// ToServerShowConsoleOutputMap formats a ShowConsoleOutputOpts structure into a request body.
func (opts ShowConsoleOutputOpts) ToServerShowConsoleOutputMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-getConsoleOutput")
}
// ShowConsoleOutput makes a request against the nova API to get console log from the server
func ShowConsoleOutput(client *gophercloud.ServiceClient, id string, opts ShowConsoleOutputOptsBuilder) (r ShowConsoleOutputResult) {
b, err := opts.ToServerShowConsoleOutputMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}

View file

@ -32,54 +32,73 @@ func ExtractServersInto(r pagination.Page, v interface{}) error {
return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers")
} }
// CreateResult temporarily contains the response from a Create call. // CreateResult is the response from a Create operation. Call its Extract
// method to interpret it as a Server.
type CreateResult struct { type CreateResult struct {
serverResult serverResult
} }
// GetResult temporarily contains the response from a Get call. // GetResult is the response from a Get operation. Call its Extract
// method to interpret it as a Server.
type GetResult struct { type GetResult struct {
serverResult serverResult
} }
// UpdateResult temporarily contains the response from an Update call. // UpdateResult is the response from an Update operation. Call its Extract
// method to interpret it as a Server.
type UpdateResult struct { type UpdateResult struct {
serverResult serverResult
} }
// DeleteResult temporarily contains the response from a Delete call. // DeleteResult is the response from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct { type DeleteResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
// RebuildResult temporarily contains the response from a Rebuild call. // RebuildResult is the response from a Rebuild operation. Call its Extract
// method to interpret it as a Server.
type RebuildResult struct { type RebuildResult struct {
serverResult serverResult
} }
// ActionResult represents the result of server action operations, like reboot // 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 { type ActionResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
// RescueResult represents the result of a server rescue operation // CreateImageResult is the response from a CreateImage operation. Call its
type RescueResult struct { // ExtractImageID method to retrieve the ID of the newly created image.
ActionResult
}
// CreateImageResult represents the result of an image creation operation
type CreateImageResult struct { type CreateImageResult struct {
gophercloud.Result gophercloud.Result
} }
// ShowConsoleOutputResult represents the result of console output from a server
type ShowConsoleOutputResult struct {
gophercloud.Result
}
// Extract will return the console output from a ShowConsoleOutput request.
func (r ShowConsoleOutputResult) Extract() (string, error) {
var s struct {
Output string `json:"output"`
}
err := r.ExtractInto(&s)
return s.Output, err
}
// GetPasswordResult represent the result of a get os-server-password operation. // GetPasswordResult represent the result of a get os-server-password operation.
// Call its ExtractPassword method to retrieve the password.
type GetPasswordResult struct { type GetPasswordResult struct {
gophercloud.Result gophercloud.Result
} }
// ExtractPassword gets the encrypted password. // ExtractPassword gets the encrypted password.
// If privateKey != nil the password is decrypted with the private key. // If privateKey != nil the password is decrypted with the private key.
// If privateKey == nil the encrypted password is returned and can be decrypted with: // If privateKey == nil the encrypted password is returned and can be decrypted
// with:
// echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key> // echo '<pwd>' | base64 -D | openssl rsautl -decrypt -inkey <private_key>
func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) {
var s struct { var s struct {
@ -107,7 +126,7 @@ func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (stri
return string(password), nil return string(password), nil
} }
// ExtractImageID gets the ID of the newly created server image from the header // ExtractImageID gets the ID of the newly created server image from the header.
func (r CreateImageResult) ExtractImageID() (string, error) { func (r CreateImageResult) ExtractImageID() (string, error) {
if r.Err != nil { if r.Err != nil {
return "", r.Err return "", r.Err
@ -124,54 +143,84 @@ func (r CreateImageResult) ExtractImageID() (string, error) {
return imageID, nil return imageID, nil
} }
// Extract interprets any RescueResult as an AdminPass, if possible. // Server represents a server/instance in the OpenStack cloud.
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 { type Server struct {
// ID uniquely identifies this server amongst all other servers, including those not accessible to the current tenant. // ID uniquely identifies this server amongst all other servers,
// including those not accessible to the current tenant.
ID string `json:"id"` ID string `json:"id"`
// TenantID identifies the tenant owning this server resource. // TenantID identifies the tenant owning this server resource.
TenantID string `json:"tenant_id"` TenantID string `json:"tenant_id"`
// UserID uniquely identifies the user account owning the tenant. // UserID uniquely identifies the user account owning the tenant.
UserID string `json:"user_id"` UserID string `json:"user_id"`
// Name contains the human-readable name for the server. // Name contains the human-readable name for the server.
Name string `json:"name"` 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 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"` Updated time.Time `json:"updated"`
Created time.Time `json:"created"` Created time.Time `json:"created"`
HostID string `json:"hostid"`
// Status contains the current operational status of the server, such as IN_PROGRESS or ACTIVE. // 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"` Status string `json:"status"`
// Progress ranges from 0..100. // Progress ranges from 0..100.
// A request made against the server completes only once Progress reaches 100. // A request made against the server completes only once Progress reaches 100.
Progress int `json:"progress"` Progress int `json:"progress"`
// AccessIPv4 and AccessIPv6 contain the IP addresses of the server, suitable for remote access for administration.
// AccessIPv4 and AccessIPv6 contain the IP addresses of the server,
// suitable for remote access for administration.
AccessIPv4 string `json:"accessIPv4"` AccessIPv4 string `json:"accessIPv4"`
AccessIPv6 string `json:"accessIPv6"` AccessIPv6 string `json:"accessIPv6"`
// Image refers to a JSON object, which itself indicates the OS image used to deploy the server.
// Image refers to a JSON object, which itself indicates the OS image used to
// deploy the server.
Image map[string]interface{} `json:"-"` Image map[string]interface{} `json:"-"`
// Flavor refers to a JSON object, which itself indicates the hardware configuration of the deployed server.
// Flavor refers to a JSON object, which itself indicates the hardware
// configuration of the deployed server.
Flavor map[string]interface{} `json:"flavor"` Flavor map[string]interface{} `json:"flavor"`
// Addresses includes a list of all IP addresses assigned to the server, keyed by pool.
// Addresses includes a list of all IP addresses assigned to the server,
// keyed by pool.
Addresses map[string]interface{} `json:"addresses"` Addresses map[string]interface{} `json:"addresses"`
// Metadata includes a list of all user-specified key-value pairs attached to the server.
// Metadata includes a list of all user-specified key-value pairs attached
// to the server.
Metadata map[string]string `json:"metadata"` 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 includes HTTP references to the itself, useful for passing along to
// other APIs that might want a server reference.
Links []interface{} `json:"links"` Links []interface{} `json:"links"`
// KeyName indicates which public key was injected into the server on launch. // KeyName indicates which public key was injected into the server on launch.
KeyName string `json:"key_name"` 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.
// 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. // Note that this is the ONLY time this field will be valid.
AdminPass string `json:"adminPass"` AdminPass string `json:"adminPass"`
// SecurityGroups includes the security groups that this instance has applied to it
// SecurityGroups includes the security groups that this instance has applied
// to it.
SecurityGroups []map[string]interface{} `json:"security_groups"` 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 { func (r *Server) UnmarshalJSON(b []byte) error {
@ -200,9 +249,10 @@ func (r *Server) UnmarshalJSON(b []byte) error {
return err return err
} }
// ServerPage abstracts the raw results of making a List() request against the API. // ServerPage abstracts the raw results of making a List() request against
// As OpenStack extensions may freely alter the response bodies of structures returned to the client, you may only safely access the // the API. As OpenStack extensions may freely alter the response bodies of
// data provided through the ExtractServers call. // structures returned to the client, you may only safely access the data
// provided through the ExtractServers call.
type ServerPage struct { type ServerPage struct {
pagination.LinkedPageBase pagination.LinkedPageBase
} }
@ -213,7 +263,8 @@ func (r ServerPage) IsEmpty() (bool, error) {
return len(s) == 0, err return len(s) == 0, err
} }
// NextPageURL uses the response's embedded link reference to navigate to the next page of results. // NextPageURL uses the response's embedded link reference to navigate to the
// next page of results.
func (r ServerPage) NextPageURL() (string, error) { func (r ServerPage) NextPageURL() (string, error) {
var s struct { var s struct {
Links []gophercloud.Link `json:"servers_links"` Links []gophercloud.Link `json:"servers_links"`
@ -225,49 +276,59 @@ func (r ServerPage) NextPageURL() (string, error) {
return gophercloud.ExtractNextURL(s.Links) return gophercloud.ExtractNextURL(s.Links)
} }
// ExtractServers interprets the results of a single page from a List() call, producing a slice of Server entities. // 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) { func ExtractServers(r pagination.Page) ([]Server, error) {
var s []Server var s []Server
err := ExtractServersInto(r, &s) err := ExtractServersInto(r, &s)
return s, err return s, err
} }
// MetadataResult contains the result of a call for (potentially) multiple key-value pairs. // 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 { type MetadataResult struct {
gophercloud.Result gophercloud.Result
} }
// GetMetadataResult temporarily contains the response from a metadata Get call. // GetMetadataResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type GetMetadataResult struct { type GetMetadataResult struct {
MetadataResult MetadataResult
} }
// ResetMetadataResult temporarily contains the response from a metadata Reset call. // ResetMetadataResult contains the result of a Reset operation. Call its
// Extract method to interpret it as a map[string]interface.
type ResetMetadataResult struct { type ResetMetadataResult struct {
MetadataResult MetadataResult
} }
// UpdateMetadataResult temporarily contains the response from a metadata Update call. // UpdateMetadataResult contains the result of an Update operation. Call its
// Extract method to interpret it as a map[string]interface.
type UpdateMetadataResult struct { type UpdateMetadataResult struct {
MetadataResult MetadataResult
} }
// MetadatumResult contains the result of a call for individual a single key-value pair. // MetadatumResult contains the result of a call for individual a single
// key-value pair.
type MetadatumResult struct { type MetadatumResult struct {
gophercloud.Result gophercloud.Result
} }
// GetMetadatumResult temporarily contains the response from a metadatum Get call. // GetMetadatumResult contains the result of a Get operation. Call its Extract
// method to interpret it as a map[string]interface.
type GetMetadatumResult struct { type GetMetadatumResult struct {
MetadatumResult MetadatumResult
} }
// CreateMetadatumResult temporarily contains the response from a metadatum Create call. // CreateMetadatumResult contains the result of a Create operation. Call its
// Extract method to interpret it as a map[string]interface.
type CreateMetadatumResult struct { type CreateMetadatumResult struct {
MetadatumResult MetadatumResult
} }
// DeleteMetadatumResult temporarily contains the response from a metadatum Delete call. // DeleteMetadatumResult contains the result of a Delete operation. Call its
// ExtractErr method to determine if the call succeeded or failed.
type DeleteMetadatumResult struct { type DeleteMetadatumResult struct {
gophercloud.ErrResult gophercloud.ErrResult
} }
@ -296,9 +357,10 @@ type Address struct {
Address string `json:"addr"` Address string `json:"addr"`
} }
// AddressPage abstracts the raw results of making a ListAddresses() request against the API. // AddressPage abstracts the raw results of making a ListAddresses() request
// As OpenStack extensions may freely alter the response bodies of structures returned // against the API. As OpenStack extensions may freely alter the response bodies
// to the client, you may only safely access the data provided through the ExtractAddresses call. // of structures returned to the client, you may only safely access the data
// provided through the ExtractAddresses call.
type AddressPage struct { type AddressPage struct {
pagination.SinglePageBase pagination.SinglePageBase
} }
@ -309,8 +371,8 @@ func (r AddressPage) IsEmpty() (bool, error) {
return len(addresses) == 0, err return len(addresses) == 0, err
} }
// ExtractAddresses interprets the results of a single page from a ListAddresses() call, // ExtractAddresses interprets the results of a single page from a
// producing a map of addresses. // ListAddresses() call, producing a map of addresses.
func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { func ExtractAddresses(r pagination.Page) (map[string][]Address, error) {
var s struct { var s struct {
Addresses map[string][]Address `json:"addresses"` Addresses map[string][]Address `json:"addresses"`
@ -319,9 +381,11 @@ func ExtractAddresses(r pagination.Page) (map[string][]Address, error) {
return s.Addresses, err return s.Addresses, err
} }
// NetworkAddressPage abstracts the raw results of making a ListAddressesByNetwork() request against the API. // NetworkAddressPage abstracts the raw results of making a
// As OpenStack extensions may freely alter the response bodies of structures returned // ListAddressesByNetwork() request against the API.
// to the client, you may only safely access the data provided through the ExtractAddresses call. // 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 { type NetworkAddressPage struct {
pagination.SinglePageBase pagination.SinglePageBase
} }
@ -332,8 +396,8 @@ func (r NetworkAddressPage) IsEmpty() (bool, error) {
return len(addresses) == 0, err return len(addresses) == 0, err
} }
// ExtractNetworkAddresses interprets the results of a single page from a ListAddressesByNetwork() call, // ExtractNetworkAddresses interprets the results of a single page from a
// producing a slice of addresses. // ListAddressesByNetwork() call, producing a slice of addresses.
func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) {
var s map[string][]Address var s map[string][]Address
err := (r.(NetworkAddressPage)).ExtractInto(&s) err := (r.(NetworkAddressPage)).ExtractInto(&s)

View file

@ -2,8 +2,9 @@ package servers
import "github.com/gophercloud/gophercloud" import "github.com/gophercloud/gophercloud"
// WaitForStatus will continually poll a server until it successfully transitions to a specified // WaitForStatus will continually poll a server until it successfully
// status. It will do this for at most the number of seconds specified. // 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 { func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error {
return gophercloud.WaitFor(secs, func() (bool, error) { return gophercloud.WaitFor(secs, func() (bool, error) {
current, err := Get(c, id).Extract() current, err := Get(c, id).Extract()

View file

@ -0,0 +1,14 @@
/*
Package openstack contains resources for the individual OpenStack projects
supported in Gophercloud. It also includes functions to authenticate to an
OpenStack cloud and for provisioning various service-level clients.
Example of Creating a Service Client
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
client, err := openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
*/
package openstack

View file

@ -6,12 +6,16 @@ import (
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
) )
// V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired /*
// during the v2 identity service. The specified EndpointOpts are used to identify a unique, V2EndpointURL discovers the endpoint URL for a specific service from a
// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided ServiceCatalog acquired during the v2 identity service.
// criteria and when none do. The minimum that can be specified is a Type, but you will also often
// need to specify a Name and/or a Region depending on what's available on your OpenStack The specified EndpointOpts are used to identify a unique, unambiguous endpoint
// deployment. to return. It's an error both when multiple endpoints match the provided
criteria and when none do. The minimum that can be specified is a Type, but you
will also often need to specify a Name and/or a Region depending on what's
available on your OpenStack deployment.
*/
func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
// Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
var endpoints = make([]tokens2.Endpoint, 0, 1) var endpoints = make([]tokens2.Endpoint, 0, 1)
@ -54,12 +58,16 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt
return "", err return "", err
} }
// V3EndpointURL discovers the endpoint URL for a specific service from a Catalog acquired /*
// during the v3 identity service. The specified EndpointOpts are used to identify a unique, V3EndpointURL discovers the endpoint URL for a specific service from a Catalog
// unambiguous endpoint to return. It's an error both when multiple endpoints match the provided acquired during the v3 identity service.
// criteria and when none do. The minimum that can be specified is a Type, but you will also often
// need to specify a Name and/or a Region depending on what's available on your OpenStack The specified EndpointOpts are used to identify a unique, unambiguous endpoint
// deployment. to return. It's an error both when multiple endpoints match the provided
criteria and when none do. The minimum that can be specified is a Type, but you
will also often need to specify a Name and/or a Region depending on what's
available on your OpenStack deployment.
*/
func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
// Extract Endpoints from the catalog entries that match the requested Type, Interface, // Extract Endpoints from the catalog entries that match the requested Type, Interface,
// Name if provided, and Region if provided. // Name if provided, and Region if provided.
@ -76,7 +84,7 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
return "", err return "", err
} }
if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && if (opts.Availability == gophercloud.Availability(endpoint.Interface)) &&
(opts.Region == "" || endpoint.Region == opts.Region) { (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) {
endpoints = append(endpoints, endpoint) endpoints = append(endpoints, endpoint)
} }
} }

View file

@ -1,7 +1,65 @@
// Package tenants provides information and interaction with the /*
// tenants API resource for the OpenStack Identity service. Package tenants provides information and interaction with the
// tenants API resource for the OpenStack Identity service.
// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
// for more information. and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
for more information.
Example to List Tenants
listOpts := tenants.ListOpts{
Limit: 2,
}
allPages, err := tenants.List(identityClient, listOpts).AllPages()
if err != nil {
panic(err)
}
allTenants, err := tenants.ExtractTenants(allPages)
if err != nil {
panic(err)
}
for _, tenant := range allTenants {
fmt.Printf("%+v\n", tenant)
}
Example to Create a Tenant
createOpts := tenants.CreateOpts{
Name: "tenant_name",
Description: "this is a tenant",
Enabled: gophercloud.Enabled,
}
tenant, err := tenants.Create(identityClient, createOpts).Extract()
if err != nil {
panic(err)
}
Example to Update a Tenant
tenantID := "e6db6ed6277c461a853458589063b295"
updateOpts := tenants.UpdateOpts{
Description: "this is a new description",
Enabled: gophercloud.Disabled,
}
tenant, err := tenants.Update(identityClient, tenantID, updateOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete a Tenant
tenantID := "e6db6ed6277c461a853458589063b295"
err := tenants.Delete(identitYClient, tenantID).ExtractErr()
if err != nil {
panic(err)
}
*/
package tenants package tenants

View file

@ -9,6 +9,7 @@ import (
type ListOpts struct { type ListOpts struct {
// Marker is the ID of the last Tenant on the previous page. // Marker is the ID of the last Tenant on the previous page.
Marker string `q:"marker"` Marker string `q:"marker"`
// Limit specifies the page size. // Limit specifies the page size.
Limit int `q:"limit"` Limit int `q:"limit"`
} }
@ -27,3 +28,89 @@ func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
return TenantPage{pagination.LinkedPageBase{PageResult: r}} return TenantPage{pagination.LinkedPageBase{PageResult: r}}
}) })
} }
// CreateOpts represents the options needed when creating new tenant.
type CreateOpts struct {
// Name is the name of the tenant.
Name string `json:"name" required:"true"`
// Description is the description of the tenant.
Description string `json:"description,omitempty"`
// Enabled sets the tenant status to enabled or disabled.
Enabled *bool `json:"enabled,omitempty"`
}
// CreateOptsBuilder enables extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToTenantCreateMap() (map[string]interface{}, error)
}
// ToTenantCreateMap assembles a request body based on the contents of
// a CreateOpts.
func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "tenant")
}
// Create is the operation responsible for creating new tenant.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToTenantCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return
}
// Get requests details on a single tenant by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
return
}
// UpdateOptsBuilder allows extensions to add additional parameters to the
// Update request.
type UpdateOptsBuilder interface {
ToTenantUpdateMap() (map[string]interface{}, error)
}
// UpdateOpts specifies the base attributes that may be updated on an existing
// tenant.
type UpdateOpts struct {
// Name is the name of the tenant.
Name string `json:"name,omitempty"`
// Description is the description of the tenant.
Description string `json:"description,omitempty"`
// Enabled sets the tenant status to enabled or disabled.
Enabled *bool `json:"enabled,omitempty"`
}
// ToTenantUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToTenantUpdateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "tenant")
}
// Update is the operation responsible for updating exist tenants by their TenantID.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
b, err := opts.ToTenantUpdateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Delete is the operation responsible for permanently deleting a tenant.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
return
}

View file

@ -43,7 +43,8 @@ func (r TenantPage) NextPageURL() (string, error) {
return gophercloud.ExtractNextURL(s.Links) return gophercloud.ExtractNextURL(s.Links)
} }
// ExtractTenants returns a slice of Tenants contained in a single page of results. // ExtractTenants returns a slice of Tenants contained in a single page of
// results.
func ExtractTenants(r pagination.Page) ([]Tenant, error) { func ExtractTenants(r pagination.Page) ([]Tenant, error) {
var s struct { var s struct {
Tenants []Tenant `json:"tenants"` Tenants []Tenant `json:"tenants"`
@ -51,3 +52,40 @@ func ExtractTenants(r pagination.Page) ([]Tenant, error) {
err := (r.(TenantPage)).ExtractInto(&s) err := (r.(TenantPage)).ExtractInto(&s)
return s.Tenants, err return s.Tenants, err
} }
type tenantResult struct {
gophercloud.Result
}
// Extract interprets any tenantResults as a Tenant.
func (r tenantResult) Extract() (*Tenant, error) {
var s struct {
Tenant *Tenant `json:"tenant"`
}
err := r.ExtractInto(&s)
return s.Tenant, err
}
// GetResult is the response from a Get request. Call its Extract method to
// interpret it as a Tenant.
type GetResult struct {
tenantResult
}
// CreateResult is the response from a Create request. Call its Extract method
// to interpret it as a Tenant.
type CreateResult struct {
tenantResult
}
// DeleteResult is the response from a Get request. Call its ExtractErr method
// to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// UpdateResult is the response from a Update request. Call its Extract method
// to interpret it as a Tenant.
type UpdateResult struct {
tenantResult
}

View file

@ -5,3 +5,19 @@ import "github.com/gophercloud/gophercloud"
func listURL(client *gophercloud.ServiceClient) string { func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tenants") return client.ServiceURL("tenants")
} }
func getURL(client *gophercloud.ServiceClient, tenantID string) string {
return client.ServiceURL("tenants", tenantID)
}
func createURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tenants")
}
func deleteURL(client *gophercloud.ServiceClient, tenantID string) string {
return client.ServiceURL("tenants", tenantID)
}
func updateURL(client *gophercloud.ServiceClient, tenantID string) string {
return client.ServiceURL("tenants", tenantID)
}

View file

@ -1,5 +1,46 @@
// Package tokens provides information and interaction with the token API /*
// resource for the OpenStack Identity service. Package tokens provides information and interaction with the token API
// For more information, see: resource for the OpenStack Identity service.
// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
For more information, see:
http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
Example to Create an Unscoped Token from a Password
authOpts := gophercloud.AuthOptions{
Username: "user",
Password: "pass"
}
token, err := tokens.Create(identityClient, authOpts).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token from a Tenant ID and Password
authOpts := gophercloud.AuthOptions{
Username: "user",
Password: "password",
TenantID: "fc394f2ab2df4114bde39905f800dc57"
}
token, err := tokens.Create(identityClient, authOpts).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token from a Tenant Name and Password
authOpts := gophercloud.AuthOptions{
Username: "user",
Password: "password",
TenantName: "tenantname"
}
token, err := tokens.Create(identityClient, authOpts).ExtractToken()
if err != nil {
panic(err)
}
*/
package tokens package tokens

View file

@ -2,17 +2,21 @@ package tokens
import "github.com/gophercloud/gophercloud" import "github.com/gophercloud/gophercloud"
// PasswordCredentialsV2 represents the required options to authenticate
// with a username and password.
type PasswordCredentialsV2 struct { type PasswordCredentialsV2 struct {
Username string `json:"username" required:"true"` Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"` Password string `json:"password" required:"true"`
} }
// TokenCredentialsV2 represents the required options to authenticate
// with a token.
type TokenCredentialsV2 struct { type TokenCredentialsV2 struct {
ID string `json:"id,omitempty" required:"true"` ID string `json:"id,omitempty" required:"true"`
} }
// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder // AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the
// interface. // AuthOptionsBuilder interface.
type AuthOptionsV2 struct { type AuthOptionsV2 struct {
PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"`
@ -23,15 +27,16 @@ type AuthOptionsV2 struct {
TenantID string `json:"tenantId,omitempty"` TenantID string `json:"tenantId,omitempty"`
TenantName string `json:"tenantName,omitempty"` TenantName string `json:"tenantName,omitempty"`
// TokenCredentials allows users to authenticate (possibly as another user) with an // TokenCredentials allows users to authenticate (possibly as another user)
// authentication token ID. // with an authentication token ID.
TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"`
} }
// AuthOptionsBuilder describes any argument that may be passed to the Create call. // AuthOptionsBuilder allows extensions to add additional parameters to the
// token create request.
type AuthOptionsBuilder interface { type AuthOptionsBuilder interface {
// ToTokenCreateMap assembles the Create request body, returning an error if parameters are // ToTokenCreateMap assembles the Create request body, returning an error
// missing or inconsistent. // if parameters are missing or inconsistent.
ToTokenV2CreateMap() (map[string]interface{}, error) ToTokenV2CreateMap() (map[string]interface{}, error)
} }
@ -47,8 +52,7 @@ type AuthOptions struct {
TokenID string TokenID string
} }
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder // ToTokenV2CreateMap builds a token request body from the given AuthOptions.
// interface in the v2 tokens package
func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
v2Opts := AuthOptionsV2{ v2Opts := AuthOptionsV2{
TenantID: opts.TenantID, TenantID: opts.TenantID,
@ -74,9 +78,9 @@ func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
} }
// Create authenticates to the identity service and attempts to acquire a Token. // Create authenticates to the identity service and attempts to acquire a Token.
// If successful, the CreateResult // Generally, rather than interact with this call directly, end users should
// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(), // call openstack.AuthenticatedClient(), which abstracts all of the gory details
// which abstracts all of the gory details about navigating service catalogs and such. // about navigating service catalogs and such.
func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) {
b, err := auth.ToTokenV2CreateMap() b, err := auth.ToTokenV2CreateMap()
if err != nil { if err != nil {

View file

@ -7,20 +7,24 @@ import (
"github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants"
) )
// Token provides only the most basic information related to an authentication token. // Token provides only the most basic information related to an authentication
// token.
type Token struct { type Token struct {
// ID provides the primary means of identifying a user to the OpenStack API. // ID provides the primary means of identifying a user to the OpenStack API.
// OpenStack defines this field as an opaque value, so do not depend on its content. // OpenStack defines this field as an opaque value, so do not depend on its
// It is safe, however, to compare for equality. // content. It is safe, however, to compare for equality.
ID string ID string
// ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid. // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the
// After this point in time, future API requests made using this authentication token will respond with errors. // authentication token becomes invalid. After this point in time, future
// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication. // API requests made using this authentication token will respond with
// errors. Either the caller will need to reauthenticate manually, or more
// preferably, the caller should exploit automatic re-authentication.
// See the AuthOptions structure for more details. // See the AuthOptions structure for more details.
ExpiresAt time.Time ExpiresAt time.Time
// Tenant provides information about the tenant to which this token grants access. // Tenant provides information about the tenant to which this token grants
// access.
Tenant tenants.Tenant Tenant tenants.Tenant
} }
@ -38,13 +42,17 @@ type User struct {
} }
// Endpoint represents a single API endpoint offered by a service. // Endpoint represents a single API endpoint offered by a service.
// It provides the public and internal URLs, if supported, along with a region specifier, again if provided. // It provides the public and internal URLs, if supported, along with a region
// specifier, again if provided.
//
// The significance of the Region field will depend upon your provider. // The significance of the Region field will depend upon your provider.
// //
// In addition, the interface offered by the service will have version information associated with it // In addition, the interface offered by the service will have version
// through the VersionId, VersionInfo, and VersionList fields, if provided or supported. // information associated with it through the VersionId, VersionInfo, and
// VersionList fields, if provided or supported.
// //
// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value (""). // In all cases, fields which aren't supported by the provider and service
// combined will assume a zero-value ("").
type Endpoint struct { type Endpoint struct {
TenantID string `json:"tenantId"` TenantID string `json:"tenantId"`
PublicURL string `json:"publicURL"` PublicURL string `json:"publicURL"`
@ -56,38 +64,44 @@ type Endpoint struct {
VersionList string `json:"versionList"` VersionList string `json:"versionList"`
} }
// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing. // CatalogEntry provides a type-safe interface to an Identity API V2 service
// Each class of service, such as cloud DNS or block storage services, will have a single // catalog listing.
// CatalogEntry representing it.
// //
// Note: when looking for the desired service, try, whenever possible, to key off the type field. // Each class of service, such as cloud DNS or block storage services, will have
// Otherwise, you'll tie the representation of the service to a specific provider. // a single CatalogEntry representing it.
//
// Note: when looking for the desired service, try, whenever possible, to key
// off the type field. Otherwise, you'll tie the representation of the service
// to a specific provider.
type CatalogEntry struct { type CatalogEntry struct {
// Name will contain the provider-specified name for the service. // Name will contain the provider-specified name for the service.
Name string `json:"name"` Name string `json:"name"`
// Type will contain a type string if OpenStack defines a type for the service. // Type will contain a type string if OpenStack defines a type for the
// Otherwise, for provider-specific services, the provider may assign their own type strings. // service. Otherwise, for provider-specific services, the provider may assign
// their own type strings.
Type string `json:"type"` Type string `json:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for // Endpoints will let the caller iterate over all the different endpoints that
// the service. // may exist for the service.
Endpoints []Endpoint `json:"endpoints"` Endpoints []Endpoint `json:"endpoints"`
} }
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. // ServiceCatalog provides a view into the service catalog from a previous,
// successful authentication.
type ServiceCatalog struct { type ServiceCatalog struct {
Entries []CatalogEntry Entries []CatalogEntry
} }
// CreateResult defers the interpretation of a created token. // CreateResult is the response from a Create request. Use ExtractToken() to
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. // interpret it as a Token, or ExtractServiceCatalog() to interpret it as a
// service catalog.
type CreateResult struct { type CreateResult struct {
gophercloud.Result gophercloud.Result
} }
// GetResult is the deferred response from a Get call, which is the same with a Created token. // GetResult is the deferred response from a Get call, which is the same with a
// Use ExtractUser() to interpret it as a User. // Created token. Use ExtractUser() to interpret it as a User.
type GetResult struct { type GetResult struct {
CreateResult CreateResult
} }
@ -121,7 +135,8 @@ func (r CreateResult) ExtractToken() (*Token, error) {
}, nil }, nil
} }
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. // ExtractServiceCatalog returns the ServiceCatalog that was generated along
// with the user's Token.
func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
var s struct { var s struct {
Access struct { Access struct {

View file

@ -1,6 +1,108 @@
// Package tokens provides information and interaction with the token API /*
// resource for the OpenStack Identity service. Package tokens provides information and interaction with the token API
// resource for the OpenStack Identity service.
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 For more information, see:
http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
Example to Create a Token From a Username and Password
authOptions := tokens.AuthOptions{
UserID: "username",
Password: "password",
}
token, err := tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token From a Username, Password, and Domain
authOptions := tokens.AuthOptions{
UserID: "username",
Password: "password",
DomainID: "default",
}
token, err := tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
authOptions = tokens.AuthOptions{
UserID: "username",
Password: "password",
DomainName: "default",
}
token, err = tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token From a Token
authOptions := tokens.AuthOptions{
TokenID: "token_id",
}
token, err := tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token from a Username and Password with Project ID Scope
scope := tokens.Scope{
ProjectID: "0fe36e73809d46aeae6705c39077b1b3",
}
authOptions := tokens.AuthOptions{
Scope: &scope,
UserID: "username",
Password: "password",
}
token, err = tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token from a Username and Password with Domain ID Scope
scope := tokens.Scope{
DomainID: "default",
}
authOptions := tokens.AuthOptions{
Scope: &scope,
UserID: "username",
Password: "password",
}
token, err = tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to Create a Token from a Username and Password with Project Name Scope
scope := tokens.Scope{
ProjectName: "project_name",
DomainID: "default",
}
authOptions := tokens.AuthOptions{
Scope: &scope,
UserID: "username",
Password: "password",
}
token, err = tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
*/
package tokens package tokens

View file

@ -4,26 +4,28 @@ import "github.com/gophercloud/gophercloud"
// Scope allows a created token to be limited to a specific domain or project. // Scope allows a created token to be limited to a specific domain or project.
type Scope struct { type Scope struct {
ProjectID string `json:"scope.project.id,omitempty" not:"ProjectName,DomainID,DomainName"` ProjectID string
ProjectName string `json:"scope.project.name,omitempty"` ProjectName string
DomainID string `json:"scope.project.id,omitempty" not:"ProjectName,ProjectID,DomainName"` DomainID string
DomainName string `json:"scope.project.id,omitempty"` DomainName string
} }
// AuthOptionsBuilder describes any argument that may be passed to the Create call. // AuthOptionsBuilder provides the ability for extensions to add additional
// parameters to AuthOptions. Extensions must satisfy all required methods.
type AuthOptionsBuilder interface { type AuthOptionsBuilder interface {
// ToTokenV3CreateMap assembles the Create request body, returning an error if parameters are // ToTokenV3CreateMap assembles the Create request body, returning an error
// missing or inconsistent. // if parameters are missing or inconsistent.
ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error)
ToTokenV3ScopeMap() (map[string]interface{}, error) ToTokenV3ScopeMap() (map[string]interface{}, error)
CanReauth() bool CanReauth() bool
} }
// AuthOptions represents options for authenticating a user.
type AuthOptions struct { type AuthOptions struct {
// IdentityEndpoint specifies the HTTP endpoint that is required to work with // IdentityEndpoint specifies the HTTP endpoint that is required to work with
// the Identity API of the appropriate version. While it's ultimately needed by // the Identity API of the appropriate version. While it's ultimately needed
// all of the identity services, it will often be populated by a provider-level // by all of the identity services, it will often be populated by a
// function. // provider-level function.
IdentityEndpoint string `json:"-"` IdentityEndpoint string `json:"-"`
// Username is required if using Identity V2 API. Consult with your provider's // Username is required if using Identity V2 API. Consult with your provider's
@ -36,99 +38,58 @@ type AuthOptions struct {
// At most one of DomainID and DomainName must be provided if using Username // At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional. // with Identity V3. Otherwise, either are optional.
DomainID string `json:"id,omitempty"` DomainID string `json:"-"`
DomainName string `json:"name,omitempty"` DomainName string `json:"name,omitempty"`
// AllowReauth should be set to true if you grant permission for Gophercloud to // AllowReauth should be set to true if you grant permission for Gophercloud
// cache your credentials in memory, and to allow Gophercloud to attempt to // to cache your credentials in memory, and to allow Gophercloud to attempt
// re-authenticate automatically if/when your token expires. If you set it to // to re-authenticate automatically if/when your token expires. If you set
// false, it will not cache these settings, but re-authentication will not be // it to false, it will not cache these settings, but re-authentication will
// possible. This setting defaults to false. // not be possible. This setting defaults to false.
AllowReauth bool `json:"-"` AllowReauth bool `json:"-"`
// TokenID allows users to authenticate (possibly as another user) with an // TokenID allows users to authenticate (possibly as another user) with an
// authentication token ID. // authentication token ID.
TokenID string `json:"-"` TokenID string `json:"-"`
// Authentication through Application Credentials requires supplying name, project and secret
// For project we can use TenantID
ApplicationCredentialID string `json:"-"`
ApplicationCredentialName string `json:"-"`
ApplicationCredentialSecret string `json:"-"`
Scope Scope `json:"-"` Scope Scope `json:"-"`
} }
// ToTokenV3CreateMap builds a request body from AuthOptions.
func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
gophercloudAuthOpts := gophercloud.AuthOptions{ gophercloudAuthOpts := gophercloud.AuthOptions{
Username: opts.Username, Username: opts.Username,
UserID: opts.UserID, UserID: opts.UserID,
Password: opts.Password, Password: opts.Password,
DomainID: opts.DomainID, DomainID: opts.DomainID,
DomainName: opts.DomainName, DomainName: opts.DomainName,
AllowReauth: opts.AllowReauth, AllowReauth: opts.AllowReauth,
TokenID: opts.TokenID, TokenID: opts.TokenID,
ApplicationCredentialID: opts.ApplicationCredentialID,
ApplicationCredentialName: opts.ApplicationCredentialName,
ApplicationCredentialSecret: opts.ApplicationCredentialSecret,
} }
return gophercloudAuthOpts.ToTokenV3CreateMap(scope) return gophercloudAuthOpts.ToTokenV3CreateMap(scope)
} }
// ToTokenV3CreateMap builds a scope request body from AuthOptions.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
if opts.Scope.ProjectName != "" { scope := gophercloud.AuthScope(opts.Scope)
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
}
if opts.Scope.ProjectID != "" {
return nil, gophercloud.ErrScopeProjectIDOrProjectName{}
}
if opts.Scope.DomainID != "" { gophercloudAuthOpts := gophercloud.AuthOptions{
// ProjectName + DomainID Scope: &scope,
return map[string]interface{}{ DomainID: opts.DomainID,
"project": map[string]interface{}{ DomainName: opts.DomainName,
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
},
}, nil
}
if opts.Scope.DomainName != "" {
// ProjectName + DomainName
return map[string]interface{}{
"project": map[string]interface{}{
"name": &opts.Scope.ProjectName,
"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
},
}, nil
}
} else if opts.Scope.ProjectID != "" {
// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
if opts.Scope.DomainID != "" {
return nil, gophercloud.ErrScopeProjectIDAlone{}
}
if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeProjectIDAlone{}
}
// ProjectID
return map[string]interface{}{
"project": map[string]interface{}{
"id": &opts.Scope.ProjectID,
},
}, nil
} else if opts.Scope.DomainID != "" {
// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeDomainIDOrDomainName{}
}
// DomainID
return map[string]interface{}{
"domain": map[string]interface{}{
"id": &opts.Scope.DomainID,
},
}, nil
} else if opts.Scope.DomainName != "" {
return nil, gophercloud.ErrScopeDomainName{}
} }
return nil, nil return gophercloudAuthOpts.ToTokenV3ScopeMap()
} }
func (opts *AuthOptions) CanReauth() bool { func (opts *AuthOptions) CanReauth() bool {
@ -141,7 +102,8 @@ func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[
} }
} }
// Create authenticates and either generates a new token, or changes the Scope of an existing token. // Create authenticates and either generates a new token, or changes the Scope
// of an existing token.
func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) {
scope, err := opts.ToTokenV3ScopeMap() scope, err := opts.ToTokenV3ScopeMap()
if err != nil { if err != nil {
@ -180,9 +142,9 @@ func Get(c *gophercloud.ServiceClient, token string) (r GetResult) {
// Validate determines if a specified token is valid or not. // Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
resp, err := c.Request("HEAD", tokenURL(c), &gophercloud.RequestOpts{ resp, err := c.Head(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token), MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{200, 204, 400, 401, 403, 404}, OkCodes: []int{200, 204, 404},
}) })
if err != nil { if err != nil {
return false, err return false, err

View file

@ -13,35 +13,71 @@ import (
type Endpoint struct { type Endpoint struct {
ID string `json:"id"` ID string `json:"id"`
Region string `json:"region"` Region string `json:"region"`
RegionID string `json:"region_id"`
Interface string `json:"interface"` Interface string `json:"interface"`
URL string `json:"url"` URL string `json:"url"`
} }
// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing. // CatalogEntry provides a type-safe interface to an Identity API V3 service
// Each class of service, such as cloud DNS or block storage services, could have multiple // catalog listing. Each class of service, such as cloud DNS or block storage
// CatalogEntry representing it (one by interface type, e.g public, admin or internal). // services, could have multiple CatalogEntry representing it (one by interface
// type, e.g public, admin or internal).
// //
// Note: when looking for the desired service, try, whenever possible, to key off the type field. // Note: when looking for the desired service, try, whenever possible, to key
// Otherwise, you'll tie the representation of the service to a specific provider. // off the type field. Otherwise, you'll tie the representation of the service
// to a specific provider.
type CatalogEntry struct { type CatalogEntry struct {
// Service ID // Service ID
ID string `json:"id"` ID string `json:"id"`
// Name will contain the provider-specified name for the service. // Name will contain the provider-specified name for the service.
Name string `json:"name"` Name string `json:"name"`
// Type will contain a type string if OpenStack defines a type for the service.
// Otherwise, for provider-specific services, the provider may assign their own type strings. // Type will contain a type string if OpenStack defines a type for the
// service. Otherwise, for provider-specific services, the provider may
// assign their own type strings.
Type string `json:"type"` Type string `json:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service. // Endpoints will let the caller iterate over all the different endpoints that
// may exist for the service.
Endpoints []Endpoint `json:"endpoints"` Endpoints []Endpoint `json:"endpoints"`
} }
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication. // ServiceCatalog provides a view into the service catalog from a previous,
// successful authentication.
type ServiceCatalog struct { type ServiceCatalog struct {
Entries []CatalogEntry `json:"catalog"` Entries []CatalogEntry `json:"catalog"`
} }
// commonResult is the deferred result of a Create or a Get call. // Domain provides information about the domain to which this token grants
// access.
type Domain struct {
ID string `json:"id"`
Name string `json:"name"`
}
// User represents a user resource that exists in the Identity Service.
type User struct {
Domain Domain `json:"domain"`
ID string `json:"id"`
Name string `json:"name"`
}
// Role provides information about roles to which User is authorized.
type Role struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Project provides information about project to which User is authorized.
type Project struct {
Domain Domain `json:"domain"`
ID string `json:"id"`
Name string `json:"name"`
}
// commonResult is the response from a request. A commonResult has various
// methods which can be used to extract different details about the result.
type commonResult struct { type commonResult struct {
gophercloud.Result gophercloud.Result
} }
@ -66,34 +102,66 @@ func (r commonResult) ExtractToken() (*Token, error) {
return &s, err return &s, err
} }
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token. // ExtractServiceCatalog returns the ServiceCatalog that was generated along
func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { // with the user's Token.
func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
var s ServiceCatalog var s ServiceCatalog
err := r.ExtractInto(&s) err := r.ExtractInto(&s)
return &s, err return &s, err
} }
// CreateResult defers the interpretation of a created token. // ExtractUser returns the User that is the owner of the Token.
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog. func (r commonResult) ExtractUser() (*User, error) {
var s struct {
User *User `json:"user"`
}
err := r.ExtractInto(&s)
return s.User, err
}
// ExtractRoles returns Roles to which User is authorized.
func (r commonResult) ExtractRoles() ([]Role, error) {
var s struct {
Roles []Role `json:"roles"`
}
err := r.ExtractInto(&s)
return s.Roles, err
}
// ExtractProject returns Project to which User is authorized.
func (r commonResult) ExtractProject() (*Project, error) {
var s struct {
Project *Project `json:"project"`
}
err := r.ExtractInto(&s)
return s.Project, err
}
// CreateResult is the response from a Create request. Use ExtractToken()
// to interpret it as a Token, or ExtractServiceCatalog() to interpret it
// as a service catalog.
type CreateResult struct { type CreateResult struct {
commonResult commonResult
} }
// GetResult is the deferred response from a Get call. // GetResult is the response from a Get request. Use ExtractToken()
// to interpret it as a Token, or ExtractServiceCatalog() to interpret it
// as a service catalog.
type GetResult struct { type GetResult struct {
commonResult commonResult
} }
// RevokeResult is the deferred response from a Revoke call. // RevokeResult is response from a Revoke request.
type RevokeResult struct { type RevokeResult struct {
commonResult commonResult
} }
// Token is a string that grants a user access to a controlled set of services in an OpenStack provider. // Token is a string that grants a user access to a controlled set of services
// Each Token is valid for a set length of time. // in an OpenStack provider. Each Token is valid for a set length of time.
type Token struct { type Token struct {
// ID is the issued token. // ID is the issued token.
ID string `json:"id"` ID string `json:"id"`
// ExpiresAt is the timestamp at which this token will no longer be accepted. // ExpiresAt is the timestamp at which this token will no longer be accepted.
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
} }

View file

@ -0,0 +1,28 @@
package utils
import (
"net/url"
"regexp"
"strings"
)
// BaseEndpoint will return a URL without the /vX.Y
// portion of the URL.
func BaseEndpoint(endpoint string) (string, error) {
u, err := url.Parse(endpoint)
if err != nil {
return "", err
}
u.RawQuery, u.Fragment = "", ""
path := u.Path
versionRe := regexp.MustCompile("v[0-9.]+/?")
if version := versionRe.FindString(path); version != "" {
versionIndex := strings.Index(path, version)
u.Path = path[:versionIndex]
}
return u.String(), nil
}

View file

@ -68,11 +68,6 @@ func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*
return nil, "", err return nil, "", err
} }
byID := make(map[string]*Version)
for _, version := range recognized {
byID[version.ID] = version
}
var highest *Version var highest *Version
var endpoint string var endpoint string
@ -84,20 +79,22 @@ func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*
} }
} }
if matching, ok := byID[value.ID]; ok { for _, version := range recognized {
// Prefer a version that exactly matches the provided endpoint. if strings.Contains(value.ID, version.ID) {
if href == identityEndpoint { // Prefer a version that exactly matches the provided endpoint.
if href == "" { if href == identityEndpoint {
return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) if href == "" {
return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase)
}
return version, href, nil
} }
return matching, href, nil
}
// Otherwise, find the highest-priority version with a whitelisted status. // Otherwise, find the highest-priority version with a whitelisted status.
if goodStatus[strings.ToLower(value.Status)] { if goodStatus[strings.ToLower(value.Status)] {
if highest == nil || matching.Priority > highest.Priority { if highest == nil || version.Priority > highest.Priority {
highest = matching highest = version
endpoint = href endpoint = href
}
} }
} }
} }

View file

@ -55,6 +55,6 @@ func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) {
return client.Get(url, nil, &gophercloud.RequestOpts{ return client.Get(url, nil, &gophercloud.RequestOpts{
MoreHeaders: headers, MoreHeaders: headers,
OkCodes: []int{200, 204}, OkCodes: []int{200, 204, 300},
}) })
} }

View file

@ -22,7 +22,6 @@ var (
// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type // Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type
// will need to implement. // will need to implement.
type Page interface { type Page interface {
// NextPageURL generates the URL for the page of data that follows this collection. // NextPageURL generates the URL for the page of data that follows this collection.
// Return "" if no such page exists. // Return "" if no such page exists.
NextPageURL() (string, error) NextPageURL() (string, error)
@ -42,6 +41,8 @@ type Pager struct {
createPage func(r PageResult) Page createPage func(r PageResult) Page
firstPage Page
Err error Err error
// Headers supplies additional HTTP headers to populate on each paged request. // Headers supplies additional HTTP headers to populate on each paged request.
@ -90,9 +91,18 @@ func (p Pager) EachPage(handler func(Page) (bool, error)) error {
} }
currentURL := p.initialURL currentURL := p.initialURL
for { for {
currentPage, err := p.fetchNextPage(currentURL) var currentPage Page
if err != nil {
return err // if first page has already been fetched, no need to fetch it again
if p.firstPage != nil {
currentPage = p.firstPage
p.firstPage = nil
} else {
var err error
currentPage, err = p.fetchNextPage(currentURL)
if err != nil {
return err
}
} }
empty, err := currentPage.IsEmpty() empty, err := currentPage.IsEmpty()
@ -129,23 +139,26 @@ func (p Pager) AllPages() (Page, error) {
// body will contain the final concatenated Page body. // body will contain the final concatenated Page body.
var body reflect.Value var body reflect.Value
// Grab a test page to ascertain the page body type. // Grab a first page to ascertain the page body type.
testPage, err := p.fetchNextPage(p.initialURL) firstPage, err := p.fetchNextPage(p.initialURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Store the page type so we can use reflection to create a new mega-page of // Store the page type so we can use reflection to create a new mega-page of
// that type. // that type.
pageType := reflect.TypeOf(testPage) pageType := reflect.TypeOf(firstPage)
// if it's a single page, just return the testPage (first page) // if it's a single page, just return the firstPage (first page)
if _, found := pageType.FieldByName("SinglePageBase"); found { if _, found := pageType.FieldByName("SinglePageBase"); found {
return testPage, nil return firstPage, nil
} }
// store the first page to avoid getting it twice
p.firstPage = firstPage
// Switch on the page body type. Recognized types are `map[string]interface{}`, // Switch on the page body type. Recognized types are `map[string]interface{}`,
// `[]byte`, and `[]interface{}`. // `[]byte`, and `[]interface{}`.
switch pb := testPage.GetBody().(type) { switch pb := firstPage.GetBody().(type) {
case map[string]interface{}: case map[string]interface{}:
// key is the map key for the page body if the body type is `map[string]interface{}`. // key is the map key for the page body if the body type is `map[string]interface{}`.
var key string var key string

View file

@ -10,10 +10,28 @@ import (
"time" "time"
) )
// BuildRequestBody builds a map[string]interface from the given `struct`. If /*
// parent is not the empty string, the final map[string]interface returned will BuildRequestBody builds a map[string]interface from the given `struct`. If
// encapsulate the built one parent is not an empty string, the final map[string]interface returned will
// encapsulate the built one. For example:
disk := 1
createOpts := flavors.CreateOpts{
ID: "1",
Name: "m1.tiny",
Disk: &disk,
RAM: 512,
VCPUs: 1,
RxTxFactor: 1.0,
}
body, err := gophercloud.BuildRequestBody(createOpts, "flavor")
The above example can be run as-is, however it is recommended to look at how
BuildRequestBody is used within Gophercloud to more fully understand how it
fits within the request process as a whole rather than use it directly as shown
above.
*/
func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) { func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) {
optsValue := reflect.ValueOf(opts) optsValue := reflect.ValueOf(opts)
if optsValue.Kind() == reflect.Ptr { if optsValue.Kind() == reflect.Ptr {
@ -97,10 +115,31 @@ func BuildRequestBody(opts interface{}, parent string) (map[string]interface{},
} }
} }
jsonTag := f.Tag.Get("json")
if jsonTag == "-" {
continue
}
if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) {
sliceValue := v
if sliceValue.Kind() == reflect.Ptr {
sliceValue = sliceValue.Elem()
}
for i := 0; i < sliceValue.Len(); i++ {
element := sliceValue.Index(i)
if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) {
_, err := BuildRequestBody(element.Interface(), "")
if err != nil {
return nil, err
}
}
}
}
if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
if zero { if zero {
//fmt.Printf("value before change: %+v\n", optsValue.Field(i)) //fmt.Printf("value before change: %+v\n", optsValue.Field(i))
if jsonTag := f.Tag.Get("json"); jsonTag != "" { if jsonTag != "" {
jsonTagPieces := strings.Split(jsonTag, ",") jsonTagPieces := strings.Split(jsonTag, ",")
if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" {
if v.CanSet() { if v.CanSet() {
@ -329,12 +368,20 @@ func BuildQueryString(opts interface{}) (*url.URL, error) {
params.Add(tags[0], v.Index(i).String()) params.Add(tags[0], v.Index(i).String())
} }
} }
case reflect.Map:
if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String {
var s []string
for _, k := range v.MapKeys() {
value := v.MapIndex(k).String()
s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value))
}
params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", ")))
}
} }
} else { } else {
// Otherwise, the field is not set. // if the field has a 'required' tag, it can't have a zero-value
if len(tags) == 2 && tags[1] == "required" { if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
// And the field is required. Return an error. return &url.URL{}, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
return nil, fmt.Errorf("Required query parameter [%s] not set.", f.Name)
} }
} }
} }
@ -407,10 +454,9 @@ func BuildHeaders(opts interface{}) (map[string]string, error) {
optsMap[tags[0]] = strconv.FormatBool(v.Bool()) optsMap[tags[0]] = strconv.FormatBool(v.Bool())
} }
} else { } else {
// Otherwise, the field is not set. // if the field has a 'required' tag, it can't have a zero-value
if len(tags) == 2 && tags[1] == "required" { if requiredTag := f.Tag.Get("required"); requiredTag == "true" {
// And the field is required. Return an error. return optsMap, fmt.Errorf("Required header [%s] not set.", f.Name)
return optsMap, fmt.Errorf("Required header not set.")
} }
} }
} }

View file

@ -7,6 +7,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"strings" "strings"
"sync"
) )
// DefaultUserAgent is the default User-Agent string set in the request header. // DefaultUserAgent is the default User-Agent string set in the request header.
@ -51,6 +52,8 @@ type ProviderClient struct {
IdentityEndpoint string IdentityEndpoint string
// TokenID is the ID of the most recently issued valid token. // TokenID is the ID of the most recently issued valid token.
// NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application.
// To safely read or write this value, call `Token` or `SetToken`, respectively
TokenID string TokenID string
// EndpointLocator describes how this provider discovers the endpoints for // EndpointLocator describes how this provider discovers the endpoints for
@ -68,16 +71,89 @@ type ProviderClient struct {
// authentication functions for different Identity service versions. // authentication functions for different Identity service versions.
ReauthFunc func() error ReauthFunc func() error
Debug bool mut *sync.RWMutex
reauthmut *reauthlock
}
type reauthlock struct {
sync.RWMutex
reauthing bool
} }
// AuthenticatedHeaders returns a map of HTTP headers that are common for all // AuthenticatedHeaders returns a map of HTTP headers that are common for all
// authenticated service requests. // authenticated service requests.
func (client *ProviderClient) AuthenticatedHeaders() map[string]string { func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
if client.TokenID == "" { if client.reauthmut != nil {
return map[string]string{} client.reauthmut.RLock()
if client.reauthmut.reauthing {
client.reauthmut.RUnlock()
return
}
client.reauthmut.RUnlock()
} }
return map[string]string{"X-Auth-Token": client.TokenID} t := client.Token()
if t == "" {
return
}
return map[string]string{"X-Auth-Token": t}
}
// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
// If the application's ProviderClient is not used concurrently, this doesn't need to be called.
func (client *ProviderClient) UseTokenLock() {
client.mut = new(sync.RWMutex)
client.reauthmut = new(reauthlock)
}
// Token safely reads the value of the auth token from the ProviderClient. Applications should
// call this method to access the token instead of the TokenID field
func (client *ProviderClient) Token() string {
if client.mut != nil {
client.mut.RLock()
defer client.mut.RUnlock()
}
return client.TokenID
}
// SetToken safely sets the value of the auth token in the ProviderClient. Applications may
// use this method in a custom ReauthFunc
func (client *ProviderClient) SetToken(t string) {
if client.mut != nil {
client.mut.Lock()
defer client.mut.Unlock()
}
client.TokenID = t
}
// Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
// called because of a 401 response, the caller may pass the previous token. In
// this case, the reauthentication can be skipped if another thread has already
// reauthenticated in the meantime. If no previous token is known, an empty
// string should be passed instead to force unconditional reauthentication.
func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
if client.ReauthFunc == nil {
return nil
}
if client.mut == nil {
return client.ReauthFunc()
}
client.mut.Lock()
defer client.mut.Unlock()
client.reauthmut.Lock()
client.reauthmut.reauthing = true
client.reauthmut.Unlock()
if previousToken == "" || client.TokenID == previousToken {
err = client.ReauthFunc()
}
client.reauthmut.Lock()
client.reauthmut.reauthing = false
client.reauthmut.Unlock()
return
} }
// RequestOpts customizes the behavior of the provider.Request() method. // RequestOpts customizes the behavior of the provider.Request() method.
@ -145,10 +221,6 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
} }
req.Header.Set("Accept", applicationJSON) req.Header.Set("Accept", applicationJSON)
for k, v := range client.AuthenticatedHeaders() {
req.Header.Add(k, v)
}
// Set the User-Agent header // Set the User-Agent header
req.Header.Set("User-Agent", client.UserAgent.Join()) req.Header.Set("User-Agent", client.UserAgent.Join())
@ -162,9 +234,16 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
} }
} }
// get latest token from client
for k, v := range client.AuthenticatedHeaders() {
req.Header.Set(k, v)
}
// Set connection parameter to close the connection immediately when we've got the response // Set connection parameter to close the connection immediately when we've got the response
req.Close = true req.Close = true
prereqtok := req.Header.Get("X-Auth-Token")
// Issue the request. // Issue the request.
resp, err := client.HTTPClient.Do(req) resp, err := client.HTTPClient.Do(req)
if err != nil { if err != nil {
@ -188,9 +267,6 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
if !ok { if !ok {
body, _ := ioutil.ReadAll(resp.Body) body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
//pc := make([]uintptr, 1)
//runtime.Callers(2, pc)
//f := runtime.FuncForPC(pc[0])
respErr := ErrUnexpectedResponseCode{ respErr := ErrUnexpectedResponseCode{
URL: url, URL: url,
Method: method, Method: method,
@ -198,7 +274,6 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
Actual: resp.StatusCode, Actual: resp.StatusCode,
Body: body, Body: body,
} }
//respErr.Function = "gophercloud.ProviderClient.Request"
errType := options.ErrorContext errType := options.ErrorContext
switch resp.StatusCode { switch resp.StatusCode {
@ -209,7 +284,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
} }
case http.StatusUnauthorized: case http.StatusUnauthorized:
if client.ReauthFunc != nil { if client.ReauthFunc != nil {
err = client.ReauthFunc() err = client.Reauthenticate(prereqtok)
if err != nil { if err != nil {
e := &ErrUnableToReauthenticate{} e := &ErrUnableToReauthenticate{}
e.ErrOriginal = respErr e.ErrOriginal = respErr
@ -239,6 +314,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
if error401er, ok := errType.(Err401er); ok { if error401er, ok := errType.(Err401er); ok {
err = error401er.Error401(respErr) err = error401er.Error401(respErr)
} }
case http.StatusForbidden:
err = ErrDefault403{respErr}
if error403er, ok := errType.(Err403er); ok {
err = error403er.Error403(respErr)
}
case http.StatusNotFound: case http.StatusNotFound:
err = ErrDefault404{respErr} err = ErrDefault404{respErr}
if error404er, ok := errType.(Err404er); ok { if error404er, ok := errType.(Err404er); ok {
@ -298,7 +378,7 @@ func defaultOkCodes(method string) []int {
case method == "PUT": case method == "PUT":
return []int{201, 202} return []int{201, 202}
case method == "PATCH": case method == "PATCH":
return []int{200, 204} return []int{200, 202, 204}
case method == "DELETE": case method == "DELETE":
return []int{202, 204} return []int{202, 204}
} }

View file

@ -78,6 +78,77 @@ func (r Result) extractIntoPtr(to interface{}, label string) error {
return err return err
} }
toValue := reflect.ValueOf(to)
if toValue.Kind() == reflect.Ptr {
toValue = toValue.Elem()
}
switch toValue.Kind() {
case reflect.Slice:
typeOfV := toValue.Type().Elem()
if typeOfV.Kind() == reflect.Struct {
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0)
if mSlice, ok := m[label].([]interface{}); ok {
for _, v := range mSlice {
// For each iteration of the slice, we create a new struct.
// This is to work around a bug where elements of a slice
// are reused and not overwritten when the same copy of the
// struct is used:
//
// https://github.com/golang/go/issues/21092
// https://github.com/golang/go/issues/24155
// https://play.golang.org/p/NHo3ywlPZli
newType := reflect.New(typeOfV).Elem()
b, err := json.Marshal(v)
if err != nil {
return err
}
// This is needed for structs with an UnmarshalJSON method.
// Technically this is just unmarshalling the response into
// a struct that is never used, but it's good enough to
// trigger the UnmarshalJSON method.
for i := 0; i < newType.NumField(); i++ {
s := newType.Field(i).Addr().Interface()
// Unmarshal is used rather than NewDecoder to also work
// around the above-mentioned bug.
err = json.Unmarshal(b, s)
if err != nil {
return err
}
}
newSlice = reflect.Append(newSlice, newType)
}
}
// "to" should now be properly modeled to receive the
// JSON response body and unmarshal into all the correct
// fields of the struct or composed extension struct
// at the end of this method.
toValue.Set(newSlice)
}
}
case reflect.Struct:
typeOfV := toValue.Type()
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
for i := 0; i < toValue.NumField(); i++ {
toField := toValue.Field(i)
if toField.Kind() == reflect.Struct {
s := toField.Addr().Interface()
err = json.NewDecoder(bytes.NewReader(b)).Decode(s)
if err != nil {
return err
}
}
}
}
}
err = json.Unmarshal(b, &to) err = json.Unmarshal(b, &to)
return err return err
} }
@ -177,9 +248,8 @@ type HeaderResult struct {
Result Result
} }
// ExtractHeader will return the http.Header and error from the HeaderResult. // ExtractInto allows users to provide an object into which `Extract` will
// // extract the http.Header headers of the result.
// header, err := objects.Create(client, "my_container", objects.CreateOpts{}).ExtractHeader()
func (r HeaderResult) ExtractInto(to interface{}) error { func (r HeaderResult) ExtractInto(to interface{}) error {
if r.Err != nil { if r.Err != nil {
return r.Err return r.Err
@ -299,6 +369,48 @@ func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// RFC3339ZNoT is the time format used in Zun (Containers Service).
const RFC3339ZNoT = "2006-01-02 15:04:05-07:00"
type JSONRFC3339ZNoT time.Time
func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(RFC3339ZNoT, s)
if err != nil {
return err
}
*jt = JSONRFC3339ZNoT(t)
return nil
}
// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service).
const RFC3339ZNoTNoZ = "2006-01-02 15:04:05"
type JSONRFC3339ZNoTNoZ time.Time
func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
if s == "" {
return nil
}
t, err := time.Parse(RFC3339ZNoTNoZ, s)
if err != nil {
return err
}
*jt = JSONRFC3339ZNoTNoZ(t)
return nil
}
/* /*
Link is an internal type to be used in packages of collection resources that are Link is an internal type to be used in packages of collection resources that are
paginated in a certain way. paginated in a certain way.

View file

@ -28,6 +28,10 @@ type ServiceClient struct {
// The microversion of the service to use. Set this to use a particular microversion. // The microversion of the service to use. Set this to use a particular microversion.
Microversion string Microversion string
// MoreHeaders allows users (or Gophercloud) to set service-wide headers on requests. Put another way,
// values set in this field will be set on all the HTTP requests the service client sends.
MoreHeaders map[string]string
} }
// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. // ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /.
@ -108,15 +112,39 @@ func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Respon
return client.Request("DELETE", url, opts) return client.Request("DELETE", url, opts)
} }
// Head calls `Request` with the "HEAD" HTTP verb.
func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response, error) {
if opts == nil {
opts = new(RequestOpts)
}
client.initReqOpts(url, nil, nil, opts)
return client.Request("HEAD", url, opts)
}
func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) {
switch client.Type { switch client.Type {
case "compute": case "compute":
opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion
case "sharev2": case "sharev2":
opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion
case "volume":
opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion
} }
if client.Type != "" { if client.Type != "" {
opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion
} }
} }
// Request carries out the HTTP operation for the service client
func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
if len(client.MoreHeaders) > 0 {
if options == nil {
options = new(RequestOpts)
}
for k, v := range client.MoreHeaders {
options.MoreHeaders[k] = v
}
}
return client.ProviderClient.Request(method, url, options)
}

2
vendor/modules.txt vendored
View file

@ -90,7 +90,7 @@ github.com/google/pprof/profile
github.com/googleapis/gnostic/OpenAPIv2 github.com/googleapis/gnostic/OpenAPIv2
github.com/googleapis/gnostic/compiler github.com/googleapis/gnostic/compiler
github.com/googleapis/gnostic/extensions github.com/googleapis/gnostic/extensions
# github.com/gophercloud/gophercloud v0.0.0-20170607034829-caf34a65f602 # github.com/gophercloud/gophercloud v0.0.0-20181206160319-9d88c34913a9
github.com/gophercloud/gophercloud github.com/gophercloud/gophercloud
github.com/gophercloud/gophercloud/openstack github.com/gophercloud/gophercloud/openstack
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips