Update to latest gophercloud

Catch up with all the latest stuff from gophercloud

4b7db606 - only try to reauth once
d13755e6 - BlockStorage v3: Rename VolumeType PublicAccess to IsPublic
614da04d - Add UPDATE support in V3 volume types (#656)
be3fd784 - Flavor Extra Specs Create
c2cafb46 - Flavor Extra Specs: List / Get
7b1b8775 - Compute v2: Flavor Access Add
cf81d92c - Add DELETE support in V3 volume types
a879b375 - Fix incorrect variable name
2997913a - Add pagination support in snapshots
a5c71868 - Support pagination in volume resources
1db0312e - TrivialFix incorrect variable name
69194d93 - Add basic CRUD acceptance testcases in snapshot V3
22c7abce - Add CREATE support in V3 volume types
aed60e9f - Add basic CRUD acceptance in volume V3
7cbf4661 - BlockStorage v3: volumetype get/list acc test
bcab0f79 - Update README with Thank Yous
f85e7c0f - Docs: Updating Contributing and Style Guides
be1b616c - Fix a small syntax error of TestShareTypeExtraSpecs test
3f38a1ee - Add List/Get support for volume type in volume V3
48a40399 - Support for setting availability_zone_hints to a router
747776a7 - Fix the undefined function error of TestPortsbindingCRUD test
a7ec61ea - Fix the undefined function error of TestNetworksProviderCRUD test
25e18920 - Compute v2: Add the extended status information API
b63d2fd3 - availability_zone_hints for network(s)
157d7511 - Add support for ipv6_address_mode and ipv6_ra_mode in subnets
ed468967 - DBv1: configurations acceptance test
578e2aab - Configuration group time parsing error
669959f8 - Compute v2: attachinterfaces acceptance test
8113f0cb - Add Nova interface-detach support
d6484abc - Add Nova interface-attach support
7883fd95 - fix reauth deadlock by not calling Token() during reauth
4d0f8253 - Add support to get interface of a server
7dc13e0d - AccTests: BlockStorage v2 ForceDelete
1e86e54d - Refactor blockstorage actionURL
e30da231 - Feature/support force delete
e193578c - add UseTokenLock method in ProviderClient to allow safe concurrent access
e6a5f874 - ObjectStorage v1: Rename ExtractLastMarker to extractLastMarker
c47bb004 - BlockStorage v2/v3: Reorder snapshot/volume ListOpts and update godoc
2c05d0e4 - Add 'tenant' support in volume&snapshot API
639d71fd - Networking v2: Port Security Extension
755794a7 - ObjectStorage v1: Subdir and Marker detection
a043441f - fixed bug with endless loop when using delimiter on folded directory
a4799293 - OpenStack: support OS_PROJECT_* variables
This commit is contained in:
Davanum Srinivas 2017-12-22 17:12:51 -05:00
parent ff58401257
commit 406ef92623
26 changed files with 605 additions and 124 deletions

60
Godeps/Godeps.json generated
View File

@ -1633,123 +1633,123 @@
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud", "ImportPath": "github.com/gophercloud/gophercloud",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack", "ImportPath": "github.com/gophercloud/gophercloud/openstack",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions", "ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes", "ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v1/volumes",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes", "ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v2/volumes",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes", "ImportPath": "github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/common/extensions", "ImportPath": "github.com/gophercloud/gophercloud/openstack/common/extensions",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces", "ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach", "ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/volumeattach",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors", "ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/images", "ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/images",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/servers", "ImportPath": "github.com/gophercloud/gophercloud/openstack/compute/v2/servers",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/trusts",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/loadbalancers",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/monitors",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/lbaas_v2/pools",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/groups",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/security/rules",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/networks", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/networks",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/ports", "ImportPath": "github.com/gophercloud/gophercloud/openstack/networking/v2/ports",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/utils", "ImportPath": "github.com/gophercloud/gophercloud/openstack/utils",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/pagination", "ImportPath": "github.com/gophercloud/gophercloud/pagination",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gorilla/context", "ImportPath": "github.com/gorilla/context",

View File

@ -408,31 +408,31 @@
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud", "ImportPath": "github.com/gophercloud/gophercloud",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack", "ImportPath": "github.com/gophercloud/gophercloud/openstack",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/utils", "ImportPath": "github.com/gophercloud/gophercloud/openstack/utils",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/pagination", "ImportPath": "github.com/gophercloud/gophercloud/pagination",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gregjones/httpcache", "ImportPath": "github.com/gregjones/httpcache",

View File

@ -172,31 +172,31 @@
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud", "ImportPath": "github.com/gophercloud/gophercloud",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack", "ImportPath": "github.com/gophercloud/gophercloud/openstack",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens", "ImportPath": "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/openstack/utils", "ImportPath": "github.com/gophercloud/gophercloud/openstack/utils",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gophercloud/gophercloud/pagination", "ImportPath": "github.com/gophercloud/gophercloud/pagination",
"Rev": "8183543f90d1aef267a5ecc209f2e0715b355acb" "Rev": "8e59687aa4b27ab22a0bf3295f1e165ff7bd5f97"
}, },
{ {
"ImportPath": "github.com/gregjones/httpcache", "ImportPath": "github.com/gregjones/httpcache",

View File

@ -141,3 +141,19 @@ See the [contributing guide](./.github/CONTRIBUTING.md).
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](/issues).
## Thank You
We'd like to extend special thanks and appreciation to the following:
### OpenLab
<a href="http://openlabtesting.org/"><img src="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="assets/vexxhost.png" width="600px"></a>
VEXXHOST is providing their services to assist with the development and testing of Gophercloud.

View File

@ -1,6 +1,8 @@
## On Pull Requests ## On Pull Requests
- Please make sure to read our [contributing guide](/.github/CONTRIBUTING.md).
- Before you start a PR there needs to be a Github issue and a discussion about it - 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'. on that issue with a core contributor, even if it's just a 'SGTM'.
@ -34,6 +36,9 @@
append. It makes it difficult for the reviewer to see what's changed from one append. It makes it difficult for the reviewer to see what's changed from one
review to the next. review to the next.
- See [#583](https://github.com/gophercloud/gophercloud/issues/583) as an example of a
well-formatted issue which contains all relevant information we need to review and approve.
## On Code ## On Code
- In re design: follow as closely as is reasonable the code already in the library. - In re design: follow as closely as is reasonable the code already in the library.

View File

@ -16,7 +16,12 @@ The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME.
Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings,
or an error will result. OS_TENANT_ID and OS_TENANT_NAME are optional. 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, To use this function, first set the OS_* environment variables (for example,
by sourcing an `openrc` file), then: by sourcing an `openrc` file), then:
@ -34,6 +39,16 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
domainID := os.Getenv("OS_DOMAIN_ID") domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME") domainName := os.Getenv("OS_DOMAIN_NAME")
// 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.ErrMissingInput{Argument: "authURL"}
return nilOptions, err return nilOptions, err

View File

@ -47,7 +47,7 @@ func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(attachURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
@ -56,7 +56,7 @@ func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder
// BeginDetach will mark the volume as detaching. // BeginDetach will mark the volume as detaching.
func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) { func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) {
b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})} b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})}
_, r.Err = client.Post(beginDetachingURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
@ -87,7 +87,7 @@ func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(detachURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
@ -96,7 +96,7 @@ func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder
// Reserve will reserve a volume based on volume ID. // Reserve will reserve a volume based on volume ID.
func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) { func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
b := map[string]interface{}{"os-reserve": make(map[string]interface{})} b := map[string]interface{}{"os-reserve": make(map[string]interface{})}
_, r.Err = client.Post(reserveURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202}, OkCodes: []int{200, 201, 202},
}) })
return return
@ -105,7 +105,7 @@ func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
// Unreserve will unreserve a volume based on volume ID. // Unreserve will unreserve a volume based on volume ID.
func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) { func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) {
b := map[string]interface{}{"os-unreserve": make(map[string]interface{})} b := map[string]interface{}{"os-unreserve": make(map[string]interface{})}
_, r.Err = client.Post(unreserveURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202}, OkCodes: []int{200, 201, 202},
}) })
return return
@ -145,7 +145,7 @@ func InitializeConnection(client *gophercloud.ServiceClient, id string, opts Ini
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(initializeConnectionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202}, OkCodes: []int{200, 201, 202},
}) })
return return
@ -183,7 +183,7 @@ func TerminateConnection(client *gophercloud.ServiceClient, id string, opts Term
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(teminateConnectionURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
@ -216,7 +216,7 @@ func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOpt
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(extendSizeURL(client, id), b, nil, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
@ -256,8 +256,14 @@ func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageO
r.Err = err r.Err = err
return return
} }
_, r.Err = client.Post(uploadURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ _, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
return return
} }
// ForceDelete will delete the volume regardless of state.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil)
return
}

