Merge pull request #9573 from zhsj/bump-gophercloud

Bump gophercloud to latest version
This commit is contained in:
Megan Marsh 2020-07-14 10:11:06 -07:00 committed by GitHub
commit f5031a1eb5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 3320 additions and 896 deletions

View File

@ -6,6 +6,7 @@ import (
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
flavors_utils "github.com/gophercloud/utils/openstack/compute/v2/flavors"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
@ -37,7 +38,7 @@ func (s *StepLoadFlavor) Run(ctx context.Context, state multistep.StateBag) mult
geterr := err
log.Printf("[INFO] Loading flavor by name: %s", s.Flavor)
id, err := flavors.IDFromName(client, s.Flavor)
id, err := flavors_utils.IDFromName(client, s.Flavor)
if err != nil {
log.Printf("[ERROR] Failed to find flavor by name: %s", err)
err = fmt.Errorf(

4
go.mod
View File

@ -53,8 +53,8 @@ require (
github.com/google/go-querystring v1.0.0 // indirect
github.com/google/shlex v0.0.0-20150127133951-6f45313302b9
github.com/google/uuid v1.1.1
github.com/gophercloud/gophercloud v0.2.0
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6
github.com/gophercloud/gophercloud v0.12.0
github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0

17
go.sum
View File

@ -285,10 +285,11 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gophercloud/gophercloud v0.2.0 h1:lD2Bce2xBAMNNcFZ0dObTpXkGLlVIb33RPVUNVpw6ic=
github.com/gophercloud/gophercloud v0.2.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 h1:Cw/B8Bu7Rryomxf7bjc8zNfIyLgjxsDd91n0eGRWpuo=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM=
github.com/gophercloud/gophercloud v0.12.0 h1:mZrie07npp6ODiwHZolTicr5jV8Ogn43AvAsSMm6Ork=
github.com/gophercloud/gophercloud v0.12.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c h1:iawx2ojEQA7c+GmkaVO5sN+k8YONibXyDO8RlsC+1bs=
github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 h1:JIM+OacoOJRU30xpjMf8sulYqjr0ViA3WDrTX6j/yDI=
@ -646,7 +647,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -656,6 +656,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmV
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200422194213-44a606286825 h1:dSChiwOTvzwbHFTMq2l6uRardHH7/E6SqEkqccinS/o=
golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -718,6 +719,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -745,7 +747,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -763,6 +764,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -809,6 +811,7 @@ golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@ -901,6 +904,8 @@ gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -7,9 +7,9 @@ install:
- GO111MODULE=off go get github.com/mattn/goveralls
- GO111MODULE=off go get golang.org/x/tools/cmd/goimports
go:
- "1.10"
- "1.11"
- "1.12"
- "1.13"
- "tip"
env:
global:

View File

@ -12,6 +12,7 @@
description: |
Run gophercloud acceptance test on master branch
run: .zuul/playbooks/gophercloud-acceptance-test/run.yaml
nodeset: ubuntu-bionic
- job:
name: gophercloud-acceptance-test-ironic
@ -19,6 +20,7 @@
description: |
Run gophercloud ironic acceptance test on master branch
run: .zuul/playbooks/gophercloud-acceptance-test-ironic/run.yaml
nodeset: ubuntu-bionic
- job:
name: gophercloud-acceptance-test-stein
@ -34,6 +36,7 @@
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on rocky branch
nodeset: ubuntu-xenial
vars:
global_env:
OS_BRANCH: stable/rocky
@ -43,6 +46,7 @@
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on queens branch
nodeset: ubuntu-xenial
vars:
global_env:
OS_BRANCH: stable/queens
@ -52,6 +56,7 @@
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on pike branch
nodeset: ubuntu-xenial
vars:
global_env:
OS_BRANCH: stable/pike
@ -61,6 +66,7 @@
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on ocata branch
nodeset: ubuntu-xenial
vars:
global_env:
OS_BRANCH: stable/ocata
@ -70,20 +76,11 @@
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on newton branch
nodeset: ubuntu-xenial
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:
@ -91,9 +88,6 @@
- gophercloud-unittest
- gophercloud-acceptance-test
- gophercloud-acceptance-test-ironic
recheck-mitaka:
jobs:
- gophercloud-acceptance-test-mitaka
recheck-newton:
jobs:
- gophercloud-acceptance-test-newton

View File

@ -1,4 +1,343 @@
## 0.3.0 (Unreleaesd)
## 0.13.0 (Unlreleased)
## 0.12.0 (June 25, 2020)
UPGRADE NOTES
* The URL used in the `compute/v2/extensions/bootfromvolume` package has been changed from `os-volumes_boot` to `servers`.
IMPROVEMENTS
* The URL used in the `compute/v2/extensions/bootfromvolume` package has been changed from `os-volumes_boot` to `servers` [GH-1973](https://github.com/gophercloud/gophercloud/pull/1973)
* Modify `baremetal/v1/nodes.LogicalDisk.PhysicalDisks` type to support physical disks hints [GH-1982](https://github.com/gophercloud/gophercloud/pull/1982)
* Added `baremetalintrospection/httpbasic` which provides an HTTP Basic Auth client [GH-1986](https://github.com/gophercloud/gophercloud/pull/1986)
* Added `baremetal/httpbasic` which provides an HTTP Basic Auth client [GH-1983](https://github.com/gophercloud/gophercloud/pull/1983)
* Added `containerinfra/v1/clusters.CreateOpts.MergeLabels` [GH-1985](https://github.com/gophercloud/gophercloud/pull/1985)
BUG FIXES
* Changed `containerinfra/v1/clusters.Cluster.HealthStatusReason` from `string` to `map[string]interface{}` [GH-1968](https://github.com/gophercloud/gophercloud/pull/1968)
* Fixed marshalling of `blockstorage/extensions/backups.ImportBackup.Metadata` [GH-1967](https://github.com/gophercloud/gophercloud/pull/1967)
* Fixed typo of "OAUth" to "OAuth" in `identity/v3/extensions/oauth1` [GH-1969](https://github.com/gophercloud/gophercloud/pull/1969)
* Fixed goroutine leak during reauthentication [GH-1978](https://github.com/gophercloud/gophercloud/pull/1978)
## 0.11.0 (May 14, 2020)
UPGRADE NOTES
* Object storage container and object names are now URL encoded [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930)
* All responses now have access to the returned headers. Please report any issues this has caused [GH-1942](https://github.com/gophercloud/gophercloud/pull/1942)
* Changes have been made to the internal HTTP client to ensure response bodies are handled in a way that enables connections to be re-used more efficiently [GH-1952](https://github.com/gophercloud/gophercloud/pull/1952)
IMPROVEMENTS
* Added `objectstorage/v1/containers.BulkDelete` [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930)
* Added `objectstorage/v1/objects.BulkDelete` [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930)
* Object storage container and object names are now URL encoded [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930)
* All responses now have access to the returned headers [GH-1942](https://github.com/gophercloud/gophercloud/pull/1942)
* Added `compute/v2/extensions/injectnetworkinfo.InjectNetworkInfo` [GH-1941](https://github.com/gophercloud/gophercloud/pull/1941)
* Added `compute/v2/extensions/resetnetwork.ResetNetwork` [GH-1941](https://github.com/gophercloud/gophercloud/pull/1941)
* Added `identity/v3/extensions/trusts.ListRoles` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939)
* Added `identity/v3/extensions/trusts.GetRole` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939)
* Added `identity/v3/extensions/trusts.CheckRole` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939)
* Added `identity/v3/extensions/oauth1.Create` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.CreateConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.DeleteConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.ListConsumers` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.GetConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.UpdateConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.RequestToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.AuthorizeToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.CreateAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.GetAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.RevokeAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.ListAccessTokens` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.ListAccessTokenRoles` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `identity/v3/extensions/oauth1.GetAccessTokenRole` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935)
* Added `networking/v2/extensions/agents.Update` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954)
* Added `networking/v2/extensions/agents.Delete` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954)
* Added `networking/v2/extensions/agents.ScheduleDHCPNetwork` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954)
* Added `networking/v2/extensions/agents.RemoveDHCPNetwork` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954)
* Added `identity/v3/projects.CreateOpts.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `identity/v3/projects.CreateOpts.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `identity/v3/projects.UpdateOpts.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `identity/v3/projects.UpdateOpts.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `identity/v3/projects.Project.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `identity/v3/projects.Options.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951)
* Added `imageservice/v2/images.Image.OpenStackImageImportMethods` [GH-1962](https://github.com/gophercloud/gophercloud/pull/1962)
* Added `imageservice/v2/images.Image.OpenStackImageStoreIDs` [GH-1962](https://github.com/gophercloud/gophercloud/pull/1962)
BUG FIXES
* Changed`identity/v3/extensions/trusts.Trust.RemainingUses` from `bool` to `int` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939)
* Changed `identity/v3/applicationcredentials.CreateOpts.ExpiresAt` from `string` to `*time.Time` [GH-1937](https://github.com/gophercloud/gophercloud/pull/1937)
* Fixed issue with unmarshalling/decoding slices of composed structs [GH-1964](https://github.com/gophercloud/gophercloud/pull/1964)
## 0.10.0 (April 12, 2020)
UPGRADE NOTES
* The various `IDFromName` convenience functions have been moved to https://github.com/gophercloud/utils [GH-1897](https://github.com/gophercloud/gophercloud/pull/1897)
* `sharedfilesystems/v2/shares.GetExportLocations` was renamed to `sharedfilesystems/v2/shares.ListExportLocations` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932)
IMPROVEMENTS
* Added `blockstorage/extensions/volumeactions.SetBootable` [GH-1891](https://github.com/gophercloud/gophercloud/pull/1891)
* Added `blockstorage/extensions/backups.Export` [GH-1894](https://github.com/gophercloud/gophercloud/pull/1894)
* Added `blockstorage/extensions/backups.Import` [GH-1894](https://github.com/gophercloud/gophercloud/pull/1894)
* Added `placement/v1/resourceproviders.GetTraits` [GH-1899](https://github.com/gophercloud/gophercloud/pull/1899)
* Added the ability to authenticate with Amazon EC2 Credentials [GH-1900](https://github.com/gophercloud/gophercloud/pull/1900)
* Added ability to list Nova services by binary and host [GH-1904](https://github.com/gophercloud/gophercloud/pull/1904)
* Added `compute/v2/extensions/services.Update` [GH-1902](https://github.com/gophercloud/gophercloud/pull/1902)
* Added system scope to v3 authentication [GH-1908](https://github.com/gophercloud/gophercloud/pull/1908)
* Added `identity/v3/extensions/ec2tokens.ValidateS3Token` [GH-1906](https://github.com/gophercloud/gophercloud/pull/1906)
* Added `containerinfra/v1/clusters.Cluster.HealthStatus` [GH-1910](https://github.com/gophercloud/gophercloud/pull/1910)
* Added `containerinfra/v1/clusters.Cluster.HealthStatusReason` [GH-1910](https://github.com/gophercloud/gophercloud/pull/1910)
* Added `loadbalancer/v2/amphorae.Failover` [GH-1912](https://github.com/gophercloud/gophercloud/pull/1912)
* Added `identity/v3/extensions/ec2credentials.List` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916)
* Added `identity/v3/extensions/ec2credentials.Get` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916)
* Added `identity/v3/extensions/ec2credentials.Create` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916)
* Added `identity/v3/extensions/ec2credentials.Delete` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916)
* Added `ErrUnexpectedResponseCode.ResponseHeader` [GH-1919](https://github.com/gophercloud/gophercloud/pull/1919)
* Added support for TOTP authentication [GH-1922](https://github.com/gophercloud/gophercloud/pull/1922)
* `sharedfilesystems/v2/shares.GetExportLocations` was renamed to `sharedfilesystems/v2/shares.ListExportLocations` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932)
* Added `sharedfilesystems/v2/shares.GetExportLocation` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932)
* Added `sharedfilesystems/v2/shares.Revert` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931)
* Added `sharedfilesystems/v2/shares.ResetStatus` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931)
* Added `sharedfilesystems/v2/shares.ForceDelete` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931)
* Added `sharedfilesystems/v2/shares.Unmanage` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931)
* Added `blockstorage/v3/attachments.Create` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
* Added `blockstorage/v3/attachments.List` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
* Added `blockstorage/v3/attachments.Get` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
* Added `blockstorage/v3/attachments.Update` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
* Added `blockstorage/v3/attachments.Delete` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
* Added `blockstorage/v3/attachments.Complete` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934)
BUG FIXES
* Fixed issue with Orchestration `get_file` only being able to read JSON and YAML files [GH-1915](https://github.com/gophercloud/gophercloud/pull/1915)
## 0.9.0 (March 10, 2020)
UPGRADE NOTES
* The way we implement new API result fields added by microversions has changed. Previously, we would declare a dedicated `ExtractFoo` function in a file called `microversions.go`. Now, we are declaring those fields inline of the original result struct as a pointer. [GH-1854](https://github.com/gophercloud/gophercloud/pull/1854)
* `compute/v2/servers.CreateOpts.Networks` has changed from `[]Network` to `interface{}` in order to support creating servers that have no networks. [GH-1884](https://github.com/gophercloud/gophercloud/pull/1884)
IMPROVEMENTS
* Added `compute/v2/extensions/instanceactions.List` [GH-1848](https://github.com/gophercloud/gophercloud/pull/1848)
* Added `compute/v2/extensions/instanceactions.Get` [GH-1848](https://github.com/gophercloud/gophercloud/pull/1848)
* Added `networking/v2/ports.List.FixedIPs` [GH-1849](https://github.com/gophercloud/gophercloud/pull/1849)
* Added `identity/v3/extensions/trusts.List` [GH-1855](https://github.com/gophercloud/gophercloud/pull/1855)
* Added `identity/v3/extensions/trusts.Get` [GH-1855](https://github.com/gophercloud/gophercloud/pull/1855)
* Added `identity/v3/extensions/trusts.Trust.ExpiresAt` [GH-1857](https://github.com/gophercloud/gophercloud/pull/1857)
* Added `identity/v3/extensions/trusts.Trust.DeletedAt` [GH-1857](https://github.com/gophercloud/gophercloud/pull/1857)
* Added `compute/v2/extensions/instanceactions.InstanceActionDetail` [GH-1851](https://github.com/gophercloud/gophercloud/pull/1851)
* Added `compute/v2/extensions/instanceactions.Event` [GH-1851](https://github.com/gophercloud/gophercloud/pull/1851)
* Added `compute/v2/extensions/instanceactions.ListOpts` [GH-1858](https://github.com/gophercloud/gophercloud/pull/1858)
* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey` [GH-1864](https://github.com/gophercloud/gophercloud/pull/1864)
* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey2` [GH-1864](https://github.com/gophercloud/gophercloud/pull/1864)
* Added `placement/v1/resourceproviders.GetUsages` [GH-1862](https://github.com/gophercloud/gophercloud/pull/1862)
* Added `placement/v1/resourceproviders.GetInventories` [GH-1862](https://github.com/gophercloud/gophercloud/pull/1862)
* Added `imageservice/v2/images.ReplaceImageMinRam` [GH-1867](https://github.com/gophercloud/gophercloud/pull/1867)
* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey` [GH-1865](https://github.com/gophercloud/gophercloud/pull/1865)
* Added `objectstorage/v1/containers.CreateOpts.TempURLKey2` [GH-1865](https://github.com/gophercloud/gophercloud/pull/1865)
* Added `blockstorage/extensions/volumetransfers.List` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869)
* Added `blockstorage/extensions/volumetransfers.Create` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869)
* Added `blockstorage/extensions/volumetransfers.Accept` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869)
* Added `blockstorage/extensions/volumetransfers.Get` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869)
* Added `blockstorage/extensions/volumetransfers.Delete` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869)
* Added `blockstorage/extensions/backups.RestoreFromBackup` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871)
* Added `blockstorage/v3/volumes.CreateOpts.BackupID` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871)
* Added `blockstorage/v3/volumes.Volume.BackupID` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871)
* Added `identity/v3/projects.ListOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.ListOpts.TagsAny` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.ListOpts.NotTags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.ListOpts.NotTagsAny` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.CreateOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.UpdateOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Added `identity/v3/projects.Project.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882)
* Changed `compute/v2/servers.CreateOpts.Networks` from `[]Network` to `interface{}` to support creating servers with no networks. [GH-1884](https://github.com/gophercloud/gophercloud/pull/1884)
BUG FIXES
* Added support for `int64` headers, which were previously being silently dropped [GH-1860](https://github.com/gophercloud/gophercloud/pull/1860)
* Allow image properties with empty values [GH-1875](https://github.com/gophercloud/gophercloud/pull/1875)
* Fixed `compute/v2/extensions/extendedserverattributes.ServerAttributesExt.Userdata` JSON tag [GH-1881](https://github.com/gophercloud/gophercloud/pull/1881)
## 0.8.0 (February 8, 2020)
UPGRADE NOTES
* The behavior of `keymanager/v1/acls.SetOpts` has changed. Instead of a struct, it is now `[]SetOpt`. See [GH-1816](https://github.com/gophercloud/gophercloud/pull/1816) for implementation details.
IMPROVEMENTS
* The result of `containerinfra/v1/clusters.Resize` now returns only the UUID when calling `Extract`. This is a backwards-breaking change from the previous struct that was returned [GH-1649](https://github.com/gophercloud/gophercloud/pull/1649)
* Added `compute/v2/extensions/shelveunshelve.Shelve` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799)
* Added `compute/v2/extensions/shelveunshelve.ShelveOffload` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799)
* Added `compute/v2/extensions/shelveunshelve.Unshelve` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799)
* Added `containerinfra/v1/nodegroups.Get` [GH-1774](https://github.com/gophercloud/gophercloud/pull/1774)
* Added `containerinfra/v1/nodegroups.List` [GH-1774](https://github.com/gophercloud/gophercloud/pull/1774)
* Added `orchestration/v1/resourcetypes.List` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806)
* Added `orchestration/v1/resourcetypes.GetSchema` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806)
* Added `orchestration/v1/resourcetypes.GenerateTemplate` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806)
* Added `keymanager/v1/acls.SetOpt` and changed `keymanager/v1/acls.SetOpts` to `[]SetOpt` [GH-1816](https://github.com/gophercloud/gophercloud/pull/1816)
* Added `blockstorage/apiversions.List` [GH-458](https://github.com/gophercloud/gophercloud/pull/458)
* Added `blockstorage/apiversions.Get` [GH-458](https://github.com/gophercloud/gophercloud/pull/458)
* Added `StatusCodeError` interface and `GetStatusCode` convenience method [GH-1820](https://github.com/gophercloud/gophercloud/pull/1820)
* Added pagination support to `compute/v2/extensions/usage.SingleTenant` [GH-1819](https://github.com/gophercloud/gophercloud/pull/1819)
* Added pagination support to `compute/v2/extensions/usage.AllTenants` [GH-1819](https://github.com/gophercloud/gophercloud/pull/1819)
* Added `placement/v1/resourceproviders.List` [GH-1815](https://github.com/gophercloud/gophercloud/pull/1815)
* Allow `CreateMemberOptsBuilder` to be passed in `loadbalancer/v2/pools.Create` [GH-1822](https://github.com/gophercloud/gophercloud/pull/1822)
* Added `Backup` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824)
* Added `MonitorAddress` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824)
* Added `MonitorPort` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824)
* Changed `Impersonation` to a non-required field in `identity/v3/extensions/trusts.CreateOpts` [GH-1818](https://github.com/gophercloud/gophercloud/pull/1818)
* Added `InsertHeaders` to `loadbalancer/v2/listeners.UpdateOpts` [GH-1835]
* Added `NUMATopology` to `baremetalintrospection/v1/introspection.Data` [GH-1842](https://github.com/gophercloud/gophercloud/pull/1842)
* Added `placement/v1/resourceproviders.Create` [GH-1841](https://github.com/gophercloud/gophercloud/pull/1841)
* Added `blockstorage/extensions/volumeactions.UploadImageOpts.Visibility` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873)
* Added `blockstorage/extensions/volumeactions.UploadImageOpts.Protected` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873)
* Added `blockstorage/extensions/volumeactions.VolumeImage.Visibility` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873)
* Added `blockstorage/extensions/volumeactions.VolumeImage.Protected` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873)
BUG FIXES
* Changed `sort_key` to `sort_keys` in ` workflow/v2/crontriggers.ListOpts` [GH-1809](https://github.com/gophercloud/gophercloud/pull/1809)
* Allow `blockstorage/extensions/schedulerstats.Capabilities.MaxOverSubscriptionRatio` to accept both string and int/float responses [GH-1817](https://github.com/gophercloud/gophercloud/pull/1817)
* Fixed bug in `NewLoadBalancerV2` for situations when the LBaaS service was advertised without a `/v2.0` endpoint [GH-1829](https://github.com/gophercloud/gophercloud/pull/1829)
* Fixed JSON tags in `baremetal/v1/ports.UpdateOperation` [GH-1840](https://github.com/gophercloud/gophercloud/pull/1840)
* Fixed JSON tags in `networking/v2/extensions/lbaas/vips.commonResult.Extract()` [GH-1840](https://github.com/gophercloud/gophercloud/pull/1840)
## 0.7.0 (December 3, 2019)
IMPROVEMENTS
* Allow a token to be used directly for authentication instead of generating a new token based on a given token [GH-1752](https://github.com/gophercloud/gophercloud/pull/1752)
* Moved `tags.ServerTagsExt` to servers.TagsExt` [GH-1760](https://github.com/gophercloud/gophercloud/pull/1760)
* Added `tags`, `tags-any`, `not-tags`, and `not-tags-any` to `compute/v2/servers.ListOpts` [GH-1759](https://github.com/gophercloud/gophercloud/pull/1759)
* Added `AccessRule` to `identity/v3/applicationcredentials` [GH-1758](https://github.com/gophercloud/gophercloud/pull/1758)
* Gophercloud no longer returns an error when multiple endpoints are found. Instead, it will choose the first endpoint and discard the others [GH-1766](https://github.com/gophercloud/gophercloud/pull/1766)
* Added `networking/v2/extensions/fwaas_v2/rules.Create` [GH-1768](https://github.com/gophercloud/gophercloud/pull/1768)
* Added `networking/v2/extensions/fwaas_v2/rules.Delete` [GH-1771](https://github.com/gophercloud/gophercloud/pull/1771)
* Added `loadbalancer/v2/providers.List` [GH-1765](https://github.com/gophercloud/gophercloud/pull/1765)
* Added `networking/v2/extensions/fwaas_v2/rules.Get` [GH-1772](https://github.com/gophercloud/gophercloud/pull/1772)
* Added `networking/v2/extensions/fwaas_v2/rules.Update` [GH-1776](https://github.com/gophercloud/gophercloud/pull/1776)
* Added `networking/v2/extensions/fwaas_v2/rules.List` [GH-1783](https://github.com/gophercloud/gophercloud/pull/1783)
* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.CreateOpts` [GH-1785](https://github.com/gophercloud/gophercloud/pull/1785)
* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.UpdateOpts` [GH-1786](https://github.com/gophercloud/gophercloud/pull/1786)
* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.Monitor` [GH-1787](https://github.com/gophercloud/gophercloud/pull/1787)
* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.ListOpts` [GH-1788](https://github.com/gophercloud/gophercloud/pull/1788)
* Updated `go.mod` dependencies, specifically to account for CVE-2019-11840 with `golang.org/x/crypto` [GH-1793](https://github.com/gophercloud/gophercloud/pull/1788)
## 0.6.0 (October 17, 2019)
UPGRADE NOTES
* The way reauthentication works has been refactored. This should not cause a problem, but please report bugs if it does. See [GH-1746](https://github.com/gophercloud/gophercloud/pull/1746) for more information.
IMPROVEMENTS
* Added `networking/v2/extensions/quotas.Get` [GH-1742](https://github.com/gophercloud/gophercloud/pull/1742)
* Added `networking/v2/extensions/quotas.Update` [GH-1747](https://github.com/gophercloud/gophercloud/pull/1747)
* Refactored the reauthentication implementation to use goroutines and added a check to prevent an infinite loop in certain situations. [GH-1746](https://github.com/gophercloud/gophercloud/pull/1746)
BUG FIXES
* Changed `Flavor` to `FlavorID` in `loadbalancer/v2/loadbalancers` [GH-1744](https://github.com/gophercloud/gophercloud/pull/1744)
* Changed `Flavor` to `FlavorID` in `networking/v2/extensions/lbaas_v2/loadbalancers` [GH-1744](https://github.com/gophercloud/gophercloud/pull/1744)
* The `go-yaml` dependency was updated to `v2.2.4` to fix possible DDOS vulnerabilities [GH-1751](https://github.com/gophercloud/gophercloud/pull/1751)
## 0.5.0 (October 13, 2019)
IMPROVEMENTS
* Added `VolumeType` to `compute/v2/extensions/bootfromvolume.BlockDevice`[GH-1690](https://github.com/gophercloud/gophercloud/pull/1690)
* Added `networking/v2/extensions/layer3/portforwarding.List` [GH-1688](https://github.com/gophercloud/gophercloud/pull/1688)
* Added `networking/v2/extensions/layer3/portforwarding.Get` [GH-1698](https://github.com/gophercloud/gophercloud/pull/1696)
* Added `compute/v2/extensions/tags.ReplaceAll` [GH-1696](https://github.com/gophercloud/gophercloud/pull/1696)
* Added `compute/v2/extensions/tags.Add` [GH-1696](https://github.com/gophercloud/gophercloud/pull/1696)
* Added `networking/v2/extensions/layer3/portforwarding.Update` [GH-1703](https://github.com/gophercloud/gophercloud/pull/1703)
* Added `ExtractDomain` method to token results in `identity/v3/tokens` [GH-1712](https://github.com/gophercloud/gophercloud/pull/1712)
* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.CreateOpts` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710)
* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.UpdateOpts` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710)
* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.Listener` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710)
* Added `compute/v2/extensions/tags.Add` [GH-1695](https://github.com/gophercloud/gophercloud/pull/1695)
* Added `compute/v2/extensions/tags.ReplaceAll` [GH-1694](https://github.com/gophercloud/gophercloud/pull/1694)
* Added `compute/v2/extensions/tags.Delete` [GH-1699](https://github.com/gophercloud/gophercloud/pull/1699)
* Added `compute/v2/extensions/tags.DeleteAll` [GH-1700](https://github.com/gophercloud/gophercloud/pull/1700)
* Added `ImageStatusImporting` as an image status [GH-1725](https://github.com/gophercloud/gophercloud/pull/1725)
* Added `ByPath` to `baremetalintrospection/v1/introspection.RootDiskType` [GH-1730](https://github.com/gophercloud/gophercloud/pull/1730)
* Added `AttachedVolumes` to `compute/v2/servers.Server` [GH-1732](https://github.com/gophercloud/gophercloud/pull/1732)
* Enable unmarshaling server tags to a `compute/v2/servers.Server` struct [GH-1734]
* Allow setting an empty members list in `loadbalancer/v2/pools.BatchUpdateMembers` [GH-1736](https://github.com/gophercloud/gophercloud/pull/1736)
* Allow unsetting members' subnet ID and name in `loadbalancer/v2/pools.BatchUpdateMemberOpts` [GH-1738](https://github.com/gophercloud/gophercloud/pull/1738)
BUG FIXES
* Changed struct type for options in `networking/v2/extensions/lbaas_v2/listeners` to `UpdateOptsBuilder` interface instead of specific UpdateOpts type [GH-1705](https://github.com/gophercloud/gophercloud/pull/1705)
* Changed struct type for options in `networking/v2/extensions/lbaas_v2/loadbalancers` to `UpdateOptsBuilder` interface instead of specific UpdateOpts type [GH-1706](https://github.com/gophercloud/gophercloud/pull/1706)
* Fixed issue with `blockstorage/v1/volumes.Create` where the response was expected to be 202 [GH-1720](https://github.com/gophercloud/gophercloud/pull/1720)
* Changed `DefaultTlsContainerRef` from `string` to `*string` in `loadbalancer/v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723)
* Changed `SniContainerRefs` from `[]string{}` to `*[]string{}` in `loadbalancer/v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723)
* Changed `DefaultTlsContainerRef` from `string` to `*string` in `networking/v2/extensions/lbaas_v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723)
* Changed `SniContainerRefs` from `[]string{}` to `*[]string{}` in `networking/v2/extensions/lbaas_v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723)
## 0.4.0 (September 3, 2019)
IMPROVEMENTS
* Added `blockstorage/extensions/quotasets.results.QuotaSet.Groups` [GH-1668](https://github.com/gophercloud/gophercloud/pull/1668)
* Added `blockstorage/extensions/quotasets.results.QuotaUsageSet.Groups` [GH-1668](https://github.com/gophercloud/gophercloud/pull/1668)
* Added `containerinfra/v1/clusters.CreateOpts.FixedNetwork` [GH-1674](https://github.com/gophercloud/gophercloud/pull/1674)
* Added `containerinfra/v1/clusters.CreateOpts.FixedSubnet` [GH-1676](https://github.com/gophercloud/gophercloud/pull/1676)
* Added `containerinfra/v1/clusters.CreateOpts.FloatingIPEnabled` [GH-1677](https://github.com/gophercloud/gophercloud/pull/1677)
* Added `CreatedAt` and `UpdatedAt` to `loadbalancers/v2/loadbalancers.LoadBalancer` [GH-1681](https://github.com/gophercloud/gophercloud/pull/1681)
* Added `networking/v2/extensions/layer3/portforwarding.Create` [GH-1651](https://github.com/gophercloud/gophercloud/pull/1651)
* Added `networking/v2/extensions/agents.ListDHCPNetworks` [GH-1686](https://github.com/gophercloud/gophercloud/pull/1686)
* Added `networking/v2/extensions/layer3/portforwarding.Delete` [GH-1652](https://github.com/gophercloud/gophercloud/pull/1652)
* Added `compute/v2/extensions/tags.List` [GH-1679](https://github.com/gophercloud/gophercloud/pull/1679)
* Added `compute/v2/extensions/tags.Check` [GH-1679](https://github.com/gophercloud/gophercloud/pull/1679)
BUG FIXES
* Changed `identity/v3/endpoints.ListOpts.RegionID` from `int` to `string` [GH-1664](https://github.com/gophercloud/gophercloud/pull/1664)
* Fixed issue where older time formats in some networking APIs/resources were unable to be parsed [GH-1671](https://github.com/gophercloud/gophercloud/pull/1664)
* Changed `SATA`, `SCSI`, and `SAS` types to `InterfaceType` in `baremetal/v1/nodes` [GH-1683]
## 0.3.0 (July 31, 2019)
IMPROVEMENTS
* Added `baremetal/apiversions.List` [GH-1577](https://github.com/gophercloud/gophercloud/pull/1577)
* Added `baremetal/apiversions.Get` [GH-1577](https://github.com/gophercloud/gophercloud/pull/1577)
* Added `compute/v2/extensions/servergroups.CreateOpts.Policy` [GH-1636](https://github.com/gophercloud/gophercloud/pull/1636)
* Added `identity/v3/extensions/trusts.Create` [GH-1644](https://github.com/gophercloud/gophercloud/pull/1644)
* Added `identity/v3/extensions/trusts.Delete` [GH-1644](https://github.com/gophercloud/gophercloud/pull/1644)
* Added `CreatedAt` and `UpdatedAt` to `networking/v2/extensions/layer3/floatingips.FloatingIP` [GH-1647](https://github.com/gophercloud/gophercloud/issues/1646)
* Added `CreatedAt` and `UpdatedAt` to `networking/v2/extensions/security/groups.SecGroup` [GH-1654](https://github.com/gophercloud/gophercloud/issues/1654)
* Added `CreatedAt` and `UpdatedAt` to `networking/v2/networks.Network` [GH-1657](https://github.com/gophercloud/gophercloud/issues/1657)
* Added `keymanager/v1/containers.CreateSecretRef` [GH-1659](https://github.com/gophercloud/gophercloud/issues/1659)
* Added `keymanager/v1/containers.DeleteSecretRef` [GH-1659](https://github.com/gophercloud/gophercloud/issues/1659)
* Added `sharedfilesystems/v2/shares.GetMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656)
* Added `sharedfilesystems/v2/shares.GetMetadatum` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656)
* Added `sharedfilesystems/v2/shares.SetMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656)
* Added `sharedfilesystems/v2/shares.UpdateMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656)
* Added `sharedfilesystems/v2/shares.DeleteMetadatum` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656)
* Added `sharedfilesystems/v2/sharetypes.IDFromName` [GH-1662](https://github.com/gophercloud/gophercloud/issues/1662)
BUG FIXES
* Changed `baremetal/v1/nodes.CleanStep.Args` from `map[string]string` to `map[string]interface{}` [GH-1638](https://github.com/gophercloud/gophercloud/pull/1638)
* Removed `URLPath` and `ExpectedCodes` from `loadbalancer/v2/monitors.ToMonitorCreateMap` since Octavia now provides default values when these fields are not specified [GH-1640](https://github.com/gophercloud/gophercloud/pull/1540)
## 0.2.0 (June 17, 2019)

View File

@ -45,6 +45,9 @@ type AuthOptions struct {
Password string `json:"password,omitempty"`
// Passcode is used in TOTP authentication method
Passcode string `json:"passcode,omitempty"`
// At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional.
DomainID string `json:"-"`
@ -98,6 +101,7 @@ type AuthScope struct {
ProjectName string
DomainID string
DomainName string
System bool
}
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
@ -133,6 +137,8 @@ func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
return map[string]interface{}{"auth": authMap}, nil
}
// ToTokenV3CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v3 tokens package
func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
type domainReq struct {
ID *string `json:"id,omitempty"`
@ -148,7 +154,8 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
type userReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Password string `json:"password,omitempty"`
Password *string `json:"password,omitempty"`
Passcode *string `json:"passcode,omitempty"`
Domain *domainReq `json:"domain,omitempty"`
}
@ -167,11 +174,16 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
Secret *string `json:"secret,omitempty"`
}
type totpReq struct {
User *userReq `json:"user,omitempty"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
TOTP *totpReq `json:"totp,omitempty"`
}
type authReq struct {
@ -186,7 +198,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// if insufficient or incompatible information is present.
var req request
if opts.Password == "" {
if opts.Password == "" && opts.Passcode == "" {
if opts.TokenID != "" {
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters.
@ -274,7 +286,14 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
}
} else {
// Password authentication.
req.Auth.Identity.Methods = []string{"password"}
if opts.Password != "" {
req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password")
}
// TOTP authentication.
if opts.Passcode != "" {
req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "totp")
}
// At least one of Username and UserID must be specified.
if opts.Username == "" && opts.UserID == "" {
@ -298,23 +317,46 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
}
// Configure the request for Username and Password authentication with a DomainID.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: opts.Password,
Domain: &domainReq{ID: &opts.DomainID},
},
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: &opts.Password,
Domain: &domainReq{ID: &opts.DomainID},
},
}
}
if opts.Passcode != "" {
req.Auth.Identity.TOTP = &totpReq{
User: &userReq{
Name: &opts.Username,
Passcode: &opts.Passcode,
Domain: &domainReq{ID: &opts.DomainID},
},
}
}
}
if opts.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: opts.Password,
Domain: &domainReq{Name: &opts.DomainName},
},
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &opts.Username,
Password: &opts.Password,
Domain: &domainReq{Name: &opts.DomainName},
},
}
}
if opts.Passcode != "" {
req.Auth.Identity.TOTP = &totpReq{
User: &userReq{
Name: &opts.Username,
Passcode: &opts.Passcode,
Domain: &domainReq{Name: &opts.DomainName},
},
}
}
}
}
@ -329,8 +371,22 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
}
// Configure the request for UserID and Password authentication.
req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &opts.UserID, Password: opts.Password},
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{
User: userReq{
ID: &opts.UserID,
Password: &opts.Password,
},
}
}
if opts.Passcode != "" {
req.Auth.Identity.TOTP = &totpReq{
User: &userReq{
ID: &opts.UserID,
Passcode: &opts.Passcode,
},
}
}
}
}
@ -347,6 +403,8 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
return b, nil
}
// ToTokenV3ScopeMap builds a scope from AuthOptions and satisfies interface in
// the v3 tokens package.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
// For backwards compatibility.
// If AuthOptions.Scope was not set, try to determine it.
@ -364,6 +422,14 @@ func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
}
}
if opts.Scope.System {
return map[string]interface{}{
"system": map[string]interface{}{
"all": true,
},
}, nil
}
if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied.
@ -433,5 +499,16 @@ func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
}
func (opts AuthOptions) CanReauth() bool {
if opts.Passcode != "" {
// cannot reauth using TOTP passcode
return false
}
return opts.AllowReauth
}
// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v3 tokens package.
func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) {
return nil, nil
}

View File

@ -2,6 +2,7 @@ package gophercloud
import (
"fmt"
"net/http"
"strings"
)
@ -77,11 +78,12 @@ func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string {
// those listed in OkCodes is encountered.
type ErrUnexpectedResponseCode struct {
BaseError
URL string
Method string
Expected []int
Actual int
Body []byte
URL string
Method string
Expected []int
Actual int
Body []byte
ResponseHeader http.Header
}
func (e ErrUnexpectedResponseCode) Error() string {
@ -92,6 +94,23 @@ func (e ErrUnexpectedResponseCode) Error() string {
return e.choseErrString()
}
// GetStatusCode returns the actual status code of the error.
func (e ErrUnexpectedResponseCode) GetStatusCode() int {
return e.Actual
}
// StatusCodeError is a convenience interface to easily allow access to the
// status code field of the various ErrDefault* types.
//
// By using this interface, you only have to make a single type cast of
// the returned error to err.(StatusCodeError) and then call GetStatusCode()
// instead of having a large switch statement checking for each of the
// ErrDefault* types.
type StatusCodeError interface {
Error() string
GetStatusCode() int
}
// ErrDefault400 is the default error type returned on a 400 HTTP response code.
type ErrDefault400 struct {
ErrUnexpectedResponseCode

View File

@ -1,7 +1,13 @@
module github.com/gophercloud/gophercloud
require (
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect
gopkg.in/yaml.v2 v2.2.2
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect
golang.org/x/text v0.3.2 // indirect
golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371 // indirect
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.7
)

View File

@ -1,8 +1,26 @@
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU=
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -38,6 +38,7 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
username := os.Getenv("OS_USERNAME")
userID := os.Getenv("OS_USERID")
password := os.Getenv("OS_PASSWORD")
passcode := os.Getenv("OS_PASSCODE")
tenantID := os.Getenv("OS_TENANT_ID")
tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID")
@ -73,8 +74,9 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
}
}
if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
if password == "" && passcode == "" && applicationCredentialID == "" && applicationCredentialName == "" {
err := gophercloud.ErrMissingEnvironmentVariable{
// silently ignore TOTP passcode warning, since it is not a common auth method
EnvironmentVariable: "OS_PASSWORD",
}
return nilOptions, err
@ -112,6 +114,7 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
UserID: userID,
Username: username,
Password: password,
Passcode: passcode,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,

View File

@ -82,5 +82,16 @@ Example of Initializing a Volume Connection
if err != nil {
panic(err)
}
Example of Setting a Volume's Bootable status
options := volumeactions.BootableOpts{
Bootable: true,
}
err := volumeactions.SetBootable(client, volume.ID, options).ExtractErr()
if err != nil {
panic(err)
}
*/
package volumeactions

View File

@ -47,18 +47,20 @@ func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// BeginDetach will mark the volume as detaching.
func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) {
b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -87,27 +89,30 @@ func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Reserve will reserve a volume based on volume ID.
func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
b := map[string]interface{}{"os-reserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Unreserve will unreserve a volume based on volume ID.
func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) {
b := map[string]interface{}{"os-unreserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -145,9 +150,10 @@ func InitializeConnection(client *gophercloud.ServiceClient, id string, opts Ini
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -183,9 +189,10 @@ func TerminateConnection(client *gophercloud.ServiceClient, id string, opts Term
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -216,9 +223,10 @@ func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOpt
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -241,6 +249,14 @@ type UploadImageOpts struct {
// Force image creation, usable if volume attached to instance.
Force bool `json:"force,omitempty"`
// Visibility defines who can see/use the image.
// supported since 3.1 microversion
Visibility string `json:"visibility,omitempty"`
// whether the image is not deletable.
// supported since 3.1 microversion
Protected bool `json:"protected,omitempty"`
}
// ToVolumeUploadImageMap assembles a request body based on the contents of a
@ -256,15 +272,17 @@ func UploadImage(client *gophercloud.ServiceClient, id string, opts UploadImageO
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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)
resp, err := client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -293,8 +311,35 @@ func SetImageMetadata(client *gophercloud.ServiceClient, id string, opts ImageMe
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// BootableOpts contains options for setting bootable status to a volume.
type BootableOpts struct {
// Enables or disables the bootable attribute. You can boot an instance from a bootable volume.
Bootable bool `json:"bootable"`
}
// ToBootableMap assembles a request body based on the contents of a
// BootableOpts.
func (opts BootableOpts) ToBootableMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-set_bootable")
}
// SetBootable will set bootable status on a volume based on the values in BootableOpts
func SetBootable(client *gophercloud.ServiceClient, id string, opts BootableOpts) (r SetBootableResult) {
b, err := opts.ToBootableMap()
if err != nil {
r.Err = err
return
}
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -35,6 +35,12 @@ type SetImageMetadataResult struct {
gophercloud.ErrResult
}
// SetBootableResult contains the response body and error from a SetBootable
// request.
type SetBootableResult struct {
gophercloud.ErrResult
}
// ReserveResult contains the response body and error from a Reserve request.
type ReserveResult struct {
gophercloud.ErrResult
@ -157,6 +163,14 @@ type VolumeImage struct {
// Current status of the volume.
Status string `json:"status"`
// Visibility defines who can see/use the image.
// supported since 3.1 microversion
Visibility string `json:"visibility"`
// whether the image is not deletable.
// supported since 3.1 microversion
Protected bool `json:"protected"`
// The date when this volume was last updated.
UpdatedAt time.Time `json:"-"`

View File

@ -1,5 +1,23 @@
// Package volumes provides information and interaction with volumes in the
// OpenStack Block Storage service. A volume is a detachable block storage
// device, akin to a USB hard drive. It can only be attached to one instance at
// a time.
/*
Package volumes provides information and interaction with volumes in the
OpenStack Block Storage service. A volume is a detachable block storage
device, akin to a USB hard drive. It can only be attached to one instance at
a time.
Example to create a Volume from a Backup
backupID := "20c792f0-bb03-434f-b653-06ef238e337e"
options := volumes.CreateOpts{
Name: "vol-001",
BackupID: &backupID,
}
client.Microversion = "3.47"
volume, err := volumes.Create(client, options).Extract()
if err != nil {
panic(err)
}
fmt.Println(volume)
*/
package volumes

View File

@ -16,7 +16,7 @@ type CreateOptsBuilder interface {
// see the Volume object.
type CreateOpts struct {
// The size of the volume, in GB
Size int `json:"size" required:"true"`
Size int `json:"size,omitempty"`
// The availability zone
AvailabilityZone string `json:"availability_zone,omitempty"`
// ConsistencyGroupID is the ID of a consistency group
@ -36,6 +36,9 @@ type CreateOpts struct {
// The ID of the image from which you want to create the volume.
// Required to create a bootable volume.
ImageID string `json:"imageRef,omitempty"`
// Specifies the backup ID, from which you want to create the volume.
// Create a volume from a backup is supported since 3.47 microversion
BackupID string `json:"backup_id,omitempty"`
// The associated volume type
VolumeType string `json:"volume_type,omitempty"`
// Multiattach denotes if the volume is multi-attach capable.
@ -57,9 +60,10 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -93,14 +97,16 @@ func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder
}
url += query
}
_, r.Err = client.Delete(url, nil)
resp, err := client.Delete(url, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -194,44 +200,9 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// IDFromName is a convienience function that returns a server's ID given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
listOpts := ListOpts{
Name: name,
}
pages, err := List(client, listOpts).AllPages()
if err != nil {
return "", err
}
all, err := ExtractVolumes(pages)
if err != nil {
return "", err
}
for _, s := range all {
if s.Name == name {
count++
id = s.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "volume"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "volume"}
}
}

View File

@ -63,6 +63,9 @@ type Volume struct {
SnapshotID string `json:"snapshot_id"`
// The ID of another block storage volume from which the current volume was created
SourceVolID string `json:"source_volid"`
// The backup ID, from which the volume was restored
// This field is supported since 3.47 microversion
BackupID *string `json:"backup_id"`
// Arbitrary key-value pairs defined by the user.
Metadata map[string]string `json:"metadata"`
// UserID is the id of the user who created the volume.

View File

@ -3,9 +3,12 @@ package openstack
import (
"fmt"
"reflect"
"strings"
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens"
"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/openstack/utils"
)
@ -67,7 +70,7 @@ Example:
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
client, err := openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
*/
@ -187,16 +190,61 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
v3Client.Endpoint = endpoint
}
result := tokens3.Create(v3Client, opts)
var catalog *tokens3.ServiceCatalog
err = client.SetTokenAndAuthResult(result)
if err != nil {
return err
var tokenID string
// passthroughToken allows to passthrough the token without a scope
var passthroughToken bool
switch v := opts.(type) {
case *gophercloud.AuthOptions:
tokenID = v.TokenID
passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{})
case *tokens3.AuthOptions:
tokenID = v.TokenID
passthroughToken = (v.Scope == tokens3.Scope{})
}
catalog, err := result.ExtractServiceCatalog()
if err != nil {
return err
if tokenID != "" && passthroughToken {
// passing through the token ID without requesting a new scope
if opts.CanReauth() {
return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set")
}
v3Client.SetToken(tokenID)
result := tokens3.Get(v3Client, tokenID)
if result.Err != nil {
return result.Err
}
err = client.SetTokenAndAuthResult(result)
if err != nil {
return err
}
catalog, err = result.ExtractServiceCatalog()
if err != nil {
return err
}
} else {
var result tokens3.CreateResult
switch opts.(type) {
case *ec2tokens.AuthOptions:
result = ec2tokens.Create(v3Client, opts)
case *oauth1.AuthOptions:
result = oauth1.Create(v3Client, opts)
default:
result = tokens3.Create(v3Client, opts)
}
err = client.SetTokenAndAuthResult(result)
if err != nil {
return err
}
catalog, err = result.ExtractServiceCatalog()
if err != nil {
return err
}
}
if opts.CanReauth() {
@ -217,6 +265,14 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
o := *ot
o.AllowReauth = false
tao = &o
case *ec2tokens.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
case *oauth1.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
default:
tao = opts
}
@ -395,7 +451,11 @@ func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
// 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/"
// Fixes edge case having an OpenStack lb endpoint with trailing version number.
endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1)
sc.ResourceBase = endpoint + "v2.0/"
return sc, err
}
@ -436,3 +496,8 @@ func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.Endp
func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "workflowv2")
}
// NewPlacementV1 creates a ServiceClient that may be used with the placement package.
func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "placement")
}

View File

@ -0,0 +1,52 @@
/*
Package extensions provides information and interaction with the different
extensions available for an OpenStack service.
The purpose of OpenStack API extensions is to:
- Introduce new features in the API without requiring a version change.
- Introduce vendor-specific niche functionality.
- Act as a proving ground for experimental functionalities that might be
included in a future version of the API.
Extensions usually have tags that prevent conflicts with other extensions that
define attributes or resources with the same names, and with core resources and
attributes. Because an extension might not be supported by all plug-ins, its
availability varies with deployments and the specific plug-in.
The results of this package vary depending on the type of Service Client used.
In the following examples, note how the only difference is the creation of the
Service Client.
Example of Retrieving Compute Extensions
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
computeClient, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
allPages, err := extensions.List(computeClient).Allpages()
allExtensions, err := extensions.ExtractExtensions(allPages)
for _, extension := range allExtensions{
fmt.Println("%+v\n", extension)
}
Example of Retrieving Network Extensions
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
networkClient, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
allPages, err := extensions.List(networkClient).Allpages()
allExtensions, err := extensions.ExtractExtensions(allPages)
for _, extension := range allExtensions{
fmt.Println("%+v\n", extension)
}
*/
package extensions

View File

@ -0,0 +1,21 @@
package extensions
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) (r GetResult) {
resp, err := c.Get(ExtensionURL(c, alias), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page {
return ExtensionPage{pagination.SinglePageBase(r)}
})
}

View File

@ -0,0 +1,53 @@
package extensions
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// GetResult temporarily stores the result of a Get call.
// Use its Extract() method to interpret it as an Extension.
type GetResult struct {
gophercloud.Result
}
// Extract interprets a GetResult as an Extension.
func (r GetResult) Extract() (*Extension, error) {
var s struct {
Extension *Extension `json:"extension"`
}
err := r.ExtractInto(&s)
return s.Extension, err
}
// Extension is a struct that represents an OpenStack extension.
type Extension struct {
Updated string `json:"updated"`
Name string `json:"name"`
Links []interface{} `json:"links"`
Namespace string `json:"namespace"`
Alias string `json:"alias"`
Description string `json:"description"`
}
// ExtensionPage is the page returned by a pager when traversing over a collection of extensions.
type ExtensionPage struct {
pagination.SinglePageBase
}
// IsEmpty checks whether an ExtensionPage struct is empty.
func (r ExtensionPage) IsEmpty() (bool, error) {
is, err := ExtractExtensions(r)
return len(is) == 0, err
}
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage
// struct, and extracts the elements into a slice of Extension structs.
// In other words, a generic collection is mapped into a relevant slice.
func ExtractExtensions(r pagination.Page) ([]Extension, error) {
var s struct {
Extensions []Extension `json:"extensions"`
}
err := (r.(ExtensionPage)).ExtractInto(&s)
return s.Extensions, err
}

View File

@ -0,0 +1,13 @@
package extensions
import "github.com/gophercloud/gophercloud"
// ExtensionURL generates the URL for an extension resource by name.
func ExtensionURL(c *gophercloud.ServiceClient, name string) string {
return c.ServiceURL("extensions", name)
}
// ListExtensionURL generates the URL for the extensions resource collection.
func ListExtensionURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("extensions")
}

View File

@ -14,9 +14,10 @@ func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager {
// 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{
resp, err := client.Get(getInterfaceURL(client, serverID, portID), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -58,15 +59,17 @@ func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsB
r.Err = err
return
}
_, r.Err = client.Post(createInterfaceURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createInterfaceURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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)
resp, err := client.Delete(deleteInterfaceURL(client, serverID, portID), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -75,6 +75,10 @@ type BlockDevice struct {
// DiskBus is the bus type of the block devices.
// Examples of this are ide, usb, virtio, scsi, etc.
DiskBus string `json:"disk_bus,omitempty"`
// VolumeType is the volume type of the block device.
// This requires Compute API microversion 2.67 or later.
VolumeType string `json:"volume_type,omitempty"`
}
// CreateOptsExt is a structure that extends the server `CreateOpts` structure
@ -121,8 +125,9 @@ func Create(client *gophercloud.ServiceClient, opts servers.CreateOptsBuilder) (
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 202},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -3,5 +3,5 @@ package bootfromvolume
import "github.com/gophercloud/gophercloud"
func createURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("os-volumes_boot")
return c.ServiceURL("servers")
}

View File

@ -0,0 +1,23 @@
package extensions
import (
"github.com/gophercloud/gophercloud"
common "github.com/gophercloud/gophercloud/openstack/common/extensions"
"github.com/gophercloud/gophercloud/pagination"
)
// ExtractExtensions interprets a Page as a slice of Extensions.
func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
return common.ExtractExtensions(page)
}
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
return common.Get(c, alias)
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return common.List(c)
}

View File

@ -0,0 +1,3 @@
// Package extensions provides information and interaction with the
// different extensions available for the OpenStack Compute service.
package extensions

View File

@ -67,20 +67,23 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get returns public data about a previously uploaded KeyPair.
func Get(client *gophercloud.ServiceClient, name string) (r GetResult) {
_, r.Err = client.Get(getURL(client, name), &r.Body, nil)
resp, err := client.Get(getURL(client, name), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete requests the deletion of a previous stored KeyPair from the server.
func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, name), nil)
resp, err := client.Delete(deleteURL(client, name), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -1,19 +1,20 @@
package startstop
import "github.com/gophercloud/gophercloud"
func actionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions"
)
// Start is the operation responsible for starting a Compute server.
func Start(client *gophercloud.ServiceClient, id string) (r StartResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil)
resp, err := client.Post(extensions.ActionURL(client, id), map[string]interface{}{"os-start": nil}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Stop is the operation responsible for stopping a Compute server.
func Stop(client *gophercloud.ServiceClient, id string) (r StopResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil)
resp, err := client.Post(extensions.ActionURL(client, id), map[string]interface{}{"os-stop": nil}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -0,0 +1,7 @@
package extensions
import "github.com/gophercloud/gophercloud"
func ActionURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("servers", id, "action")
}

View File

@ -142,22 +142,25 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get retrieves details of a single flavor. Use Extract to convert its
// result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete deletes the specified flavor ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -194,9 +197,10 @@ func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsB
r.Err = err
return
}
_, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -224,20 +228,23 @@ func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAcces
r.Err = err
return
}
_, r.Err = client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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)
resp, err := client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) {
_, r.Err = client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil)
resp, err := client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -264,9 +271,10 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C
r.Err = err
return
}
_, r.Err = client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -302,56 +310,19 @@ func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts Up
r.Err = err
return
}
_, r.Err = client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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{
resp, err := client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// IDFromName is a convienience function that returns a flavor's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
allPages, err := ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := ExtractFlavors(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "flavor"
err.Name = name
return "", err
case 1:
return id, nil
default:
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "flavor"
err.Name = name
err.Count = count
return "", err
}
}

View File

@ -1,32 +0,0 @@
/*
Package images provides information and interaction with the images through
the OpenStack Compute service.
This API is deprecated and will be removed from a future version of the Nova
API service.
An image is a collection of files used to create or rebuild a server.
Operators provide a number of pre-built OS images by default. You may also
create custom images from cloud servers you have launched.
Example to List Images
listOpts := images.ListOpts{
Limit: 2,
}
allPages, err := images.ListDetail(computeClient, listOpts).AllPages()
if err != nil {
panic(err)
}
allImages, err := images.ExtractImages(allPages)
if err != nil {
panic(err)
}
for _, image := range allImages {
fmt.Printf("%+v\n", image)
}
*/
package images

View File

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

View File

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

View File

@ -1,15 +0,0 @@
package images
import "github.com/gophercloud/gophercloud"
func listDetailURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("images", "detail")
}
func getURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("images", id)
}
func deleteURL(client *gophercloud.ServiceClient, id string) string {
return client.ServiceURL("images", id)
}

View File

@ -1,11 +0,0 @@
package servers
// ExtractTags will extract the tags of a server.
// This requires the client to be set to microversion 2.26 or later.
func (r serverResult) ExtractTags() ([]string, error) {
var s struct {
Tags []string `json:"tags"`
}
err := r.ExtractInto(&s)
return s.Tags, err
}

View File

@ -3,10 +3,9 @@ package servers
import (
"encoding/base64"
"encoding/json"
"fmt"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/pagination"
)
@ -55,6 +54,22 @@ type ListOpts struct {
// TenantID lists servers for a particular tenant.
// Setting "AllTenants = true" is required.
TenantID string `q:"tenant_id"`
// This requires the client to be set to microversion 2.26 or later.
// Tags filters on specific server tags. All tags must be present for the server.
Tags string `q:"tags"`
// This requires the client to be set to microversion 2.26 or later.
// TagsAny filters on specific server tags. At least one of the tags must be present for the server.
TagsAny string `q:"tags-any"`
// This requires the client to be set to microversion 2.26 or later.
// NotTags filters on specific server tags. All tags must be absent for the server.
NotTags string `q:"not-tags"`
// This requires the client to be set to microversion 2.26 or later.
// NotTagsAny filters on specific server tags. At least one of the tags must be absent for the server.
NotTagsAny string `q:"not-tags-any"`
}
// ToServerListQuery formats a ListOpts into a query string.
@ -131,24 +146,14 @@ type CreateOpts struct {
// Name is the name to assign to the newly launched server.
Name string `json:"name" required:"true"`
// ImageRef [optional; required if ImageName is not provided] is the ID or
// full URL to the image that contains the server's OS and initial state.
// ImageRef is the ID or full URL to the image that contains the
// server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageRef string `json:"imageRef"`
// ImageName [optional; required if ImageRef is not provided] is the name of
// the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageName string `json:"-"`
// FlavorRef [optional; required if FlavorName is not provided] is the ID or
// full URL to the flavor that describes the server's specs.
// FlavorRef is the ID or full URL to the flavor that describes the server's specs.
FlavorRef string `json:"flavorRef"`
// FlavorName [optional; required if FlavorRef is not provided] is the name of
// the flavor that describes the server's specs.
FlavorName string `json:"-"`
// SecurityGroups lists the names of the security groups to which this server
// should belong.
SecurityGroups []string `json:"-"`
@ -163,7 +168,9 @@ type CreateOpts struct {
// Networks dictates how this server will be attached to available networks.
// By default, the server will be attached to all isolated networks for the
// tenant.
Networks []Network `json:"-"`
// Starting with microversion 2.37 networks can also be an "auto" or "none"
// string.
Networks interface{} `json:"-"`
// Metadata contains key-value pairs (up to 255 bytes each) to attach to the
// server.
@ -204,7 +211,6 @@ type CreateOpts struct {
// ToServerCreateMap assembles a request body based on the contents of a
// CreateOpts.
func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
sc := opts.ServiceClient
opts.ServiceClient = nil
b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil {
@ -229,59 +235,32 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
b["security_groups"] = securityGroups
}
if len(opts.Networks) > 0 {
networks := make([]map[string]interface{}, len(opts.Networks))
for i, net := range opts.Networks {
networks[i] = make(map[string]interface{})
if net.UUID != "" {
networks[i]["uuid"] = net.UUID
}
if net.Port != "" {
networks[i]["port"] = net.Port
}
if net.FixedIP != "" {
networks[i]["fixed_ip"] = net.FixedIP
switch v := opts.Networks.(type) {
case []Network:
if len(v) > 0 {
networks := make([]map[string]interface{}, len(v))
for i, net := range v {
networks[i] = make(map[string]interface{})
if net.UUID != "" {
networks[i]["uuid"] = net.UUID
}
if net.Port != "" {
networks[i]["port"] = net.Port
}
if net.FixedIP != "" {
networks[i]["fixed_ip"] = net.FixedIP
}
}
b["networks"] = networks
}
b["networks"] = networks
}
// If ImageRef isn't provided, check if ImageName was provided to ascertain
// the image ID.
if opts.ImageRef == "" {
if opts.ImageName != "" {
if sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
imageID, err := images.IDFromName(sc, opts.ImageName)
if err != nil {
return nil, err
}
b["imageRef"] = imageID
case string:
if v == "auto" || v == "none" {
b["networks"] = v
} else {
return nil, fmt.Errorf(`networks must be a slice of Network struct or a string with "auto" or "none" values, current value is %q`, v)
}
}
// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
if opts.FlavorRef == "" {
if opts.FlavorName == "" {
err := ErrNeitherFlavorIDNorFlavorNameProvided{}
err.Argument = "FlavorRef/FlavorName"
return nil, err
}
if sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
flavorID, err := flavors.IDFromName(sc, opts.FlavorName)
if err != nil {
return nil, err
}
b["flavorRef"] = flavorID
}
if opts.Min != 0 {
b["min_count"] = opts.Min
}
@ -300,28 +279,32 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return
}
_, r.Err = client.Post(listURL(client), reqBody, &r.Body, nil)
resp, err := client.Post(listURL(client), reqBody, &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete requests that a server previously provisioned be removed from your
// account.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// ForceDelete forces the deletion of a server.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
resp, err := client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
resp, err := client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -358,9 +341,10 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -372,7 +356,8 @@ func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword stri
"adminPass": newPassword,
},
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
resp, err := client.Post(actionURL(client, id), b, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -413,7 +398,7 @@ func (opts RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
HardReboot (aka PowerCycle) starts the server instance by physically cutting
power to the machine, or if a VM, terminating it at the hypervisor level.
It's done. Caput. Full stop.
Then, after a brief while, power is rtored or the VM instance restarted.
Then, after a brief while, power is restored or the VM instance restarted.
SoftReboot (aka OSReboot) simply tells the OS to restart under its own
procedure.
@ -426,7 +411,8 @@ func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
resp, err := client.Post(actionURL(client, id), b, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -442,11 +428,8 @@ type RebuildOpts struct {
// AdminPass is the server's admin password
AdminPass string `json:"adminPass,omitempty"`
// ImageID is the ID of the image you want your server to be provisioned on.
ImageID string `json:"imageRef"`
// ImageName is readable name of an image.
ImageName string `json:"-"`
// ImageRef is the ID of the image you want your server to be provisioned on.
ImageRef string `json:"imageRef"`
// Name to set the server to
Name string `json:"name,omitempty"`
@ -477,23 +460,6 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
return nil, err
}
// If ImageRef isn't provided, check if ImageName was provided to ascertain
// the image ID.
if opts.ImageID == "" {
if opts.ImageName != "" {
if opts.ServiceClient == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
imageID, err := images.IDFromName(opts.ServiceClient, opts.ImageName)
if err != nil {
return nil, err
}
b["imageRef"] = imageID
}
}
return map[string]interface{}{"rebuild": b}, nil
}
@ -505,7 +471,8 @@ func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuild
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, nil)
resp, err := client.Post(actionURL(client, id), b, &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -543,23 +510,26 @@ func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, nil, nil)
resp, err := client.Post(actionURL(client, id), b, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details.
func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{
OkCodes: []int{201, 202, 204},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// RevertResize cancels a previous resize operation on a server.
// See Resize() for more details.
func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil)
resp, err := client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -595,15 +565,17 @@ func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetad
r.Err = err
return
}
_, r.Err = client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Metadata requests all the metadata for the given server ID.
func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) {
_, r.Err = client.Get(metadataURL(client, id), &r.Body, nil)
resp, err := client.Get(metadataURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -622,9 +594,10 @@ func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMet
r.Err = err
return
}
_, r.Err = client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -662,23 +635,26 @@ func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts Metadatu
r.Err = err
return
}
_, r.Err = client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Metadatum requests the key-value pair with the given key for the given
// server ID.
func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) {
_, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil)
resp, err := client.Get(metadatumURL(client, id, key), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// DeleteMetadatum will delete the key-value pair with the given key for the
// given server ID.
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) {
_, r.Err = client.Delete(metadatumURL(client, id, key), nil)
resp, err := client.Delete(metadatumURL(client, id, key), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -731,52 +707,15 @@ func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageO
resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202},
})
r.Err = err
r.Header = resp.Header
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// IDFromName is a convienience function that returns a server's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
listOpts := ListOpts{
Name: name,
}
allPages, err := List(client, listOpts).AllPages()
if err != nil {
return "", err
}
all, err := ExtractServers(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "server"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "server"}
}
}
// GetPassword makes a request against the nova API to get the encrypted
// administrative password.
func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil)
resp, err := client.Get(passwordURL(client, serverId), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -805,8 +744,9 @@ func ShowConsoleOutput(client *gophercloud.ServiceClient, id string, opts ShowCo
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -212,8 +212,19 @@ type Server struct {
// to it.
SecurityGroups []map[string]interface{} `json:"security_groups"`
// AttachedVolumes includes the volume attachments of this instance
AttachedVolumes []AttachedVolume `json:"os-extended-volumes:volumes_attached"`
// Fault contains failure information about a server.
Fault Fault `json:"fault"`
// Tags is a slice/list of string tags in a server.
// The requires microversion 2.26 or later.
Tags *[]string `json:"tags"`
}
type AttachedVolume struct {
ID string `json:"id"`
}
type Fault struct {

View File

@ -7,7 +7,7 @@ Example of Creating a Service Client
ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao)
client, err := openstack.NewNetworkV2(client, gophercloud.EndpointOpts{
client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
Region: os.Getenv("OS_REGION_NAME"),
})
*/

View File

@ -29,11 +29,12 @@ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpt
}
}
// Report an error if the options were ambiguous.
// If multiple endpoints were found, use the first result
// and disregard the other endpoints.
//
// This behavior matches the Python library. See GH-1764.
if len(endpoints) > 1 {
err := &ErrMultipleMatchingEndpointsV2{}
err.Endpoints = endpoints
return "", err
endpoints = endpoints[0:1]
}
// Extract the appropriate URL from the matching Endpoint.
@ -91,9 +92,12 @@ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpt
}
}
// Report an error if the options were ambiguous.
// If multiple endpoints were found, use the first result
// and disregard the other endpoints.
//
// This behavior matches the Python library. See GH-1764.
if len(endpoints) > 1 {
return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints}
endpoints = endpoints[0:1]
}
// Extract the URL from the matching Endpoint.

View File

@ -4,8 +4,6 @@ import (
"fmt"
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)
// ErrEndpointNotFound is the error when no suitable endpoint can be found
@ -24,28 +22,6 @@ func (e ErrInvalidAvailabilityProvided) Error() string {
return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value)
}
// ErrMultipleMatchingEndpointsV2 is the error when more than one endpoint
// for the given options is found in the v2 catalog
type ErrMultipleMatchingEndpointsV2 struct {
gophercloud.BaseError
Endpoints []tokens2.Endpoint
}
func (e ErrMultipleMatchingEndpointsV2) Error() string {
return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
}
// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint
// for the given options is found in the v3 catalog
type ErrMultipleMatchingEndpointsV3 struct {
gophercloud.BaseError
Endpoints []tokens3.Endpoint
}
func (e ErrMultipleMatchingEndpointsV3) Error() string {
return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints)
}
// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not
// found
type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput }

View File

@ -8,7 +8,7 @@ for more information.
Example to List Tenants
listOpts := tenants.ListOpts{
listOpts := &tenants.ListOpts{
Limit: 2,
}

View File

@ -60,15 +60,17 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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)
resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -103,14 +105,16 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err
return
}
_, r.Err = client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
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)
resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -87,17 +87,19 @@ func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r Creat
r.Err = err
return
}
_, r.Err = client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
MoreHeaders: map[string]string{"X-Auth-Token": ""},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get validates and retrieves information for user's token.
func Get(client *gophercloud.ServiceClient, token string) (r GetResult) {
_, r.Err = client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{
resp, err := client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -0,0 +1,41 @@
/*
Package tokens provides information and interaction with the EC2 token API
resource for the OpenStack Identity service.
For more information, see:
https://docs.openstack.org/api-ref/identity/v2-ext/
Example to Create a Token From an EC2 access and secret keys
var authOptions tokens.AuthOptionsBuilder
authOptions = &ec2tokens.AuthOptions{
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
Secret: "18f4f6761ada4e3795fa5273c30349b9",
}
token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken()
if err != nil {
panic(err)
}
Example to auth a client using EC2 access and secret keys
client, err := openstack.NewClient("http://localhost:5000/v3")
if err != nil {
panic(err)
}
var authOptions tokens.AuthOptionsBuilder
authOptions = &ec2tokens.AuthOptions{
Access: "a7f1e798b7c2417cba4a02de97dc3cdc",
Secret: "18f4f6761ada4e3795fa5273c30349b9",
AllowReauth: true,
}
err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{})
if err != nil {
panic(err)
}
*/
package ec2tokens

View File

@ -0,0 +1,377 @@
package ec2tokens
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"fmt"
"math/rand"
"net/url"
"sort"
"strings"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)
const (
// EC2CredentialsAwsRequestV4 is a constant, used to generate AWS
// Credential V4.
EC2CredentialsAwsRequestV4 = "aws4_request"
// EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to
// generate AWS Credential V2.
EC2CredentialsHmacSha1V2 = "HmacSHA1"
// EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used
// to generate AWS Credential V2.
EC2CredentialsHmacSha256V2 = "HmacSHA256"
// EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method.
// More details:
// https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256"
// EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp
// format.
EC2CredentialsTimestampFormatV4 = "20060102T150405Z"
// EC2CredentialsDateFormatV4 is an AWS signature V4 date format.
EC2CredentialsDateFormatV4 = "20060102"
)
// AuthOptions represents options for authenticating a user using EC2 credentials.
type AuthOptions struct {
// Access is the EC2 Credential Access ID.
Access string `json:"access" required:"true"`
// Secret is the EC2 Credential Secret, used to calculate signature.
// Not used, when a Signature is is.
Secret string `json:"-"`
// Host is a HTTP request Host header. Used to calculate an AWS
// signature V2. For signature V4 set the Host inside Headers map.
// Optional.
Host string `json:"host"`
// Path is a HTTP request path. Optional.
Path string `json:"path"`
// Verb is a HTTP request method. Optional.
Verb string `json:"verb"`
// Headers is a map of HTTP request headers. Optional.
Headers map[string]string `json:"headers"`
// Region is a region name to calculate an AWS signature V4. Optional.
Region string `json:"-"`
// Service is a service name to calculate an AWS signature V4. Optional.
Service string `json:"-"`
// Params is a map of GET method parameters. Optional.
Params map[string]string `json:"params"`
// AllowReauth allows Gophercloud to re-authenticate automatically
// if/when your token expires.
AllowReauth bool `json:"-"`
// Signature can be either a []byte (encoded to base64 automatically) or
// a string. You can set the singature explicitly, when you already know
// it. In this case default Params won't be automatically set. Optional.
Signature interface{} `json:"signature"`
// BodyHash is a HTTP request body sha256 hash. When nil and Signature
// is not set, a random hash is generated. Optional.
BodyHash *string `json:"body_hash"`
// Timestamp is a timestamp to calculate a V4 signature. Optional.
Timestamp *time.Time `json:"-"`
// Token is a []byte string (encoded to base64 automatically) which was
// signed by an EC2 secret key. Used by S3 tokens for validation only.
// Token must be set with a Signature. If a Signature is not provided,
// a Token will be generated automatically along with a Signature.
Token []byte `json:"token,omitempty"`
}
// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string
// for an AWS signature V2.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133
func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string {
var keys []string
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
var pairs []string
for _, k := range keys {
pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k])))
}
return strings.Join(pairs, "&")
}
// EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature
// V2.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148
func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte {
stringToSign := strings.Join([]string{
opts.Verb,
opts.Host,
opts.Path,
}, "\n")
return []byte(strings.Join([]string{
stringToSign,
EC2CredentialsBuildCanonicalQueryStringV2(opts.Params),
}, "\n"))
}
// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string
// for an AWS signature V4.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244
func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string {
if verb == "POST" {
return ""
}
return EC2CredentialsBuildCanonicalQueryStringV2(params)
}
// EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on
// "headers" map and "signedHeaders" string parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216
func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string {
headersLower := make(map[string]string, len(headers))
for k, v := range headers {
headersLower[strings.ToLower(k)] = v
}
var headersList []string
for _, h := range strings.Split(signedHeaders, ";") {
if v, ok := headersLower[h]; ok {
headersList = append(headersList, h+":"+v)
}
}
return strings.Join(headersList, "\n") + "\n"
}
// EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on
// input parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169
func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte {
kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4)))
kRegion := sumHMAC256(kDate, []byte(region))
kService := sumHMAC256(kRegion, []byte(service))
return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4))
}
// EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign
// based on input parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251
func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte {
scope := strings.Join([]string{
date.Format(EC2CredentialsDateFormatV4),
opts.Region,
opts.Service,
EC2CredentialsAwsRequestV4,
}, "/")
canonicalRequest := strings.Join([]string{
opts.Verb,
opts.Path,
EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params),
EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders),
signedHeaders,
bodyHash,
}, "\n")
hash := sha256.Sum256([]byte(canonicalRequest))
return []byte(strings.Join([]string{
EC2CredentialsAwsHmacV4,
date.Format(EC2CredentialsTimestampFormatV4),
scope,
hex.EncodeToString(hash[:]),
}, "\n"))
}
// EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input
// parameters.
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286
func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string {
return hex.EncodeToString(sumHMAC256(key, stringToSign))
}
// EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization
// header based on auth parameters, date and signature
func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string {
return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s",
EC2CredentialsAwsHmacV4,
opts.Access,
date.Format(EC2CredentialsDateFormatV4),
opts.Region,
opts.Service,
EC2CredentialsAwsRequestV4,
signedHeaders,
signature)
}
// ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder
// interface.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
return nil, nil
}
// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v3 tokens package.
func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) {
return nil, nil
}
// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface
func (opts *AuthOptions) CanReauth() bool {
return opts.AllowReauth
}
// ToTokenV3CreateMap formats an AuthOptions into a create request.
func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
b, err := gophercloud.BuildRequestBody(opts, "credentials")
if err != nil {
return nil, err
}
if opts.Signature != nil {
return b, nil
}
// calculate signature, when it is not set
c, _ := b["credentials"].(map[string]interface{})
h := interfaceToMap(c, "headers")
p := interfaceToMap(c, "params")
// detect and process a signature v2
if v, ok := p["SignatureVersion"]; ok && v == "2" {
if _, ok := c["body_hash"]; ok {
delete(c, "body_hash")
}
if _, ok := c["headers"]; ok {
delete(c, "headers")
}
if v, ok := p["SignatureMethod"]; ok {
// params is a map of strings
strToSign := EC2CredentialsBuildStringToSignV2(*opts)
switch v {
case EC2CredentialsHmacSha1V2:
// keystone uses this method only when HmacSHA256 is not available on the server side
// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156
c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign)
return b, nil
case EC2CredentialsHmacSha256V2:
c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign)
return b, nil
}
return nil, fmt.Errorf("unsupported signature method: %s", v)
}
return nil, fmt.Errorf("signature method must be provided")
} else if ok {
return nil, fmt.Errorf("unsupported signature version: %s", v)
}
// it is not a signature v2, but a signature v4
date := time.Now().UTC()
if opts.Timestamp != nil {
date = *opts.Timestamp
}
if v, _ := c["body_hash"]; v == nil {
// when body_hash is not set, generate a random one
c["body_hash"] = randomBodyHash()
}
signedHeaders, _ := h["X-Amz-SignedHeaders"]
stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date)
key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date)
c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign)
h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4)
h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date)
// token is only used for S3 tokens validation and will be removed when using EC2 validation
c["token"] = stringToSign
return b, nil
}
// Create authenticates and either generates a new token from EC2 credentials
func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
b, err := opts.ToTokenV3CreateMap(nil)
if err != nil {
r.Err = err
return
}
// delete "token" element, since it is used in s3tokens
deleteBodyElements(b, "token")
resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't
// generate a new token ID, but returns a tokens.CreateResult.
func ValidateS3Token(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
b, err := opts.ToTokenV3CreateMap(nil)
if err != nil {
r.Err = err
return
}
// delete unused element, since it is used in ec2tokens only
deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb")
resp, err := c.Post(s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// The following are small helper functions used to help build the signature.
// sumHMAC1 is a func to implement the HMAC SHA1 signature method.
func sumHMAC1(key []byte, data []byte) []byte {
hash := hmac.New(sha1.New, key)
hash.Write(data)
return hash.Sum(nil)
}
// sumHMAC256 is a func to implement the HMAC SHA256 signature method.
func sumHMAC256(key []byte, data []byte) []byte {
hash := hmac.New(sha256.New, key)
hash.Write(data)
return hash.Sum(nil)
}
// randomBodyHash is a func to generate a random sha256 hexdigest.
func randomBodyHash() string {
h := make([]byte, 64)
rand.Read(h)
return hex.EncodeToString(h)
}
// interfaceToMap is a func used to represent a "credentials" map element as a
// "map[string]string"
func interfaceToMap(c map[string]interface{}, key string) map[string]string {
// convert map[string]interface{} to map[string]string
m := make(map[string]string)
if v, _ := c[key].(map[string]interface{}); v != nil {
for k, v := range v {
m[k] = v.(string)
}
}
c[key] = m
return m
}
// deleteBodyElements deletes map body elements
func deleteBodyElements(b map[string]interface{}, elements ...string) {
if c, ok := b["credentials"].(map[string]interface{}); ok {
for _, k := range elements {
if _, ok := c[k]; ok {
delete(c, k)
}
}
}
}

View File

@ -0,0 +1,11 @@
package ec2tokens
import "github.com/gophercloud/gophercloud"
func ec2tokensURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("ec2tokens")
}
func s3tokensURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("s3tokens")
}