View File

@ -184,3 +184,8 @@ func (r UploadImageResult) Extract() (VolumeImage, error) {
err := r.ExtractInto(&s) err := r.ExtractInto(&s)
return s.VolumeImage, err return s.VolumeImage, err
} }
// ForceDeleteResult contains the response body and error from a ForceDelete request.
type ForceDeleteResult struct {
gophercloud.ErrResult
}

View File

@ -2,38 +2,6 @@ package volumeactions
import "github.com/gophercloud/gophercloud" import "github.com/gophercloud/gophercloud"
func attachURL(c *gophercloud.ServiceClient, id string) string { func actionURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("volumes", id, "action") return c.ServiceURL("volumes", id, "action")
} }
func beginDetachingURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func detachURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func uploadURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func reserveURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func unreserveURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func initializeConnectionURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func teminateConnectionURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}
func extendSizeURL(c *gophercloud.ServiceClient, id string) string {
return attachURL(c, id)
}

View File

@ -83,14 +83,21 @@ type ListOptsBuilder interface {
// ListOpts holds options for listing Volumes. It is passed to the volumes.List // ListOpts holds options for listing Volumes. It is passed to the volumes.List
// function. // function.
type ListOpts struct { type ListOpts struct {
// admin-only option. Set it to true to see all tenant volumes. // AllTenants will retrieve volumes of all tenants/projects.
AllTenants bool `q:"all_tenants"` AllTenants bool `q:"all_tenants"`
// List only volumes that contain Metadata.
// Metadata will filter results based on specified metadata.
Metadata map[string]string `q:"metadata"` Metadata map[string]string `q:"metadata"`
// List only volumes that have Name as the display name.
// Name will filter by the specified volume name.
Name string `q:"name"` Name string `q:"name"`
// List only volumes that have a status of Status.
// Status will filter by the specified status.
Status string `q:"status"` Status string `q:"status"`
// TenantID will filter by a specific tenant/project ID.
// Setting AllTenants is required for this.
TenantID string `q:"project_id"`
} }
// ToVolumeListQuery formats a ListOpts into a query string. // ToVolumeListQuery formats a ListOpts into a query string.

View File

@ -83,14 +83,34 @@ type ListOptsBuilder interface {
// ListOpts holds options for listing Volumes. It is passed to the volumes.List // ListOpts holds options for listing Volumes. It is passed to the volumes.List
// function. // function.
type ListOpts struct { type ListOpts struct {
// admin-only option. Set it to true to see all tenant volumes. // AllTenants will retrieve volumes of all tenants/projects.
AllTenants bool `q:"all_tenants"` AllTenants bool `q:"all_tenants"`
// List only volumes that contain Metadata.
// Metadata will filter results based on specified metadata.
Metadata map[string]string `q:"metadata"` Metadata map[string]string `q:"metadata"`
// List only volumes that have Name as the display name.
// Name will filter by the specified volume name.
Name string `q:"name"` Name string `q:"name"`
// List only volumes that have a status of Status.
// Status will filter by the specified status.
Status string `q:"status"` Status string `q:"status"`
// TenantID will filter by a specific tenant/project ID.
// Setting AllTenants is required for this.
TenantID string `q:"project_id"`
// Comma-separated list of sort keys and optional sort directions in the
// form of <key>[:<direction>].
Sort string `q:"sort"`
// Requests a page size of items.
Limit int `q:"limit"`
// Used in conjunction with limit to return a slice of items.
Offset int `q:"offset"`
// The ID of the last-seen item.
Marker string `q:"marker"`
} }
// ToVolumeListQuery formats a ListOpts into a query string. // ToVolumeListQuery formats a ListOpts into a query string.
@ -111,7 +131,7 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa
} }
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return VolumePage{pagination.SinglePageBase(r)} return VolumePage{pagination.LinkedPageBase{PageResult: r}}
}) })
} }