View File

@ -0,0 +1,123 @@
/*
Package oauth1 enables management of OpenStack OAuth1 tokens and Authentication.
Example to Create an OAuth1 Consumer
createConsumerOpts := oauth1.CreateConsumerOpts{
Description: "My consumer",
}
consumer, err := oauth1.CreateConsumer(identityClient, createConsumerOpts).Extract()
if err != nil {
panic(err)
}
// NOTE: Consumer secret is available only on create response
fmt.Printf("Consumer: %+v\n", consumer)
Example to Request an unauthorized OAuth1 token
requestTokenOpts := oauth1.RequestTokenOpts{
OAuthConsumerKey: consumer.ID,
OAuthConsumerSecret: consumer.Secret,
OAuthSignatureMethod: oauth1.HMACSHA1,
RequestedProjectID: projectID,
}
requestToken, err := oauth1.RequestToken(identityClient, requestTokenOpts).Extract()
if err != nil {
panic(err)
}
// NOTE: Request token secret is available only on request response
fmt.Printf("Request token: %+v\n", requestToken)
Example to Authorize an unauthorized OAuth1 token
authorizeTokenOpts := oauth1.AuthorizeTokenOpts{
Roles: []oauth1.Role{
{Name: "member"},
},
}
authToken, err := oauth1.AuthorizeToken(identityClient, requestToken.OAuthToken, authorizeTokenOpts).Extract()
if err != nil {
panic(err)
}
fmt.Printf("Verifier ID of the unauthorized Token: %+v\n", authToken.OAuthVerifier)
Example to Create an OAuth1 Access Token
accessTokenOpts := oauth1.CreateAccessTokenOpts{
OAuthConsumerKey: consumer.ID,
OAuthConsumerSecret: consumer.Secret,
OAuthToken: requestToken.OAuthToken,
OAuthTokenSecret: requestToken.OAuthTokenSecret,
OAuthVerifier: authToken.OAuthVerifier,
OAuthSignatureMethod: oauth1.HMACSHA1,
}
accessToken, err := oauth1.CreateAccessToken(identityClient, accessTokenOpts).Extract()
if err != nil {
panic(err)
}
// NOTE: Access token secret is available only on create response
fmt.Printf("OAuth1 Access Token: %+v\n", accessToken)
Example to List User's OAuth1 Access Tokens
allPages, err := oauth1.ListAccessTokens(identityClient, userID).AllPages()
if err != nil {
panic(err)
}
accessTokens, err := oauth1.ExtractAccessTokens(allPages)
if err != nil {
panic(err)
}
for _, accessToken := range accessTokens {
fmt.Printf("Access Token: %+v\n", accessToken)
}
Example to Authenticate a client using OAuth1 method
client, err := openstack.NewClient("http://localhost:5000/v3")
if err != nil {
panic(err)
}
authOptions := &oauth1.AuthOptions{
// consumer token, created earlier
OAuthConsumerKey: consumer.ID,
OAuthConsumerSecret: consumer.Secret,
// access token, created earlier
OAuthToken: accessToken.OAuthToken,
OAuthTokenSecret: accessToken.OAuthTokenSecret,
OAuthSignatureMethod: oauth1.HMACSHA1,
}
err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{})
if err != nil {
panic(err)
}
Example to Create a Token using OAuth1 method
var oauth1Token struct {
tokens.Token
oauth1.TokenExt
}
createOpts := &oauth1.AuthOptions{
// consumer token, created earlier
OAuthConsumerKey: consumer.ID,
OAuthConsumerSecret: consumer.Secret,
// access token, created earlier
OAuthToken: accessToken.OAuthToken,
OAuthTokenSecret: accessToken.OAuthTokenSecret,
OAuthSignatureMethod: oauth1.HMACSHA1,
}
err := tokens.Create(identityClient, createOpts).ExtractInto(&oauth1Token)
if err != nil {
panic(err)
}
*/
package oauth1

View File

@ -0,0 +1,587 @@
package oauth1
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"io/ioutil"
"math/rand"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/pagination"
)
// Type SignatureMethod is a OAuth1 SignatureMethod type.
type SignatureMethod string
const (
// HMACSHA1 is a recommended OAuth1 signature method.
HMACSHA1 SignatureMethod = "HMAC-SHA1"
// PLAINTEXT signature method is not recommended to be used in
// production environment.
PLAINTEXT SignatureMethod = "PLAINTEXT"
// OAuth1TokenContentType is a supported content type for an OAuth1
// token.
OAuth1TokenContentType = "application/x-www-form-urlencoded"
)
// AuthOptions represents options for authenticating a user using OAuth1 tokens.
type AuthOptions struct {
// OAuthConsumerKey is the OAuth1 Consumer Key.
OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
// an OAuth1 request signature.
OAuthConsumerSecret string `required:"true"`
// OAuthToken is the OAuth1 Request Token.
OAuthToken string `q:"oauth_token" required:"true"`
// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
// an OAuth1 request signature.
OAuthTokenSecret string `required:"true"`
// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
// "PLAINTEXT" is not recommended for production usage.
OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
// timestamp will be used.
OAuthTimestamp *time.Time
// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
// uniquely generated for each request. Will be generated automatically
// when it is not set.
OAuthNonce string `q:"oauth_nonce"`
// AllowReauth allows Gophercloud to re-authenticate automatically
// if/when your token expires.
AllowReauth bool
}
// ToTokenV3HeadersMap builds the headers required for an OAuth1-based create
// request.
func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]interface{}) (map[string]string, error) {
q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
if err != nil {
return nil, err
}
signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}
method := headerOpts["method"].(string)
u := headerOpts["url"].(string)
stringToSign := buildStringToSign(method, u, q.Query())
signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
authHeader := buildAuthHeader(q.Query(), signature)
headers := map[string]string{
"Authorization": authHeader,
"X-Auth-Token": "",
}
return headers, nil
}
// ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
// interface.
func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
return nil, nil
}
// CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder
// interface.
func (opts AuthOptions) CanReauth() bool {
return opts.AllowReauth
}
// ToTokenV3CreateMap builds a create request body.
func (opts AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) {
// identityReq defines the "identity" portion of an OAuth1-based authentication
// create request body.
type identityReq struct {
Methods []string `json:"methods"`
OAuth1 struct{} `json:"oauth1"`
}
// authReq defines the "auth" portion of an OAuth1-based authentication
// create request body.
type authReq struct {
Identity identityReq `json:"identity"`
}
// oauth1Request defines how an OAuth1-based authentication create
// request body looks.
type oauth1Request struct {
Auth authReq `json:"auth"`
}
var req oauth1Request
req.Auth.Identity.Methods = []string{"oauth1"}
return gophercloud.BuildRequestBody(req, "")
}
// Create authenticates and either generates a new OpenStack token from an
// OAuth1 token.
func Create(client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) {
b, err := opts.ToTokenV3CreateMap(nil)
if err != nil {
r.Err = err
return
}
headerOpts := map[string]interface{}{
"method": "POST",
"url": authURL(client),
}
h, err := opts.ToTokenV3HeadersMap(headerOpts)
if err != nil {
r.Err = err
return
}
resp, err := client.Post(authURL(client), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// CreateConsumerOptsBuilder allows extensions to add additional parameters to
// the CreateConsumer request.
type CreateConsumerOptsBuilder interface {
ToOAuth1CreateConsumerMap() (map[string]interface{}, error)
}
// CreateConsumerOpts provides options used to create a new Consumer.
type CreateConsumerOpts struct {
// Description is the consumer description.
Description string `json:"description"`
}
// ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request.
func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "consumer")
}
// Create creates a new Consumer.
func CreateConsumer(client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) {
b, err := opts.ToOAuth1CreateConsumerMap()
if err != nil {
r.Err = err
return
}
resp, err := client.Post(consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete deletes a Consumer.
func DeleteConsumer(client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) {
resp, err := client.Delete(consumerURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// List enumerates Consumers.
func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager {
return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page {
return ConsumersPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// GetConsumer retrieves details on a single Consumer by ID.
func GetConsumer(client *gophercloud.ServiceClient, id string) (r GetConsumerResult) {
resp, err := client.Get(consumerURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// UpdateConsumerOpts provides options used to update a consumer.
type UpdateConsumerOpts struct {
// Description is the consumer description.
Description string `json:"description"`
}
// ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update
// request.
func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "consumer")
}
// UpdateConsumer updates an existing Consumer.
func UpdateConsumer(client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) {
b, err := opts.ToOAuth1UpdateConsumerMap()
if err != nil {
r.Err = err
return
}
resp, err := client.Patch(consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// RequestTokenOptsBuilder allows extensions to add additional parameters to the
// RequestToken request.
type RequestTokenOptsBuilder interface {
ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error)
}
// RequestTokenOpts provides options used to get a consumer unauthorized
// request token.
type RequestTokenOpts struct {
// OAuthConsumerKey is the OAuth1 Consumer Key.
OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
// an OAuth1 request signature.
OAuthConsumerSecret string `required:"true"`
// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
// "PLAINTEXT" is not recommended for production usage.
OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
// timestamp will be used.
OAuthTimestamp *time.Time
// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
// uniquely generated for each request. Will be generated automatically
// when it is not set.
OAuthNonce string `q:"oauth_nonce"`
// RequestedProjectID is a Project ID a consumer user requested an
// access to.
RequestedProjectID string `h:"Requested-Project-Id"`
}
// ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request
// headers.
func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) {
q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob")
if err != nil {
return nil, err
}
h, err := gophercloud.BuildHeaders(opts)
if err != nil {
return nil, err
}
signatureKeys := []string{opts.OAuthConsumerSecret}
stringToSign := buildStringToSign(method, u, q.Query())
signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
authHeader := buildAuthHeader(q.Query(), signature)
h["Authorization"] = authHeader
return h, nil
}
// RequestToken requests an unauthorized OAuth1 Token.
func RequestToken(client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) {
h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client))
if err != nil {
r.Err = err
return
}
resp, err := client.Post(requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201},
KeepResponseBody: true,
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
if r.Err != nil {
return
}
defer resp.Body.Close()
if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
return
}
r.Body, r.Err = ioutil.ReadAll(resp.Body)
return
}
// AuthorizeTokenOptsBuilder allows extensions to add additional parameters to
// the AuthorizeToken request.
type AuthorizeTokenOptsBuilder interface {
ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error)
}
// AuthorizeTokenOpts provides options used to authorize a request token.
type AuthorizeTokenOpts struct {
Roles []Role `json:"roles"`
}
// Role is a struct representing a role object in a AuthorizeTokenOpts struct.
type Role struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
// ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token
// request.
func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) {
for _, r := range opts.Roles {
if r == (Role{}) {
return nil, fmt.Errorf("role must not be empty")
}
}
return gophercloud.BuildRequestBody(opts, "")
}
// AuthorizeToken authorizes an unauthorized consumer token.
func AuthorizeToken(client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) {
b, err := opts.ToOAuth1AuthorizeTokenMap()
if err != nil {
r.Err = err
return
}
resp, err := client.Put(authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// CreateAccessTokenOptsBuilder allows extensions to add additional parameters
// to the CreateAccessToken request.
type CreateAccessTokenOptsBuilder interface {
ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error)
}
// CreateAccessTokenOpts provides options used to create an OAuth1 token.
type CreateAccessTokenOpts struct {
// OAuthConsumerKey is the OAuth1 Consumer Key.
OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"`
// OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate
// an OAuth1 request signature.
OAuthConsumerSecret string `required:"true"`
// OAuthToken is the OAuth1 Request Token.
OAuthToken string `q:"oauth_token" required:"true"`
// OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate
// an OAuth1 request signature.
OAuthTokenSecret string `required:"true"`
// OAuthVerifier is the OAuth1 verification code.
OAuthVerifier string `q:"oauth_verifier" required:"true"`
// OAuthSignatureMethod is the OAuth1 signature method the Consumer used
// to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT".
// "PLAINTEXT" is not recommended for production usage.
OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"`
// OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix
// timestamp will be used.
OAuthTimestamp *time.Time
// OAuthNonce is an OAuth1 request nonce. Nonce must be a random string,
// uniquely generated for each request. Will be generated automatically
// when it is not set.
OAuthNonce string `q:"oauth_nonce"`
}
// ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of
// request headers.
func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) {
q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "")
if err != nil {
return nil, err
}
signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret}
stringToSign := buildStringToSign(method, u, q.Query())
signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys))
authHeader := buildAuthHeader(q.Query(), signature)
headers := map[string]string{
"Authorization": authHeader,
}
return headers, nil
}
// CreateAccessToken creates a new OAuth1 Access Token
func CreateAccessToken(client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) {
h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client))
if err != nil {
r.Err = err
return
}
resp, err := client.Post(createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201},
KeepResponseBody: true,
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
if r.Err != nil {
return
}
defer resp.Body.Close()
if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType {
r.Err = fmt.Errorf("unsupported Content-Type: %q", v)
return
}
r.Body, r.Err = ioutil.ReadAll(resp.Body)
return
}
// GetAccessToken retrieves details on a single OAuth1 access token by an ID.
func GetAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) {
resp, err := client.Get(userAccessTokenURL(client, userID, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// RevokeAccessToken revokes an OAuth1 access token.
func RevokeAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) {
resp, err := client.Delete(userAccessTokenURL(client, userID, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// ListAccessTokens enumerates authorized access tokens.
func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager {
url := userAccessTokensURL(client, userID)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// ListAccessTokenRoles enumerates authorized access token roles.
func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager {
url := userAccessTokenRolesURL(client, userID, id)
return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}}
})
}
// GetAccessTokenRole retrieves details on a single OAuth1 access token role by
// an ID.
func GetAccessTokenRole(client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) {
resp, err := client.Get(userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// The following are small helper functions used to help build the signature.
// buildOAuth1QueryString builds a URLEncoded parameters string specific for
// OAuth1-based requests.
func buildOAuth1QueryString(opts interface{}, timestamp *time.Time, callback string) (*url.URL, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return nil, err
}
query := q.Query()
if timestamp != nil {
// use provided timestamp
query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10))
} else {
// use current timestamp
query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10))
}
if query.Get("oauth_nonce") == "" {
// when nonce is not set, generate a random one
query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp"))
}
if callback != "" {
query.Set("oauth_callback", callback)
}
query.Set("oauth_version", "1.0")
return &url.URL{RawQuery: query.Encode()}, nil
}
// buildStringToSign builds a string to be signed.
func buildStringToSign(method string, u string, query url.Values) []byte {
parsedURL, _ := url.Parse(u)
p := parsedURL.Port()
s := parsedURL.Scheme
// Default scheme port must be stripped
if s == "http" && p == "80" || s == "https" && p == "443" {
parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p)
}
// Ensure that URL doesn't contain queries
parsedURL.RawQuery = ""
v := strings.Join(
[]string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&")
return []byte(v)
}
// signString signs a string using an OAuth1 signature method.
func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string {
var key []byte
for i, k := range signatureKeys {
key = append(key, []byte(url.QueryEscape(k))...)
if i == 0 {
key = append(key, '&')
}
}
var signedString string
switch signatureMethod {
case PLAINTEXT:
signedString = string(key)
default:
h := hmac.New(sha1.New, key)
h.Write(strToSign)
signedString = base64.StdEncoding.EncodeToString(h.Sum(nil))
}
return signedString
}
// buildAuthHeader generates an OAuth1 Authorization header with a signature
// calculated using an OAuth1 signature method.
func buildAuthHeader(query url.Values, signature string) string {
var authHeader []string
var keys []string
for k := range query {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
for _, v := range query[k] {
authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v)))
}
}
authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature))
return "OAuth " + strings.Join(authHeader, ", ")
}