View File

@ -101,7 +101,7 @@ func (r *Volume) UnmarshalJSON(b []byte) error {
// VolumePage is a pagination.pager that is returned from a call to the List function. // VolumePage is a pagination.pager that is returned from a call to the List function.
type VolumePage struct { type VolumePage struct {
pagination.SinglePageBase pagination.LinkedPageBase
} }
// IsEmpty returns true if a ListResult contains no Volumes. // IsEmpty returns true if a ListResult contains no Volumes.
@ -110,6 +110,17 @@ func (r VolumePage) IsEmpty() (bool, error) {
return len(volumes) == 0, err return len(volumes) == 0, err
} }
func (page VolumePage) NextPageURL() (string, error) {
var s struct {
Links []gophercloud.Link `json:"volumes_links"`
}
err := page.ExtractInto(&s)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(s.Links)
}
// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. // ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call.
func ExtractVolumes(r pagination.Page) ([]Volume, error) { func ExtractVolumes(r pagination.Page) ([]Volume, error) {
var s []Volume var s []Volume

View File

@ -56,11 +56,12 @@ func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
endpoint = gophercloud.NormalizeURL(endpoint) endpoint = gophercloud.NormalizeURL(endpoint)
base = gophercloud.NormalizeURL(base) base = gophercloud.NormalizeURL(base)
return &gophercloud.ProviderClient{ p := new(gophercloud.ProviderClient)
IdentityBase: base, p.IdentityBase = base
IdentityEndpoint: endpoint, p.IdentityEndpoint = endpoint
}, nil p.UseTokenLock()
return p, nil
} }
/* /*
@ -158,9 +159,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
@ -202,9 +215,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) {

View File

@ -18,5 +18,35 @@ Example of Listing a Server's Interfaces
for _, interface := range allInterfaces { for _, interface := range allInterfaces {
fmt.Printf("%+v\n", interface) fmt.Printf("%+v\n", interface)
} }
Example to Get a Server's Interface
portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e"
serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f"
interface, err := attachinterfaces.Get(computeClient, serverID, portID).Extract()
if err != nil {
panic(err)
}
Example to Create a new Interface attachment on the Server
networkID := "8a5fe506-7e9f-4091-899b-96336909d93c"
serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f"
attachOpts := attachinterfaces.CreateOpts{
NetworkID: networkID,
}
interface, err := attachinterfaces.Create(computeClient, serverID, attachOpts).Extract()
if err != nil {
panic(err)
}
Example to Delete an Interface attachment from the Server
portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e"
serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f"
err := attachinterfaces.Delete(computeClient, serverID, portID).ExtractErr()
if err != nil {
panic(err)
}
*/ */
package attachinterfaces package attachinterfaces