View File

@ -0,0 +1,305 @@
package oauth1
import (
"encoding/json"
"net/url"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
// Consumer represents a delegated authorization request between two
// identities.
type Consumer struct {
ID string `json:"id"`
Secret string `json:"secret"`
Description string `json:"description"`
}
type consumerResult struct {
gophercloud.Result
}
// CreateConsumerResult is the response from a Create operation. Call its
// Extract method to interpret it as a Consumer.
type CreateConsumerResult struct {
consumerResult
}
// UpdateConsumerResult is the response from a Create operation. Call its
// Extract method to interpret it as a Consumer.
type UpdateConsumerResult struct {
consumerResult
}
// DeleteConsumerResult is the response from a Delete operation. Call its
// ExtractErr to determine if the request succeeded or failed.
type DeleteConsumerResult struct {
gophercloud.ErrResult
}
// ConsumersPage is a single page of Region results.
type ConsumersPage struct {
pagination.LinkedPageBase
}
// GetConsumerResult is the response from a Get operation. Call its Extract
// method to interpret it as a Consumer.
type GetConsumerResult struct {
consumerResult
}
// IsEmpty determines whether or not a page of Consumers contains any results.
func (c ConsumersPage) IsEmpty() (bool, error) {
consumers, err := ExtractConsumers(c)
return len(consumers) == 0, err
}
// NextPageURL extracts the "next" link from the links section of the result.
func (c ConsumersPage) NextPageURL() (string, error) {
var s struct {
Links struct {
Next string `json:"next"`
Previous string `json:"previous"`
} `json:"links"`
}
err := c.ExtractInto(&s)
if err != nil {
return "", err
}
return s.Links.Next, err
}
// ExtractConsumers returns a slice of Consumers contained in a single page of
// results.
func ExtractConsumers(r pagination.Page) ([]Consumer, error) {
var s struct {
Consumers []Consumer `json:"consumers"`
}
err := (r.(ConsumersPage)).ExtractInto(&s)
return s.Consumers, err
}
// Extract interprets any consumer result as a Consumer.
func (c consumerResult) Extract() (*Consumer, error) {
var s struct {
Consumer *Consumer `json:"consumer"`
}
err := c.ExtractInto(&s)
return s.Consumer, err
}
// Token contains an OAuth1 token.
type Token struct {
// OAuthToken is the key value for the oauth token that the Identity API returns.
OAuthToken string `q:"oauth_token"`
// OAuthTokenSecret is the secret value associated with the OAuth Token.
OAuthTokenSecret string `q:"oauth_token_secret"`
// OAuthExpiresAt is the date and time when an OAuth token expires.
OAuthExpiresAt *time.Time `q:"-"`
}
// TokenResult is a struct to handle
// "Content-Type: application/x-www-form-urlencoded" response.
type TokenResult struct {
gophercloud.Result
Body []byte
}
// Extract interprets any OAuth1 token result as a Token.
func (r TokenResult) Extract() (*Token, error) {
if r.Err != nil {
return nil, r.Err
}
values, err := url.ParseQuery(string(r.Body))
if err != nil {
return nil, err
}
token := &Token{
OAuthToken: values.Get("oauth_token"),
OAuthTokenSecret: values.Get("oauth_token_secret"),
}
if v := values.Get("oauth_expires_at"); v != "" {
if t, err := time.Parse(gophercloud.RFC3339Milli, v); err != nil {
return nil, err
} else {
token.OAuthExpiresAt = &t
}
}
return token, nil
}
// AuthorizedToken contains an OAuth1 authorized token info.
type AuthorizedToken struct {
// OAuthVerifier is the ID of the token verifier.
OAuthVerifier string `json:"oauth_verifier"`
}
type AuthorizeTokenResult struct {
gophercloud.Result
}
// Extract interprets AuthorizeTokenResult result as a AuthorizedToken.
func (r AuthorizeTokenResult) Extract() (*AuthorizedToken, error) {
var s struct {
AuthorizedToken *AuthorizedToken `json:"token"`
}
err := r.ExtractInto(&s)
return s.AuthorizedToken, err
}
// AccessToken represents an AccessToken response as a struct.
type AccessToken struct {
ID string `json:"id"`
ConsumerID string `json:"consumer_id"`
ProjectID string `json:"project_id"`
AuthorizingUserID string `json:"authorizing_user_id"`
ExpiresAt *time.Time `json:"-"`
}
func (r *AccessToken) UnmarshalJSON(b []byte) error {
type tmp AccessToken
var s struct {
tmp
ExpiresAt *gophercloud.JSONRFC3339Milli `json:"expires_at"`
}
err := json.Unmarshal(b, &s)
if err != nil {
return err
}
*r = AccessToken(s.tmp)
if s.ExpiresAt != nil {
t := time.Time(*s.ExpiresAt)
r.ExpiresAt = &t
}
return nil
}
type GetAccessTokenResult struct {
gophercloud.Result
}
// Extract interprets any GetAccessTokenResult result as an AccessToken.
func (r GetAccessTokenResult) Extract() (*AccessToken, error) {
var s struct {
AccessToken *AccessToken `json:"access_token"`
}
err := r.ExtractInto(&s)
return s.AccessToken, err
}
// RevokeAccessTokenResult is the response from a Delete operation. Call its
// ExtractErr to determine if the request succeeded or failed.
type RevokeAccessTokenResult struct {
gophercloud.ErrResult
}
// AccessTokensPage is a single page of Access Tokens results.
type AccessTokensPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines whether or not a an AccessTokensPage contains any results.
func (r AccessTokensPage) IsEmpty() (bool, error) {
accessTokens, err := ExtractAccessTokens(r)
return len(accessTokens) == 0, err
}
// NextPageURL extracts the "next" link from the links section of the result.
func (r AccessTokensPage) NextPageURL() (string, error) {
var s struct {
Links struct {
Next string `json:"next"`
Previous string `json:"previous"`
} `json:"links"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
return s.Links.Next, err
}
// ExtractAccessTokens returns a slice of AccessTokens contained in a single
// page of results.
func ExtractAccessTokens(r pagination.Page) ([]AccessToken, error) {
var s struct {
AccessTokens []AccessToken `json:"access_tokens"`
}
err := (r.(AccessTokensPage)).ExtractInto(&s)
return s.AccessTokens, err
}
// AccessTokenRole represents an Access Token Role struct.
type AccessTokenRole struct {
ID string `json:"id"`
Name string `json:"name"`
DomainID string `json:"domain_id"`
}
// AccessTokenRolesPage is a single page of Access Token roles results.
type AccessTokenRolesPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines whether or not a an AccessTokensPage contains any results.
func (r AccessTokenRolesPage) IsEmpty() (bool, error) {
accessTokenRoles, err := ExtractAccessTokenRoles(r)
return len(accessTokenRoles) == 0, err
}
// NextPageURL extracts the "next" link from the links section of the result.
func (r AccessTokenRolesPage) NextPageURL() (string, error) {
var s struct {
Links struct {
Next string `json:"next"`
Previous string `json:"previous"`
} `json:"links"`
}
err := r.ExtractInto(&s)
if err != nil {
return "", err
}
return s.Links.Next, err
}
// ExtractAccessTokenRoles returns a slice of AccessTokenRole contained in a
// single page of results.
func ExtractAccessTokenRoles(r pagination.Page) ([]AccessTokenRole, error) {
var s struct {
AccessTokenRoles []AccessTokenRole `json:"roles"`
}
err := (r.(AccessTokenRolesPage)).ExtractInto(&s)
return s.AccessTokenRoles, err
}
type GetAccessTokenRoleResult struct {
gophercloud.Result
}
// Extract interprets any GetAccessTokenRoleResult result as an AccessTokenRole.
func (r GetAccessTokenRoleResult) Extract() (*AccessTokenRole, error) {
var s struct {
AccessTokenRole *AccessTokenRole `json:"role"`
}
err := r.ExtractInto(&s)
return s.AccessTokenRole, err
}
// OAuth1 is an OAuth1 object, returned in OAuth1 token result.
type OAuth1 struct {
AccessTokenID string `json:"access_token_id"`
ConsumerID string `json:"consumer_id"`
}
// TokenExt represents an extension of the base token result.
type TokenExt struct {
OAuth1 OAuth1 `json:"OS-OAUTH1"`
}

View File

@ -0,0 +1,43 @@
package oauth1
import "github.com/gophercloud/gophercloud"
func consumersURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("OS-OAUTH1", "consumers")
}
func consumerURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("OS-OAUTH1", "consumers", id)
}
func requestTokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("OS-OAUTH1", "request_token")
}
func authorizeTokenURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL("OS-OAUTH1", "authorize", id)
}
func createAccessTokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("OS-OAUTH1", "access_token")
}
func userAccessTokensURL(c *gophercloud.ServiceClient, userID string) string {
return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens")
}
func userAccessTokenURL(c *gophercloud.ServiceClient, userID string, id string) string {
return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id)
}
func userAccessTokenRolesURL(c *gophercloud.ServiceClient, userID string, id string) string {
return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles")
}
func userAccessTokenRoleURL(c *gophercloud.ServiceClient, userID string, id string, roleID string) string {
return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles", roleID)
}
func authURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("auth", "tokens")
}

View File

@ -8,6 +8,7 @@ type Scope struct {
ProjectName string
DomainID string
DomainName string
System bool
}
// AuthOptionsBuilder provides the ability for extensions to add additional
@ -16,6 +17,7 @@ type AuthOptionsBuilder interface {
// ToTokenV3CreateMap assembles the Create request body, returning an error
// if parameters are missing or inconsistent.
ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error)
ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error)
ToTokenV3ScopeMap() (map[string]interface{}, error)
CanReauth() bool
}
@ -36,6 +38,9 @@ type AuthOptions struct {
Password string `json:"password,omitempty"`
// Passcode is used in TOTP authentication method
Passcode string `json:"passcode,omitempty"`
// At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional.
DomainID string `json:"-"`
@ -67,6 +72,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
Username: opts.Username,
UserID: opts.UserID,
Password: opts.Password,
Passcode: opts.Passcode,
DomainID: opts.DomainID,
DomainName: opts.DomainName,
AllowReauth: opts.AllowReauth,
@ -79,7 +85,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
return gophercloudAuthOpts.ToTokenV3CreateMap(scope)
}
// ToTokenV3CreateMap builds a scope request body from AuthOptions.
// ToTokenV3ScopeMap builds a scope request body from AuthOptions.
func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
scope := gophercloud.AuthScope(opts.Scope)
@ -93,10 +99,21 @@ func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
}
func (opts *AuthOptions) CanReauth() bool {
if opts.Passcode != "" {
// cannot reauth using TOTP passcode
return false
}
return opts.AllowReauth
}
func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder
// interface in the v3 tokens package.
func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) {
return nil, nil
}
func subjectTokenHeaders(subjectToken string) map[string]string {
return map[string]string{
"X-Subject-Token": subjectToken,
}
@ -120,30 +137,24 @@ func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResu
resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{
MoreHeaders: map[string]string{"X-Auth-Token": ""},
})
r.Err = err
if resp != nil {
r.Header = resp.Header
}
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get validates and retrieves information about another token.
func Get(c *gophercloud.ServiceClient, token string) (r GetResult) {
resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
MoreHeaders: subjectTokenHeaders(token),
OkCodes: []int{200, 203},
})
if resp != nil {
r.Header = resp.Header
}
r.Err = err
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
resp, err := c.Head(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
MoreHeaders: subjectTokenHeaders(token),
OkCodes: []int{200, 204, 404},
})
if err != nil {
@ -155,8 +166,9 @@ func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) {
_, r.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
resp, err := c.Delete(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(token),
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -109,6 +109,13 @@ func (r CreateResult) ExtractTokenID() (string, error) {
return r.Header.Get("X-Subject-Token"), r.Err
}
// ExtractTokenID implements the gophercloud.AuthResult interface. The returned
// string is the same as the ID field of the Token struct returned from
// ExtractToken().
func (r GetResult) ExtractTokenID() (string, error) {
return r.Header.Get("X-Subject-Token"), r.Err
}
// ExtractServiceCatalog returns the ServiceCatalog that was generated along
// with the user's Token.
func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
@ -144,6 +151,15 @@ func (r commonResult) ExtractProject() (*Project, error) {
return s.Project, err
}
// ExtractDomain returns Domain to which User is authorized.
func (r commonResult) ExtractDomain() (*Domain, error) {
var s struct {
Domain *Domain `json:"domain"`
}
err := r.ExtractInto(&s)
return s.Domain, 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.

View File

@ -206,19 +206,22 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err
return r
}
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}})
resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete implements image delete request.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil)
resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get implements image get request.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil)
resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -229,10 +232,11 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err
return r
}
_, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -335,6 +339,20 @@ func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} {
}
}
// ReplaceImageMinRam represents an updated min_ram property request.
type ReplaceImageMinRam struct {
NewMinRam int
}
// ToImagePatchMap assembles a request body based on ReplaceImageTags.
func (r ReplaceImageMinRam) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/min_ram",
"value": r.NewMinRam,
}
}
// UpdateOp represents a valid update operation.
type UpdateOp string
@ -358,7 +376,7 @@ func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} {
"path": fmt.Sprintf("/%s", r.Name),
}
if r.Value != "" {
if r.Op != RemoveOp {
updateMap["value"] = r.Value
}

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"reflect"
"strings"
"time"
"github.com/gophercloud/gophercloud"
@ -86,13 +87,22 @@ type Image struct {
// VirtualSize is the virtual size of the image
VirtualSize int64 `json:"virtual_size"`
// OpenStackImageImportMethods is a slice listing the types of import
// methods available in the cloud.
OpenStackImageImportMethods []string `json:"-"`
// OpenStackImageStoreIDs is a slice listing the store IDs available in
// the cloud.
OpenStackImageStoreIDs []string `json:"-"`
}
func (r *Image) UnmarshalJSON(b []byte) error {
type tmp Image
var s struct {
tmp
SizeBytes interface{} `json:"size"`
SizeBytes interface{} `json:"size"`
OpenStackImageImportMethods string `json:"openstack-image-import-methods"`
OpenStackImageStoreIDs string `json:"openstack-image-store-ids"`
}
err := json.Unmarshal(b, &s)
if err != nil {
@ -120,9 +130,18 @@ func (r *Image) UnmarshalJSON(b []byte) error {
if resultMap, ok := result.(map[string]interface{}); ok {
delete(resultMap, "self")
delete(resultMap, "size")
delete(resultMap, "openstack-image-import-methods")
delete(resultMap, "openstack-image-store-ids")
r.Properties = internal.RemainingKeys(Image{}, resultMap)
}
if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageImportMethods), splitFunc); len(v) > 0 {
r.OpenStackImageImportMethods = v
}
if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageStoreIDs), splitFunc); len(v) > 0 {
r.OpenStackImageStoreIDs = v
}
return err
}
@ -133,6 +152,20 @@ type commonResult struct {
// Extract interprets any commonResult as an Image.
func (r commonResult) Extract() (*Image, error) {
var s *Image
if v, ok := r.Body.(map[string]interface{}); ok {
for k, h := range r.Header {
if strings.ToLower(k) == "openstack-image-import-methods" {
for _, s := range h {
v["openstack-image-import-methods"] = s
}
}
if strings.ToLower(k) == "openstack-image-store-ids" {
for _, s := range h {
v["openstack-image-store-ids"] = s
}
}
}
}
err := r.ExtractInto(&s)
return s, err
}
@ -200,3 +233,8 @@ func ExtractImages(r pagination.Page) ([]Image, error) {
err := (r.(ImagePage)).ExtractInto(&s)
return s.Images, err
}
// splitFunc is a helper function used to avoid a slice of empty strings.
func splitFunc(c rune) bool {
return c == ','
}

View File

@ -35,6 +35,10 @@ const (
// ImageStatusDeactivated denotes that access to image data is not allowed to
// any non-admin user.
ImageStatusDeactivated ImageStatus = "deactivated"
// ImageStatusImporting denotes that an import call has been made but that
// the image is not yet ready for use.
ImageStatusImporting ImageStatus = "importing"
)
// ImageVisibility denotes an image that is fully available in Glance.

View File

@ -25,9 +25,10 @@ import (
*/
func Create(client *gophercloud.ServiceClient, id string, member string) (r CreateResult) {
b := map[string]interface{}{"member": member}
_, r.Err = client.Post(createMemberURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := client.Post(createMemberURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -40,13 +41,15 @@ func List(client *gophercloud.ServiceClient, id string) pagination.Pager {
// Get image member details.
func Get(client *gophercloud.ServiceClient, imageID string, memberID string) (r DetailsResult) {
_, r.Err = client.Get(getMemberURL(client, imageID, memberID), &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
resp, err := client.Get(getMemberURL(client, imageID, memberID), &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete membership for given image. Callee should be image owner.
func Delete(client *gophercloud.ServiceClient, imageID string, memberID string) (r DeleteResult) {
_, r.Err = client.Delete(deleteMemberURL(client, imageID, memberID), &gophercloud.RequestOpts{OkCodes: []int{204}})
resp, err := client.Delete(deleteMemberURL(client, imageID, memberID), &gophercloud.RequestOpts{OkCodes: []int{204}})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -75,7 +78,8 @@ func Update(client *gophercloud.ServiceClient, imageID string, memberID string,
r.Err = err
return
}
_, r.Err = client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body,
resp, err := client.Put(updateMemberURL(client, imageID, memberID), b, &r.Body,
&gophercloud.RequestOpts{OkCodes: []int{200}})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -116,13 +116,15 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
r.Err = err
return
}
_, r.Err = c.Post(rootURL(c), b, &r.Body, nil)
resp, err := c.Post(rootURL(c), b, &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Get retrieves a particular floating IP resource based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = c.Get(resourceURL(c, id), &r.Body, nil)
resp, err := c.Get(resourceURL(c, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -167,9 +169,10 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r
r.Err = err
return
}
_, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := c.Put(resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -177,6 +180,7 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r
// ensure this is what you want - you can also disassociate the IP from existing
// internal ports.
func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = c.Delete(resourceURL(c, id), nil)
resp, err := c.Delete(resourceURL(c, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}

View File

@ -1,6 +1,9 @@
package floatingips
import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@ -37,6 +40,11 @@ type FloatingIP struct {
// specify a project identifier other than its own.
TenantID string `json:"tenant_id"`
// UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of
// the floating ip last changed, and when it was created.
UpdatedAt time.Time `json:"-"`
CreatedAt time.Time `json:"-"`
// ProjectID is the project owner of the floating IP.
ProjectID string `json:"project_id"`
@ -50,6 +58,44 @@ type FloatingIP struct {
Tags []string `json:"tags"`
}
func (r *FloatingIP) UnmarshalJSON(b []byte) error {
type tmp FloatingIP
// Support for older neutron time format
var s1 struct {
tmp
CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"`
UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"`
}
err := json.Unmarshal(b, &s1)
if err == nil {
*r = FloatingIP(s1.tmp)
r.CreatedAt = time.Time(s1.CreatedAt)
r.UpdatedAt = time.Time(s1.UpdatedAt)
return nil
}
// Support for newer neutron time format
var s2 struct {
tmp
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err = json.Unmarshal(b, &s2)
if err != nil {
return err
}
*r = FloatingIP(s2.tmp)
r.CreatedAt = time.Time(s2.CreatedAt)
r.UpdatedAt = time.Time(s2.UpdatedAt)
return nil
}
type commonResult struct {
gophercloud.Result
}

View File

@ -60,7 +60,8 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
// Get retrieves a specific network based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
resp, err := c.Get(getURL(c, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -99,7 +100,8 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
r.Err = err
return
}
_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
resp, err := c.Post(createURL(c), b, &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -130,51 +132,16 @@ func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuild
r.Err = err
return
}
_, r.Err = c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{
resp, err := c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete accepts a unique ID and deletes the network associated with it.
func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) {
_, r.Err = c.Delete(deleteURL(c, networkID), nil)
resp, err := c.Delete(deleteURL(c, networkID), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// IDFromName is a convenience function that returns a network's ID, given
// its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
listOpts := ListOpts{
Name: name,
}
pages, err := List(client, listOpts).AllPages()
if err != nil {
return "", err
}
all, err := ExtractNetworks(pages)
if err != nil {
return "", err
}
for _, s := range all {
if s.Name == name {
count++
id = s.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "network"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "network"}
}
}

View File

@ -1,6 +1,9 @@
package networks
import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
@ -70,6 +73,11 @@ type Network struct {
// TenantID is the project owner of the network.
TenantID string `json:"tenant_id"`
// UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the
// network last changed, and when it was created.
UpdatedAt time.Time `json:"-"`
CreatedAt time.Time `json:"-"`
// ProjectID is the project owner of the network.
ProjectID string `json:"project_id"`
@ -84,6 +92,44 @@ type Network struct {
Tags []string `json:"tags"`
}
func (r *Network) UnmarshalJSON(b []byte) error {
type tmp Network
// Support for older neutron time format
var s1 struct {
tmp
CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"`
UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"`
}
err := json.Unmarshal(b, &s1)
if err == nil {
*r = Network(s1.tmp)
r.CreatedAt = time.Time(s1.CreatedAt)
r.UpdatedAt = time.Time(s1.UpdatedAt)
return nil
}
// Support for newer neutron time format
var s2 struct {
tmp
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
err = json.Unmarshal(b, &s2)
if err != nil {
return err
}
*r = Network(s2.tmp)
r.CreatedAt = time.Time(s2.CreatedAt)
r.UpdatedAt = time.Time(s2.UpdatedAt)
return nil
}
// NetworkPage is the page returned by a pager when traversing over a
// collection of networks.
type NetworkPage struct {

View File

@ -69,7 +69,8 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
// Get retrieves a specific subnet based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = c.Get(getURL(c, id), &r.Body, nil)
resp, err := c.Get(getURL(c, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -160,7 +161,8 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
r.Err = err
return
}
_, r.Err = c.Post(createURL(c), b, &r.Body, nil)
resp, err := c.Post(createURL(c), b, &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
@ -219,51 +221,16 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r
r.Err = err
return
}
_, r.Err = c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
resp, err := c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// Delete accepts a unique ID and deletes the subnet associated with it.
func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = c.Delete(deleteURL(c, id), nil)
resp, err := c.Delete(deleteURL(c, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return
}
// IDFromName is a convenience function that returns a subnet's ID,
// given its name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
listOpts := ListOpts{
Name: name,
}
pages, err := List(client, listOpts).AllPages()
if err != nil {
return "", err
}
all, err := ExtractSubnets(pages)
if err != nil {
return "", err
}
for _, s := range all {
if s.Name == name {
count++
id = s.ID
}
}
switch count {
case 0:
return "", gophercloud.ErrResourceNotFound{Name: name, ResourceType: "subnet"}
case 1:
return id, nil
default:
return "", gophercloud.ErrMultipleResourcesFound{Name: name, Count: count, ResourceType: "subnet"}
}
}

View File

@ -54,7 +54,8 @@ func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
// Request performs an HTTP request and extracts the http.Response from the result.
func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) {
return client.Get(url, nil, &gophercloud.RequestOpts{
MoreHeaders: headers,
OkCodes: []int{200, 204, 300},
MoreHeaders: headers,
OkCodes: []int{200, 204, 300},
KeepResponseBody: true,
})
}

View File

@ -450,6 +450,8 @@ func BuildHeaders(opts interface{}) (map[string]string, error) {
optsMap[tags[0]] = v.String()
case reflect.Int:
optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
case reflect.Int64:
optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
case reflect.Bool:
optsMap[tags[0]] = strconv.FormatBool(v.Bool())
}

View File

@ -94,9 +94,32 @@ type ProviderClient struct {
// reauthlock represents a set of attributes used to help in the reauthentication process.
type reauthlock struct {
sync.RWMutex
reauthing bool
reauthingErr error
done *sync.Cond
ongoing *reauthFuture
}
// reauthFuture represents future result of the reauthentication process.
// while done channel is not closed, reauthentication is in progress.
// when done channel is closed, err contains the result of reauthentication.
type reauthFuture struct {
done chan struct{}
err error
}
func newReauthFuture() *reauthFuture {
return &reauthFuture{
make(chan struct{}),
nil,
}
}
func (f *reauthFuture) Set(err error) {
f.err = err
close(f.done)
}
func (f *reauthFuture) Get() error {
<-f.done
return f.err
}
// AuthenticatedHeaders returns a map of HTTP headers that are common for all
@ -106,11 +129,13 @@ func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
return
}
if client.reauthmut != nil {
// If a Reauthenticate is in progress, wait for it to complete.
client.reauthmut.Lock()
for client.reauthmut.reauthing {
client.reauthmut.done.Wait()
}
ongoing := client.reauthmut.ongoing
client.reauthmut.Unlock()
if ongoing != nil {
_ = ongoing.Get()
}
}
t := client.Token()
if t == "" {
@ -223,7 +248,7 @@ func (client *ProviderClient) SetThrowaway(v bool) {
// 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) {
func (client *ProviderClient) Reauthenticate(previousToken string) error {
if client.ReauthFunc == nil {
return nil
}
@ -232,33 +257,36 @@ func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
return client.ReauthFunc()
}
future := newReauthFuture()
// Check if a Reauthenticate is in progress, or start one if not.
client.reauthmut.Lock()
if client.reauthmut.reauthing {
for !client.reauthmut.reauthing {
client.reauthmut.done.Wait()
}
err = client.reauthmut.reauthingErr
client.reauthmut.Unlock()
return err
ongoing := client.reauthmut.ongoing
if ongoing == nil {
client.reauthmut.ongoing = future
}
client.reauthmut.Unlock()
client.reauthmut.Lock()
client.reauthmut.reauthing = true
client.reauthmut.done = sync.NewCond(client.reauthmut)
client.reauthmut.reauthingErr = nil
client.reauthmut.Unlock()
// If Reauthenticate is running elsewhere, wait for its result.
if ongoing != nil {
return ongoing.Get()
}
// Perform the actual reauthentication.
var err error
if previousToken == "" || client.TokenID == previousToken {
err = client.ReauthFunc()
} else {
err = nil
}
// Mark Reauthenticate as finished.
client.reauthmut.Lock()
client.reauthmut.reauthing = false
client.reauthmut.reauthingErr = err
client.reauthmut.done.Broadcast()
client.reauthmut.ongoing.Set(err)
client.reauthmut.ongoing = nil
client.reauthmut.Unlock()
return
return err
}
// RequestOpts customizes the behavior of the provider.Request() method.
@ -283,6 +311,18 @@ type RequestOpts struct {
// ErrorContext specifies the resource error type to return if an error is encountered.
// This lets resources override default error messages based on the response status code.
ErrorContext error
// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
// response body is considered for further use. Valid when JSONResponse is nil.
KeepResponseBody bool
}
// requestState contains temporary state for a single ProviderClient.Request() call.
type requestState struct {
// This flag indicates if we have reauthenticated during this request because of a 401 response.
// It ensures that we don't reauthenticate multiple times for a single request. If we
// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
// will just get us into an infinite loop.
hasReauthenticated bool
}
var applicationJSON = "application/json"
@ -290,6 +330,12 @@ var applicationJSON = "application/json"
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
return client.doRequest(method, url, options, &requestState{
hasReauthenticated: false,
})
}
func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
var body io.Reader
var contentType *string
@ -309,6 +355,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
contentType = &applicationJSON
}
// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
if options.KeepResponseBody && options.JSONResponse != nil {
return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
}
if options.RawBody != nil {
body = options.RawBody
}
@ -347,9 +398,6 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
req.Header.Set(k, v)
}
// Set connection parameter to close the connection immediately when we've got the response
req.Close = true
prereqtok := req.Header.Get("X-Auth-Token")
// Issue the request.
@ -377,11 +425,12 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
respErr := ErrUnexpectedResponseCode{
URL: url,
Method: method,
Expected: options.OkCodes,
Actual: resp.StatusCode,
Body: body,
URL: url,
Method: method,
Expected: options.OkCodes,
Actual: resp.StatusCode,
Body: body,
ResponseHeader: resp.Header,
}
errType := options.ErrorContext
@ -392,7 +441,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
err = error400er.Error400(respErr)
}
case http.StatusUnauthorized:
if client.ReauthFunc != nil {
if client.ReauthFunc != nil && !state.hasReauthenticated {
err = client.Reauthenticate(prereqtok)
if err != nil {
e := &ErrUnableToReauthenticate{}
@ -404,7 +453,8 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
seeker.Seek(0, 0)
}
}
resp, err = client.Request(method, url, options)
state.hasReauthenticated = true
resp, err = client.doRequest(method, url, options, state)
if err != nil {
switch err.(type) {
case *ErrUnexpectedResponseCode:
@ -475,25 +525,40 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
// Parse the response body as JSON, if requested to do so.
if options.JSONResponse != nil {
defer resp.Body.Close()
// Don't decode JSON when there is no content
if resp.StatusCode == http.StatusNoContent {
// read till EOF, otherwise the connection will be closed and cannot be reused
_, err = io.Copy(ioutil.Discard, resp.Body)
return resp, err
}
if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
return nil, err
}
}
// Close unused body to allow the HTTP connection to be reused
if !options.KeepResponseBody && options.JSONResponse == nil {
defer resp.Body.Close()
// read till EOF, otherwise the connection will be closed and cannot be reused
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
return nil, err
}
}
return resp, nil
}
func defaultOkCodes(method string) []int {
switch {
case method == "GET":
switch method {
case "GET", "HEAD":
return []int{200}
case method == "POST":
case "POST":
return []int{201, 202}
case method == "PUT":
case "PUT":
return []int{201, 202}
case method == "PATCH":
case "PATCH":
return []int{200, 202, 204}
case method == "DELETE":
case "DELETE":
return []int{202, 204}
}

View File

@ -131,6 +131,18 @@ func (r Result) extractIntoPtr(to interface{}, label string) error {
// fields of the struct or composed extension struct
// at the end of this method.
toValue.Set(newSlice)
// jtopjian: This was put into place to resolve the issue
// described at
// https://github.com/gophercloud/gophercloud/issues/1963
//
// This probably isn't the best fix, but it appears to
// be resolving the issue, so I'm going to implement it
// for now.
//
// For future readers, this entire case statement could
// use a review.
return nil
}
}
case reflect.Struct:

View File

@ -152,3 +152,11 @@ func (client *ServiceClient) Request(method, url string, options *RequestOpts) (
}
return client.ProviderClient.Request(method, url, options)
}
// ParseResponse is a helper function to parse http.Response to constituents.
func ParseResponse(resp *http.Response, err error) (io.ReadCloser, http.Header, error) {
if resp != nil {
return resp.Body, resp.Header, err
}
return nil, nil, err
}

11
vendor/github.com/gophercloud/utils/env/env.go generated vendored Normal file
View File

@ -0,0 +1,11 @@
// +build !windows
package env
import (
"os"
)
func Getenv(s string) string {
return os.Getenv(s)
}

106
vendor/github.com/gophercloud/utils/env/env_windows.go generated vendored Normal file
View File

@ -0,0 +1,106 @@
package env
import (
"os"
"syscall"
"golang.org/x/sys/windows"
"golang.org/x/text/encoding/charmap"
)
func Getenv(s string) string {
var st uint32
env := os.Getenv(s)
if windows.GetConsoleMode(windows.Handle(syscall.Stdin), &st) == nil ||
windows.GetConsoleMode(windows.Handle(syscall.Stdout), &st) == nil ||
windows.GetConsoleMode(windows.Handle(syscall.Stderr), &st) == nil {
// detect windows console, should be skipped in cygwin environment
var cm charmap.Charmap
switch windows.GetACP() {
case 37:
cm = *charmap.CodePage037
case 1047:
cm = *charmap.CodePage1047
case 1140:
cm = *charmap.CodePage1140
case 437:
cm = *charmap.CodePage437
case 850:
cm = *charmap.CodePage850
case 852:
cm = *charmap.CodePage852
case 855:
cm = *charmap.CodePage855
case 858:
cm = *charmap.CodePage858
case 860:
cm = *charmap.CodePage860
case 862:
cm = *charmap.CodePage862
case 863:
cm = *charmap.CodePage863
case 865:
cm = *charmap.CodePage865
case 866:
cm = *charmap.CodePage866
case 28591:
cm = *charmap.ISO8859_1
case 28592:
cm = *charmap.ISO8859_2
case 28593:
cm = *charmap.ISO8859_3
case 28594:
cm = *charmap.ISO8859_4
case 28595:
cm = *charmap.ISO8859_5
case 28596:
cm = *charmap.ISO8859_6
case 28597:
cm = *charmap.ISO8859_7
case 28598:
cm = *charmap.ISO8859_8
case 28599:
cm = *charmap.ISO8859_9
case 28600:
cm = *charmap.ISO8859_10
case 28603:
cm = *charmap.ISO8859_13
case 28604:
cm = *charmap.ISO8859_14
case 28605:
cm = *charmap.ISO8859_15
case 28606:
cm = *charmap.ISO8859_16
case 20866:
cm = *charmap.KOI8R
case 21866:
cm = *charmap.KOI8U
case 1250:
cm = *charmap.Windows1250
case 1251:
cm = *charmap.Windows1251
case 1252:
cm = *charmap.Windows1252
case 1253:
cm = *charmap.Windows1253
case 1254:
cm = *charmap.Windows1254
case 1255:
cm = *charmap.Windows1255
case 1256:
cm = *charmap.Windows1256
case 1257:
cm = *charmap.Windows1257
case 1258:
cm = *charmap.Windows1258
case 874:
cm = *charmap.Windows874
default:
return env
}
if v, err := cm.NewEncoder().String(env); err == nil {
return v
}
}
return env
}

View File

@ -7,7 +7,7 @@ See https://docs.openstack.org/os-client-config/latest for details.
Example to Create a Provider Client From clouds.yaml
opts := &clientconfig.ClientOpts{
Name: "hawaii",
Cloud: "hawaii",
}
pClient, err := clientconfig.AuthenticatedClient(opts)

View File

@ -2,12 +2,13 @@ package clientconfig
import (
"fmt"
"os"
"net/http"
"reflect"
"strings"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/utils/env"
yaml "gopkg.in/yaml.v2"
)
@ -56,11 +57,62 @@ type ClientOpts struct {
// This will override a region in clouds.yaml or can be used
// when authenticating directly with AuthInfo.
RegionName string
// EndpointType specifies whether to use the public, internal, or
// admin endpoint of a service.
EndpointType string
// HTTPClient provides the ability customize the ProviderClient's
// internal HTTP client.
HTTPClient *http.Client
// YAMLOpts provides the ability to pass a customized set
// of options and methods for loading the YAML file.
// It takes a YAMLOptsBuilder interface that is defined
// in this file. This is optional and the default behavior
// is to call the local LoadCloudsYAML functions defined
// in this file.
YAMLOpts YAMLOptsBuilder
}
// YAMLOptsBuilder defines an interface for customization when
// loading a clouds.yaml file.
type YAMLOptsBuilder interface {
LoadCloudsYAML() (map[string]Cloud, error)
LoadSecureCloudsYAML() (map[string]Cloud, error)
LoadPublicCloudsYAML() (map[string]Cloud, error)
}
// YAMLOpts represents options and methods to load a clouds.yaml file.
type YAMLOpts struct {
// By default, no options are specified.
}
// LoadCloudsYAML defines how to load a clouds.yaml file.
// By default, this calls the local LoadCloudsYAML function.
func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) {
return LoadCloudsYAML()
}
// LoadSecureCloudsYAML defines how to load a secure.yaml file.
// By default, this calls the local LoadSecureCloudsYAML function.
func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) {
return LoadSecureCloudsYAML()
}
// LoadPublicCloudsYAML defines how to load a public-secure.yaml file.
// By default, this calls the local LoadPublicCloudsYAML function.
func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) {
return LoadPublicCloudsYAML()
}
// LoadCloudsYAML will load a clouds.yaml file and return the full config.
// This is called by the YAMLOpts method. Calling this function directly
// is supported for now but has only been retained for backwards
// compatibility from before YAMLOpts was defined. This may be removed in
// the future.
func LoadCloudsYAML() (map[string]Cloud, error) {
content, err := findAndReadCloudsYAML()
_, content, err := FindAndReadCloudsYAML()
if err != nil {
return nil, err
}
@ -75,10 +127,14 @@ func LoadCloudsYAML() (map[string]Cloud, error) {
}
// LoadSecureCloudsYAML will load a secure.yaml file and return the full config.
// This is called by the YAMLOpts method. Calling this function directly
// is supported for now but has only been retained for backwards
// compatibility from before YAMLOpts was defined. This may be removed in
// the future.
func LoadSecureCloudsYAML() (map[string]Cloud, error) {
var secureClouds Clouds
content, err := findAndReadSecureCloudsYAML()
_, content, err := FindAndReadSecureCloudsYAML()
if err != nil {
if err.Error() == "no secure.yaml file found" {
// secure.yaml is optional so just ignore read error
@ -96,10 +152,14 @@ func LoadSecureCloudsYAML() (map[string]Cloud, error) {
}
// LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config.
// This is called by the YAMLOpts method. Calling this function directly
// is supported for now but has only been retained for backwards
// compatibility from before YAMLOpts was defined. This may be removed in
// the future.
func LoadPublicCloudsYAML() (map[string]Cloud, error) {
var publicClouds PublicClouds
content, err := findAndReadPublicCloudsYAML()
_, content, err := FindAndReadPublicCloudsYAML()
if err != nil {
if err.Error() == "no clouds-public.yaml file found" {
// clouds-public.yaml is optional so just ignore read error
@ -119,7 +179,13 @@ func LoadPublicCloudsYAML() (map[string]Cloud, error) {
// GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
clouds, err := LoadCloudsYAML()
if opts.YAMLOpts == nil {
opts.YAMLOpts = new(YAMLOpts)
}
yamlOpts := opts.YAMLOpts
clouds, err := yamlOpts.LoadCloudsYAML()
if err != nil {
return nil, fmt.Errorf("unable to load clouds.yaml: %s", err)
}
@ -138,7 +204,7 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "CLOUD"); v != "" {
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v
}
@ -159,32 +225,33 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
}
}
var cloudIsInCloudsYaml bool
if cloud == nil {
// not an immediate error as it might still be defined in secure.yaml
cloudIsInCloudsYaml = false
} else {
cloudIsInCloudsYaml = true
}
if cloud != nil {
// A profile points to a public cloud entry.
// If one was specified, load a list of public clouds
// and then merge the information with the current cloud data.
profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud)
publicClouds, err := LoadPublicCloudsYAML()
if err != nil {
return nil, fmt.Errorf("unable to load clouds-public.yaml: %s", err)
}
if profileName != "" {
publicClouds, err := yamlOpts.LoadPublicCloudsYAML()
if err != nil {
return nil, fmt.Errorf("unable to load clouds-public.yaml: %s", err)
}
var profileName = defaultIfEmpty(cloud.Profile, cloud.Cloud)
if profileName != "" {
publicCloud, ok := publicClouds[profileName]
if !ok {
return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
}
cloud, err = mergeClouds(cloud, publicCloud)
if err != nil {
return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName)
publicCloud, ok := publicClouds[profileName]
if !ok {
return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
}
cloud, err = mergeClouds(cloud, publicCloud)
if err != nil {
return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName)
}
}
}
secureClouds, err := LoadSecureCloudsYAML()
// Next, load a secure clouds file and see if a cloud entry
// can be found or merged.
secureClouds, err := yamlOpts.LoadSecureCloudsYAML()
if err != nil {
return nil, fmt.Errorf("unable to load secure.yaml: %s", err)
}
@ -192,12 +259,13 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
if secureClouds != nil {
// If no entry was found in clouds.yaml, no cloud name was specified,
// and only one secureCloud entry exists, use that as the cloud entry.
if !cloudIsInCloudsYaml && cloudName == "" && len(secureClouds) == 1 {
if cloud == nil && cloudName == "" && len(secureClouds) == 1 {
for _, v := range secureClouds {
cloud = &v
}
}
// Otherwise, see if the provided cloud name exists in the secure yaml file.
secureCloud, ok := secureClouds[cloudName]
if !ok && cloud == nil {
// cloud == nil serves two purposes here:
@ -217,6 +285,12 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
}
}
// As an extra precaution, do one final check to see if cloud is nil.
// We shouldn't reach this point, though.
if cloud == nil {
return nil, fmt.Errorf("Could not find cloud %s", cloudName)
}
// Default is to verify SSL API requests
if cloud.Verify == nil {
iTrue := true
@ -227,6 +301,17 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
// clouds-public.yml
// https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors
// Both Interface and EndpointType are valid settings in clouds.yaml,
// but we want to standardize on EndpointType for simplicity.
//
// If only Interface was set, we copy that to EndpointType to use as the setting.
// But in all other cases, EndpointType is used and Interface is cleared.
if cloud.Interface != "" && cloud.EndpointType == "" {
cloud.EndpointType = cloud.Interface
}
cloud.Interface = ""
return cloud, nil
}
@ -260,7 +345,7 @@ func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "CLOUD"); v != "" {
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v
}
@ -310,7 +395,7 @@ func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
identityAPI = v
}
@ -359,49 +444,49 @@ func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
}
if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
if v := os.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
}
if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" {
if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v
}
}
if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" {
if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v
}
}
if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" {
if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
if v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
}
if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
if v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
}
@ -427,115 +512,115 @@ func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
}
if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v
}
}
if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" {
if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
if v := os.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
cloud.AuthInfo.Token = v
}
}
if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" {
if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v
}
}
if cloud.AuthInfo.UserID == "" {
if v := os.Getenv(envPrefix + "USER_ID"); v != "" {
if v := env.Getenv(envPrefix + "USER_ID"); v != "" {
cloud.AuthInfo.UserID = v
}
}
if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" {
if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v
}
}
if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" {
if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
if v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
cloud.AuthInfo.ProjectID = v
}
}
if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
if v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = v
}
}
if cloud.AuthInfo.DomainID == "" {
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
cloud.AuthInfo.DomainID = v
}
}
if cloud.AuthInfo.DomainName == "" {
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
cloud.AuthInfo.DomainName = v
}
}
if cloud.AuthInfo.DefaultDomain == "" {
if v := os.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
cloud.AuthInfo.DefaultDomain = v
}
}
if cloud.AuthInfo.ProjectDomainID == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
cloud.AuthInfo.ProjectDomainID = v
}
}
if cloud.AuthInfo.ProjectDomainName == "" {
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.ProjectDomainName = v
}
}
if cloud.AuthInfo.UserDomainID == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
cloud.AuthInfo.UserDomainID = v
}
}
if cloud.AuthInfo.UserDomainName == "" {
if v := os.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
cloud.AuthInfo.UserDomainName = v
}
}
if cloud.AuthInfo.ApplicationCredentialID == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
cloud.AuthInfo.ApplicationCredentialID = v
}
}
if cloud.AuthInfo.ApplicationCredentialName == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
cloud.AuthInfo.ApplicationCredentialName = v
}
}
if cloud.AuthInfo.ApplicationCredentialSecret == "" {
if v := os.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" {
if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" {
cloud.AuthInfo.ApplicationCredentialSecret = v
}
}
@ -545,7 +630,11 @@ func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
scope := new(gophercloud.AuthScope)
// Application credentials don't support scope
if !isApplicationCredential(cloud.AuthInfo) {
if isApplicationCredential(cloud.AuthInfo) {
// If Domain* is set, but UserDomain* or ProjectDomain* aren't,
// then use Domain* as the default setting.
cloud = setDomainIfNeeded(cloud)
} else {
if !isProjectScoped(cloud.AuthInfo) {
if cloud.AuthInfo.DomainID != "" {
scope.DomainID = cloud.AuthInfo.DomainID
@ -639,7 +728,7 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
envPrefix = opts.EnvPrefix
}
if v := os.Getenv(envPrefix + "CLOUD"); v != "" {
if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v
}
@ -659,10 +748,15 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
return nil, err
}
// If an HTTPClient was specified, use it.
if opts.HTTPClient != nil {
pClient.HTTPClient = *opts.HTTPClient
}
// Determine the region to use.
// First, check if the REGION_NAME environment variable is set.
var region string
if v := os.Getenv(envPrefix + "REGION_NAME"); v != "" {
if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" {
region = v
}
@ -677,8 +771,27 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
region = v
}
// Determine the endpoint type to use.
// First, check if the OS_INTERFACE environment variable is set.
var endpointType string
if v := env.Getenv(envPrefix + "INTERFACE"); v != "" {
endpointType = v
}
// Next, check if the cloud entry sets an endpoint type.
if v := cloud.EndpointType; v != "" {
endpointType = v
}
// Finally, see if one was specified in the ClientOpts.
// If so, this takes precedence.
if v := opts.EndpointType; v != "" {
endpointType = v
}
eo := gophercloud.EndpointOpts{
Region: region,
Region: region,
Availability: GetEndpointType(endpointType),
}
switch service {
@ -688,6 +801,8 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
return openstack.NewComputeV2(pClient, eo)
case "container":
return openstack.NewContainerV1(pClient, eo)
case "container-infra":
return openstack.NewContainerInfraV1(pClient, eo)
case "database":
return openstack.NewDBV1(pClient, eo)
case "dns":

View File

@ -4,118 +4,124 @@ package clientconfig
// The format of the clouds-public.yml is documented at
// https://docs.openstack.org/python-openstackclient/latest/configuration/
type PublicClouds struct {
Clouds map[string]Cloud `yaml:"public-clouds"`
Clouds map[string]Cloud `yaml:"public-clouds" json:"public-clouds"`
}
// Clouds represents a collection of Cloud entries in a clouds.yaml file.
// The format of clouds.yaml is documented at
// https://docs.openstack.org/os-client-config/latest/user/configuration.html.
type Clouds struct {
Clouds map[string]Cloud `yaml:"clouds"`
Clouds map[string]Cloud `yaml:"clouds" json:"clouds"`
}
// Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file.
type Cloud struct {
Cloud string `yaml:"cloud"`
Profile string `yaml:"profile"`
AuthInfo *AuthInfo `yaml:"auth"`
AuthType AuthType `yaml:"auth_type"`
RegionName string `yaml:"region_name"`
Regions []interface{} `yaml:"regions"`
Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"`
Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"`
AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"`
RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"`
Regions []interface{} `yaml:"regions,omitempty" json:"regions,omitempty"`
// EndpointType and Interface both specify whether to use the public, internal,
// or admin interface of a service. They should be considered synonymous, but
// EndpointType will take precedence when both are specified.
EndpointType string `yaml:"endpoint_type,omitempty" json:"endpoint_type,omitempty"`
Interface string `yaml:"interface,omitempty" json:"interface,omitempty"`
// API Version overrides.
IdentityAPIVersion string `yaml:"identity_api_version"`
VolumeAPIVersion string `yaml:"volume_api_version"`
IdentityAPIVersion string `yaml:"identity_api_version,omitempty" json:"identity_api_version,omitempty"`
VolumeAPIVersion string `yaml:"volume_api_version,omitempty" json:"volume_api_version,omitempty"`
// Verify whether or not SSL API requests should be verified.
Verify *bool `yaml:"verify"`
Verify *bool `yaml:"verify,omitempty" json:"verify,omitempty"`
// CACertFile a path to a CA Cert bundle that can be used as part of
// verifying SSL API requests.
CACertFile string `yaml:"cacert"`
CACertFile string `yaml:"cacert,omitempty" json:"cacert,omitempty"`
// ClientCertFile a path to a client certificate to use as part of the SSL
// transaction.
ClientCertFile string `yaml:"cert"`
ClientCertFile string `yaml:"cert,omitempty" json:"cert,omitempty"`
// ClientKeyFile a path to a client key to use as part of the SSL
// transaction.
ClientKeyFile string `yaml:"key"`
ClientKeyFile string `yaml:"key,omitempty" json:"key,omitempty"`
}
// AuthInfo represents the auth section of a cloud entry or
// auth options entered explicitly in ClientOpts.
type AuthInfo struct {
// AuthURL is the keystone/identity endpoint URL.
AuthURL string `yaml:"auth_url"`
AuthURL string `yaml:"auth_url,omitempty" json:"auth_url,omitempty"`
// Token is a pre-generated authentication token.
Token string `yaml:"token"`
Token string `yaml:"token,omitempty" json:"token,omitempty"`
// Username is the username of the user.
Username string `yaml:"username"`
Username string `yaml:"username,omitempty" json:"username,omitempty"`
// UserID is the unique ID of a user.
UserID string `yaml:"user_id"`
UserID string `yaml:"user_id,omitempty" json:"user_id,omitempty"`
// Password is the password of the user.
Password string `yaml:"password"`
Password string `yaml:"password,omitempty" json:"password,omitempty"`
// Application Credential ID to login with.
ApplicationCredentialID string `yaml:"application_credential_id"`
ApplicationCredentialID string `yaml:"application_credential_id,omitempty" json:"application_credential_id,omitempty"`
// Application Credential name to login with.
ApplicationCredentialName string `yaml:"application_credential_name"`
ApplicationCredentialName string `yaml:"application_credential_name,omitempty" json:"application_credential_name,omitempty"`
// Application Credential secret to login with.
ApplicationCredentialSecret string `yaml:"application_credential_secret"`
ApplicationCredentialSecret string `yaml:"application_credential_secret,omitempty" json:"application_credential_secret,omitempty"`
// ProjectName is the common/human-readable name of a project.
// Users can be scoped to a project.
// ProjectName on its own is not enough to ensure a unique scope. It must
// also be combined with either a ProjectDomainName or ProjectDomainID.
// ProjectName cannot be combined with ProjectID in a scope.
ProjectName string `yaml:"project_name"`
ProjectName string `yaml:"project_name,omitempty" json:"project_name,omitempty"`
// ProjectID is the unique ID of a project.
// It can be used to scope a user to a specific project.
ProjectID string `yaml:"project_id"`
ProjectID string `yaml:"project_id,omitempty" json:"project_id,omitempty"`
// UserDomainName is the name of the domain where a user resides.
// It is used to identify the source domain of a user.
UserDomainName string `yaml:"user_domain_name"`
UserDomainName string `yaml:"user_domain_name,omitempty" json:"user_domain_name,omitempty"`
// UserDomainID is the unique ID of the domain where a user resides.
// It is used to identify the source domain of a user.
UserDomainID string `yaml:"user_domain_id"`
UserDomainID string `yaml:"user_domain_id,omitempty" json:"user_domain_id,omitempty"`
// ProjectDomainName is the name of the domain where a project resides.
// It is used to identify the source domain of a project.
// ProjectDomainName can be used in addition to a ProjectName when scoping
// a user to a specific project.
ProjectDomainName string `yaml:"project_domain_name"`
ProjectDomainName string `yaml:"project_domain_name,omitempty" json:"project_domain_name,omitempty"`
// ProjectDomainID is the name of the domain where a project resides.
// It is used to identify the source domain of a project.
// ProjectDomainID can be used in addition to a ProjectName when scoping
// a user to a specific project.
ProjectDomainID string `yaml:"project_domain_id"`
ProjectDomainID string `yaml:"project_domain_id,omitempty" json:"project_domain_id,omitempty"`
// DomainName is the name of a domain which can be used to identify the
// source domain of either a user or a project.
// If UserDomainName and ProjectDomainName are not specified, then DomainName
// is used as a default choice.
// It can also be used be used to specify a domain-only scope.
DomainName string `yaml:"domain_name"`
DomainName string `yaml:"domain_name,omitempty" json:"domain_name,omitempty"`
// DomainID is the unique ID of a domain which can be used to identify the
// source domain of eitehr a user or a project.
// If UserDomainID and ProjectDomainID are not specified, then DomainID is
// used as a default choice.
// It can also be used be used to specify a domain-only scope.
DomainID string `yaml:"domain_id"`
DomainID string `yaml:"domain_id,omitempty" json:"domain_id,omitempty"`
// DefaultDomain is the domain ID to fall back on if no other domain has
// been specified and a domain is required for scope.
DefaultDomain string `yaml:"default_domain"`
DefaultDomain string `yaml:"default_domain,omitempty" json:"default_domain,omitempty"`
}

View File

@ -8,6 +8,9 @@ import (
"os/user"
"path/filepath"
"reflect"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/utils/env"
)
// defaultIfEmpty is a helper function to make it cleaner to set default value
@ -87,7 +90,7 @@ func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interfa
}
}
// findAndReadCloudsYAML attempts to locate a clouds.yaml file in the following
// FindAndReadCloudsYAML attempts to locate a clouds.yaml file in the following
// locations:
//
// 1. OS_CLIENT_CONFIG_FILE
@ -96,35 +99,37 @@ func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interfa
// 4. unix-specific site_config_dir (/etc/openstack/clouds.yaml)
//
// If found, the contents of the file is returned.
func findAndReadCloudsYAML() ([]byte, error) {
func FindAndReadCloudsYAML() (string, []byte, error) {
// OS_CLIENT_CONFIG_FILE
if v := os.Getenv("OS_CLIENT_CONFIG_FILE"); v != "" {
if v := env.Getenv("OS_CLIENT_CONFIG_FILE"); v != "" {
if ok := fileExists(v); ok {
return ioutil.ReadFile(v)
content, err := ioutil.ReadFile(v)
return v, content, err
}
}
return findAndReadYAML("clouds.yaml")
return FindAndReadYAML("clouds.yaml")
}
func findAndReadPublicCloudsYAML() ([]byte, error) {
return findAndReadYAML("clouds-public.yaml")
func FindAndReadPublicCloudsYAML() (string, []byte, error) {
return FindAndReadYAML("clouds-public.yaml")
}
func findAndReadSecureCloudsYAML() ([]byte, error) {
return findAndReadYAML("secure.yaml")
func FindAndReadSecureCloudsYAML() (string, []byte, error) {
return FindAndReadYAML("secure.yaml")
}
func findAndReadYAML(yamlFile string) ([]byte, error) {
func FindAndReadYAML(yamlFile string) (string, []byte, error) {
// current directory
cwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("unable to determine working directory: %s", err)
return "", nil, fmt.Errorf("unable to determine working directory: %s", err)
}
filename := filepath.Join(cwd, yamlFile)
if ok := fileExists(filename); ok {
return ioutil.ReadFile(filename)
content, err := ioutil.ReadFile(filename)
return filename, content, err
}
// unix user config directory: ~/.config/openstack.
@ -133,17 +138,20 @@ func findAndReadYAML(yamlFile string) ([]byte, error) {
if homeDir != "" {
filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile)
if ok := fileExists(filename); ok {
return ioutil.ReadFile(filename)
content, err := ioutil.ReadFile(filename)
return filename, content, err
}
}
}
// unix-specific site config directory: /etc/openstack.
if ok := fileExists("/etc/openstack/" + yamlFile); ok {
return ioutil.ReadFile("/etc/openstack/" + yamlFile)
filename = "/etc/openstack/" + yamlFile
if ok := fileExists(filename); ok {
content, err := ioutil.ReadFile(filename)
return filename, content, err
}
return nil, fmt.Errorf("no " + yamlFile + " file found")
return "", nil, fmt.Errorf("no " + yamlFile + " file found")
}
// fileExists checks for the existence of a file at a given location.
@ -153,3 +161,15 @@ func fileExists(filename string) bool {
}
return false
}
// GetEndpointType is a helper method to determine the endpoint type
// requested by the user.
func GetEndpointType(endpointType string) gophercloud.Availability {
if endpointType == "internal" || endpointType == "internalURL" {
return gophercloud.AvailabilityInternal
}
if endpointType == "admin" || endpointType == "adminURL" {
return gophercloud.AvailabilityAdmin
}
return gophercloud.AvailabilityPublic
}

View File

@ -0,0 +1,45 @@
package flavors
import (
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
)
// IDFromName is a convienience function that returns a flavor's ID given its
// name.
func IDFromName(client *gophercloud.ServiceClient, name string) (string, error) {
count := 0
id := ""
allPages, err := flavors.ListDetail(client, nil).AllPages()
if err != nil {
return "", err
}
all, err := flavors.ExtractFlavors(allPages)
if err != nil {
return "", err
}
for _, f := range all {
if f.Name == name {
count++
id = f.ID
}
}
switch count {
case 0:
err := &gophercloud.ErrResourceNotFound{}
err.ResourceType = "flavor"
err.Name = name
return "", err
case 1:
return id, nil
default:
err := &gophercloud.ErrMultipleResourcesFound{}
err.ResourceType = "flavor"
err.Name = name
err.Count = count
return "", err
}
}

11
vendor/modules.txt vendored
View File

@ -274,21 +274,24 @@ github.com/google/shlex
github.com/google/uuid
# github.com/googleapis/gax-go/v2 v2.0.5
github.com/googleapis/gax-go/v2
# github.com/gophercloud/gophercloud v0.2.0
# github.com/gophercloud/gophercloud v0.12.0
github.com/gophercloud/gophercloud
github.com/gophercloud/gophercloud/internal
github.com/gophercloud/gophercloud/openstack
github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions
github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes
github.com/gophercloud/gophercloud/openstack/common/extensions
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop
github.com/gophercloud/gophercloud/openstack/compute/v2/flavors
github.com/gophercloud/gophercloud/openstack/compute/v2/images
github.com/gophercloud/gophercloud/openstack/compute/v2/servers
github.com/gophercloud/gophercloud/openstack/identity/v2/tenants
github.com/gophercloud/gophercloud/openstack/identity/v2/tokens
github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens
github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1
github.com/gophercloud/gophercloud/openstack/identity/v3/tokens
github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
github.com/gophercloud/gophercloud/openstack/imageservice/v2/members
@ -298,8 +301,10 @@ github.com/gophercloud/gophercloud/openstack/networking/v2/networks
github.com/gophercloud/gophercloud/openstack/networking/v2/subnets
github.com/gophercloud/gophercloud/openstack/utils
github.com/gophercloud/gophercloud/pagination
# github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6
# github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c
github.com/gophercloud/utils/env
github.com/gophercloud/utils/openstack/clientconfig
github.com/gophercloud/utils/openstack/compute/v2/flavors
# github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777
github.com/gorilla/websocket
# github.com/grpc-ecosystem/go-grpc-middleware v1.1.0