View File

@ -11,3 +11,62 @@ func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
return InterfacePage{pagination.SinglePageBase(r)} return InterfacePage{pagination.SinglePageBase(r)}
}) })
} }
// Get requests details on a single interface attachment by the server and port IDs.
func Get(client *gophercloud.ServiceClient, serverID, portID string) (r GetResult) {
_, r.Err = client.Get(getInterfaceURL(client, serverID, portID), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// CreateOptsBuilder allows extensions to add additional parameters to the
// Create request.
type CreateOptsBuilder interface {
ToAttachInterfacesCreateMap() (map[string]interface{}, error)
}
// CreateOpts specifies parameters of a new interface attachment.
type CreateOpts struct {
// PortID is the ID of the port for which you want to create an interface.
// The NetworkID and PortID parameters are mutually exclusive.
// If you do not specify the PortID parameter, the OpenStack Networking API
// v2.0 allocates a port and creates an interface for it on the network.
PortID string `json:"port_id,omitempty"`
// NetworkID is the ID of the network for which you want to create an interface.
// The NetworkID and PortID parameters are mutually exclusive.
// If you do not specify the NetworkID parameter, the OpenStack Networking
// API v2.0 uses the network information cache that is associated with the instance.
NetworkID string `json:"net_id,omitempty"`
// Slice of FixedIPs. If you request a specific FixedIP address without a
// NetworkID, the request returns a Bad Request (400) response code.
FixedIPs []FixedIP `json:"fixed_ips,omitempty"`
}
// ToAttachInterfacesCreateMap constructs a request body from CreateOpts.
func (opts CreateOpts) ToAttachInterfacesCreateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "interfaceAttachment")
}
// Create requests the creation of a new interface attachment on the server.
func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) {
b, err := opts.ToAttachInterfacesCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(createInterfaceURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// Delete makes a request against the nova API to detach a single interface from the server.
// It needs server and port IDs to make a such request.
func Delete(client *gophercloud.ServiceClient, serverID, portID string) (r DeleteResult) {
_, r.Err = client.Delete(deleteInterfaceURL(client, serverID, portID), nil)
return
}

View File

@ -1,9 +1,41 @@
package attachinterfaces package attachinterfaces
import ( import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
type attachInterfaceResult struct {
gophercloud.Result
}
// Extract interprets any attachInterfaceResult as an Interface, if possible.
func (r attachInterfaceResult) Extract() (*Interface, error) {
var s struct {
Interface *Interface `json:"interfaceAttachment"`
}
err := r.ExtractInto(&s)
return s.Interface, err
}
// GetResult is the response from a Get operation. Call its Extract
// method to interpret it as an Interface.
type GetResult struct {
attachInterfaceResult
}
// CreateResult is the response from a Create operation. Call its Extract
// method to interpret it as an Interface.
type CreateResult struct {
attachInterfaceResult
}
// DeleteResult is the response from a Delete operation. Call its ExtractErr
// method to determine if the call succeeded or failed.
type DeleteResult struct {
gophercloud.ErrResult
}
// FixedIP represents a Fixed IP Address. // FixedIP represents a Fixed IP Address.
type FixedIP struct { type FixedIP struct {
SubnetID string `json:"subnet_id"` SubnetID string `json:"subnet_id"`

View File

@ -5,3 +5,14 @@ import "github.com/gophercloud/gophercloud"
func listInterfaceURL(client *gophercloud.ServiceClient, serverID string) string { func listInterfaceURL(client *gophercloud.ServiceClient, serverID string) string {
return client.ServiceURL("servers", serverID, "os-interface") return client.ServiceURL("servers", serverID, "os-interface")
} }
func getInterfaceURL(client *gophercloud.ServiceClient, serverID, portID string) string {
return client.ServiceURL("servers", serverID, "os-interface", portID)
}
func createInterfaceURL(client *gophercloud.ServiceClient, serverID string) string {
return client.ServiceURL("servers", serverID, "os-interface")
}
func deleteInterfaceURL(client *gophercloud.ServiceClient, serverID, portID string) string {
return client.ServiceURL("servers", serverID, "os-interface", portID)
}

View File

@ -59,5 +59,45 @@ Example to List Flavor Access
for _, access := range allAccesses { for _, access := range allAccesses {
fmt.Printf("%+v", access) 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 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)
*/ */
package flavors package flavors

View File

@ -162,6 +162,75 @@ func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager
}) })
} }
// AddAccessOptsBuilder allows extensions to add additional parameters to the
// AddAccess requests.
type AddAccessOptsBuilder interface {
ToAddAccessMap() (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"`
}
// ToAddAccessMap constructs a request body from AddAccessOpts.
func (opts AddAccessOpts) ToAddAccessMap() (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.ToAddAccessMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// ExtraSpecs requests all the extra-specs for the given flavor ID.
func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) {
_, r.Err = client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil)
return
}
func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) {
_, r.Err = client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil)
return
}
// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the
// CreateExtraSpecs requests.
type CreateExtraSpecsOptsBuilder interface {
ToExtraSpecsCreateMap() (map[string]interface{}, error)
}
// ExtraSpecsOpts is a map that contains key-value pairs.
type ExtraSpecsOpts map[string]string
// ToExtraSpecsCreateMap assembles a body for a Create request based on the
// contents of a ExtraSpecsOpts
func (opts ExtraSpecsOpts) ToExtraSpecsCreateMap() (map[string]interface{}, error) {
return map[string]interface{}{"extra_specs": opts}, nil
}
// CreateExtraSpecs will create or update the extra-specs key-value pairs for the specified Flavor
func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) {
b, err := opts.ToExtraSpecsCreateMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}
// IDFromName is a convienience function that returns a flavor's ID given its // IDFromName is a convienience function that returns a flavor's ID given its
// name. // name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) { func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {

View File

@ -154,6 +154,26 @@ func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) {
return s.FlavorAccesses, err return s.FlavorAccesses, err
} }
type accessResult struct {
gophercloud.Result
}
// AddAccessResult is the response of an AddAccess operations. Call its
// Extract method to interpret it as a slice of FlavorAccess.
type AddAccessResult 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. // FlavorAccess represents an ACL of tenant access to a specific Flavor.
type FlavorAccess struct { type FlavorAccess struct {
// FlavorID is the unique ID of the flavor. // FlavorID is the unique ID of the flavor.
@ -162,3 +182,50 @@ type FlavorAccess struct {
// TenantID is the unique ID of the tenant. // TenantID is the unique ID of the tenant.
TenantID string `json:"tenant_id"` 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
}
// 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

@ -23,3 +23,19 @@ func deleteURL(client *gophercloud.ServiceClient, id string) string {
func accessURL(client *gophercloud.ServiceClient, id string) string { func accessURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("flavors", id, "os-flavor-access") 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")
}

View File

@ -49,11 +49,12 @@ type CreateOptsBuilder interface {
// CreateOpts contains all the values needed to create a new router. There are // CreateOpts contains all the values needed to create a new router. There are
// no required values. // no required values.
type CreateOpts struct { type CreateOpts struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Distributed *bool `json:"distributed,omitempty"` Distributed *bool `json:"distributed,omitempty"`
TenantID string `json:"tenant_id,omitempty"` TenantID string `json:"tenant_id,omitempty"`
GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"`
AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"`
} }
// ToRouterCreateMap builds a create request body from CreateOpts. // ToRouterCreateMap builds a create request body from CreateOpts.

View File

@ -60,6 +60,10 @@ type Router struct {
// Routes are a collection of static routes that the router will host. // Routes are a collection of static routes that the router will host.
Routes []Route `json:"routes"` Routes []Route `json:"routes"`
// Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others.
// Used to make network resources highly available.
AvailabilityZoneHints []string `json:"availability_zone_hints"`
} }
// RouterPage is the page returned by a pager when traversing over a // RouterPage is the page returned by a pager when traversing over a

View File

@ -66,10 +66,11 @@ type CreateOptsBuilder interface {
// CreateOpts represents options used to create a network. // CreateOpts represents options used to create a network.
type CreateOpts struct { type CreateOpts struct {
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Shared *bool `json:"shared,omitempty"` Shared *bool `json:"shared,omitempty"`
TenantID string `json:"tenant_id,omitempty"` TenantID string `json:"tenant_id,omitempty"`
AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"`
} }
// ToNetworkCreateMap builds a request body from CreateOpts. // ToNetworkCreateMap builds a request body from CreateOpts.

View File

@ -69,6 +69,10 @@ type Network struct {
// Specifies whether the network resource can be accessed by any tenant. // Specifies whether the network resource can be accessed by any tenant.
Shared bool `json:"shared"` Shared bool `json:"shared"`
// Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others.
// Used to make network resources highly available.
AvailabilityZoneHints []string `json:"availability_zone_hints"`
} }
// NetworkPage is the page returned by a pager when traversing over a // NetworkPage is the page returned by a pager when traversing over a

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,59 @@ 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
} }
// RequestOpts customizes the behavior of the provider.Request() method. // RequestOpts customizes the behavior of the provider.Request() method.
@ -166,6 +212,8 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
// 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 {
@ -189,9 +237,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,
@ -199,7 +244,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 {
@ -210,7 +254,21 @@ 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() if client.mut != nil {
client.mut.Lock()
client.reauthmut.Lock()
client.reauthmut.reauthing = true
client.reauthmut.Unlock()
if curtok := client.TokenID; curtok == prereqtok {
err = client.ReauthFunc()
}
client.reauthmut.Lock()
client.reauthmut.reauthing = false
client.reauthmut.Unlock()
client.mut.Unlock()
} else {
err = client.ReauthFunc()
}
if err != nil { if err != nil {
e := &ErrUnableToReauthenticate{} e := &ErrUnableToReauthenticate{}
e.ErrOriginal = respErr e.ErrOriginal = respErr