Bump gophercloud to latest version

This commit is contained in:
Shengjing Zhu 2020-07-14 00:01:37 +08:00
parent 1400662db7
commit 4fe9a92058
75 changed files with 3320 additions and 896 deletions

View File

@ -6,6 +6,7 @@ import (
"log" "log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "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/helper/multistep"
"github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer"
) )
@ -37,7 +38,7 @@ func (s *StepLoadFlavor) Run(ctx context.Context, state multistep.StateBag) mult
geterr := err geterr := err
log.Printf("[INFO] Loading flavor by name: %s", s.Flavor) 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 { if err != nil {
log.Printf("[ERROR] Failed to find flavor by name: %s", err) log.Printf("[ERROR] Failed to find flavor by name: %s", err)
err = fmt.Errorf( 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/go-querystring v1.0.0 // indirect
github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 github.com/google/shlex v0.0.0-20150127133951-6f45313302b9
github.com/google/uuid v1.1.1 github.com/google/uuid v1.1.1
github.com/gophercloud/gophercloud v0.2.0 github.com/gophercloud/gophercloud v0.12.0
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 github.com/gophercloud/utils v0.0.0-20200508015959-b0167b94122c
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 // indirect github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 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.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 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 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.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM=
github.com/gophercloud/gophercloud v0.2.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= github.com/gophercloud/gophercloud v0.12.0 h1:mZrie07npp6ODiwHZolTicr5jV8Ogn43AvAsSMm6Ork=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 h1:Cw/B8Bu7Rryomxf7bjc8zNfIyLgjxsDd91n0eGRWpuo= github.com/gophercloud/gophercloud v0.12.0/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw= 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 h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 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= 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-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 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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-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 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-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-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-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-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 h1:dSChiwOTvzwbHFTMq2l6uRardHH7/E6SqEkqccinS/o=
golang.org/x/crypto v0.0.0-20200422194213-44a606286825/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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-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-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-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-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-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= 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-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-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-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-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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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-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-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-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-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-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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-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-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-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-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-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/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/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.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.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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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 github.com/mattn/goveralls
- GO111MODULE=off go get golang.org/x/tools/cmd/goimports - GO111MODULE=off go get golang.org/x/tools/cmd/goimports
go: go:
- "1.10"
- "1.11" - "1.11"
- "1.12" - "1.12"
- "1.13"
- "tip" - "tip"
env: env:
global: global:

View File

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

View File

@ -45,6 +45,9 @@ type AuthOptions struct {
Password string `json:"password,omitempty"` 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 // At most one of DomainID and DomainName must be provided if using Username
// with Identity V3. Otherwise, either are optional. // with Identity V3. Otherwise, either are optional.
DomainID string `json:"-"` DomainID string `json:"-"`
@ -98,6 +101,7 @@ type AuthScope struct {
ProjectName string ProjectName string
DomainID string DomainID string
DomainName string DomainName string
System bool
} }
// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder // 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 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) { func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
type domainReq struct { type domainReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
@ -148,7 +154,8 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
type userReq struct { type userReq struct {
ID *string `json:"id,omitempty"` ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Password string `json:"password,omitempty"` Password *string `json:"password,omitempty"`
Passcode *string `json:"passcode,omitempty"`
Domain *domainReq `json:"domain,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"` Secret *string `json:"secret,omitempty"`
} }
type totpReq struct {
User *userReq `json:"user,omitempty"`
}
type identityReq struct { type identityReq struct {
Methods []string `json:"methods"` Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"` Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"` Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"` ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
TOTP *totpReq `json:"totp,omitempty"`
} }
type authReq struct { type authReq struct {
@ -186,7 +198,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// if insufficient or incompatible information is present. // if insufficient or incompatible information is present.
var req request var req request
if opts.Password == "" { if opts.Password == "" && opts.Passcode == "" {
if opts.TokenID != "" { if opts.TokenID != "" {
// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
// parameters. // parameters.
@ -274,7 +286,14 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
} }
} else { } else {
// Password authentication. // 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. // At least one of Username and UserID must be specified.
if opts.Username == "" && opts.UserID == "" { if opts.Username == "" && opts.UserID == "" {
@ -298,25 +317,48 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
} }
// Configure the request for Username and Password authentication with a DomainID. // Configure the request for Username and Password authentication with a DomainID.
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{ req.Auth.Identity.Password = &passwordReq{
User: userReq{ User: userReq{
Name: &opts.Username, Name: &opts.Username,
Password: opts.Password, Password: &opts.Password,
Domain: &domainReq{ID: &opts.DomainID}, 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 != "" { if opts.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName. // Configure the request for Username and Password authentication with a DomainName.
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{ req.Auth.Identity.Password = &passwordReq{
User: userReq{ User: userReq{
Name: &opts.Username, Name: &opts.Username,
Password: opts.Password, Password: &opts.Password,
Domain: &domainReq{Name: &opts.DomainName}, 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},
},
}
}
}
} }
if opts.UserID != "" { if opts.UserID != "" {
@ -329,8 +371,22 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
} }
// Configure the request for UserID and Password authentication. // Configure the request for UserID and Password authentication.
if opts.Password != "" {
req.Auth.Identity.Password = &passwordReq{ req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &opts.UserID, Password: opts.Password}, 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 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) { func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
// For backwards compatibility. // For backwards compatibility.
// If AuthOptions.Scope was not set, try to determine it. // 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 != "" { if opts.Scope.ProjectName != "" {
// ProjectName provided: either DomainID or DomainName must also be supplied. // ProjectName provided: either DomainID or DomainName must also be supplied.
// ProjectID may not be supplied. // ProjectID may not be supplied.
@ -433,5 +499,16 @@ func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
} }
func (opts AuthOptions) CanReauth() bool { func (opts AuthOptions) CanReauth() bool {
if opts.Passcode != "" {
// cannot reauth using TOTP passcode
return false
}
return opts.AllowReauth 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 ( import (
"fmt" "fmt"
"net/http"
"strings" "strings"
) )
@ -82,6 +83,7 @@ type ErrUnexpectedResponseCode struct {
Expected []int Expected []int
Actual int Actual int
Body []byte Body []byte
ResponseHeader http.Header
} }
func (e ErrUnexpectedResponseCode) Error() string { func (e ErrUnexpectedResponseCode) Error() string {
@ -92,6 +94,23 @@ func (e ErrUnexpectedResponseCode) Error() string {
return e.choseErrString() 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. // ErrDefault400 is the default error type returned on a 400 HTTP response code.
type ErrDefault400 struct { type ErrDefault400 struct {
ErrUnexpectedResponseCode ErrUnexpectedResponseCode

View File

@ -1,7 +1,13 @@
module github.com/gophercloud/gophercloud module github.com/gophercloud/gophercloud
require ( require (
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
gopkg.in/yaml.v2 v2.2.2 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-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg= golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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") username := os.Getenv("OS_USERNAME")
userID := os.Getenv("OS_USERID") userID := os.Getenv("OS_USERID")
password := os.Getenv("OS_PASSWORD") password := os.Getenv("OS_PASSWORD")
passcode := os.Getenv("OS_PASSCODE")
tenantID := os.Getenv("OS_TENANT_ID") tenantID := os.Getenv("OS_TENANT_ID")
tenantName := os.Getenv("OS_TENANT_NAME") tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID") 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{ err := gophercloud.ErrMissingEnvironmentVariable{
// silently ignore TOTP passcode warning, since it is not a common auth method
EnvironmentVariable: "OS_PASSWORD", EnvironmentVariable: "OS_PASSWORD",
} }
return nilOptions, err return nilOptions, err
@ -112,6 +114,7 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
UserID: userID, UserID: userID,
Username: username, Username: username,
Password: password, Password: password,
Passcode: passcode,
TenantID: tenantID, TenantID: tenantID,
TenantName: tenantName, TenantName: tenantName,
DomainID: domainID, DomainID: domainID,

View File

@ -82,5 +82,16 @@ Example of Initializing a Volume Connection
if err != nil { if err != nil {
panic(err) 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 package volumeactions

View File

@ -47,18 +47,20 @@ func Attach(client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder
r.Err = err r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// BeginDetach will mark the volume as detaching. // BeginDetach will mark the volume as detaching.
func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) { func BeginDetaching(client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) {
b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})} b := map[string]interface{}{"os-begin_detaching": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -87,27 +89,30 @@ func Detach(client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder
r.Err = err r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Reserve will reserve a volume based on volume ID. // Reserve will reserve a volume based on volume ID.
func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) { func Reserve(client *gophercloud.ServiceClient, id string) (r ReserveResult) {
b := map[string]interface{}{"os-reserve": make(map[string]interface{})} b := map[string]interface{}{"os-reserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202}, OkCodes: []int{200, 201, 202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Unreserve will unreserve a volume based on volume ID. // Unreserve will unreserve a volume based on volume ID.
func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) { func Unreserve(client *gophercloud.ServiceClient, id string) (r UnreserveResult) {
b := map[string]interface{}{"os-unreserve": make(map[string]interface{})} b := map[string]interface{}{"os-unreserve": make(map[string]interface{})}
_, r.Err = client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{200, 201, 202}, OkCodes: []int{200, 201, 202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -145,9 +150,10 @@ func InitializeConnection(client *gophercloud.ServiceClient, id string, opts Ini
r.Err = err r.Err = err
return 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}, OkCodes: []int{200, 201, 202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -183,9 +189,10 @@ func TerminateConnection(client *gophercloud.ServiceClient, id string, opts Term
r.Err = err r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -216,9 +223,10 @@ func ExtendSize(client *gophercloud.ServiceClient, id string, opts ExtendSizeOpt
r.Err = err r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -241,6 +249,14 @@ type UploadImageOpts struct {
// Force image creation, usable if volume attached to instance. // Force image creation, usable if volume attached to instance.
Force bool `json:"force,omitempty"` 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 // 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 r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// ForceDelete will delete the volume regardless of state. // ForceDelete will delete the volume regardless of state.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) { 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 return
} }
@ -293,8 +311,35 @@ func SetImageMetadata(client *gophercloud.ServiceClient, id string, opts ImageMe
r.Err = err r.Err = err
return 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}, 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 return
} }

View File

@ -35,6 +35,12 @@ type SetImageMetadataResult struct {
gophercloud.ErrResult 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. // ReserveResult contains the response body and error from a Reserve request.
type ReserveResult struct { type ReserveResult struct {
gophercloud.ErrResult gophercloud.ErrResult
@ -157,6 +163,14 @@ type VolumeImage struct {
// Current status of the volume. // Current status of the volume.
Status string `json:"status"` 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. // The date when this volume was last updated.
UpdatedAt time.Time `json:"-"` 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 Package volumes provides information and interaction with volumes in the
// device, akin to a USB hard drive. It can only be attached to one instance at OpenStack Block Storage service. A volume is a detachable block storage
// a time. 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 package volumes

View File

@ -16,7 +16,7 @@ type CreateOptsBuilder interface {
// see the Volume object. // see the Volume object.
type CreateOpts struct { type CreateOpts struct {
// The size of the volume, in GB // The size of the volume, in GB
Size int `json:"size" required:"true"` Size int `json:"size,omitempty"`
// The availability zone // The availability zone
AvailabilityZone string `json:"availability_zone,omitempty"` AvailabilityZone string `json:"availability_zone,omitempty"`
// ConsistencyGroupID is the ID of a consistency group // 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. // The ID of the image from which you want to create the volume.
// Required to create a bootable volume. // Required to create a bootable volume.
ImageID string `json:"imageRef,omitempty"` 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 // The associated volume type
VolumeType string `json:"volume_type,omitempty"` VolumeType string `json:"volume_type,omitempty"`
// Multiattach denotes if the volume is multi-attach capable. // 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 r.Err = err
return 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}, OkCodes: []int{202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -93,14 +97,16 @@ func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder
} }
url += query url += query
} }
_, r.Err = client.Delete(url, nil) resp, err := client.Delete(url, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get retrieves the Volume with the provided ID. To extract the Volume object // Get retrieves the Volume with the provided ID. To extract the Volume object
// from the response, call the Extract method on the GetResult. // from the response, call the Extract method on the GetResult.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -194,44 +200,9 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return 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"` SnapshotID string `json:"snapshot_id"`
// The ID of another block storage volume from which the current volume was created // The ID of another block storage volume from which the current volume was created
SourceVolID string `json:"source_volid"` 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. // Arbitrary key-value pairs defined by the user.
Metadata map[string]string `json:"metadata"` Metadata map[string]string `json:"metadata"`
// UserID is the id of the user who created the volume. // UserID is the id of the user who created the volume.

View File

@ -3,9 +3,12 @@ package openstack
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" 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" tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/openstack/utils" "github.com/gophercloud/gophercloud/openstack/utils"
) )
@ -67,7 +70,7 @@ Example:
ao, err := openstack.AuthOptionsFromEnv() ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao) 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"), Region: os.Getenv("OS_REGION_NAME"),
}) })
*/ */
@ -187,17 +190,62 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
v3Client.Endpoint = endpoint v3Client.Endpoint = endpoint
} }
result := tokens3.Create(v3Client, opts) var catalog *tokens3.ServiceCatalog
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{})
}
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) err = client.SetTokenAndAuthResult(result)
if err != nil { if err != nil {
return err return err
} }
catalog, err := result.ExtractServiceCatalog() catalog, err = result.ExtractServiceCatalog()
if err != nil { if err != nil {
return err 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() { if opts.CanReauth() {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
@ -217,6 +265,14 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
o := *ot o := *ot
o.AllowReauth = false o.AllowReauth = false
tao = &o tao = &o
case *ec2tokens.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
case *oauth1.AuthOptions:
o := *ot
o.AllowReauth = false
tao = &o
default: default:
tao = opts tao = opts
} }
@ -395,7 +451,11 @@ func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi
// load balancer service. // load balancer service.
func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
sc, err := initClientOpts(client, eo, "load-balancer") 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 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) { func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "workflowv2") 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. // Get requests details on a single interface attachment by the server and port IDs.
func Get(client *gophercloud.ServiceClient, serverID, portID string) (r GetResult) { 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -58,15 +59,17 @@ func Create(client *gophercloud.ServiceClient, serverID string, opts CreateOptsB
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Delete makes a request against the nova API to detach a single interface from the server. // 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. // It needs server and port IDs to make a such request.
func Delete(client *gophercloud.ServiceClient, serverID, portID string) (r DeleteResult) { 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 return
} }

View File

@ -75,6 +75,10 @@ type BlockDevice struct {
// DiskBus is the bus type of the block devices. // DiskBus is the bus type of the block devices.
// Examples of this are ide, usb, virtio, scsi, etc. // Examples of this are ide, usb, virtio, scsi, etc.
DiskBus string `json:"disk_bus,omitempty"` 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 // 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 r.Err = err
return 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}, OkCodes: []int{200, 202},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }

View File

@ -3,5 +3,5 @@ package bootfromvolume
import "github.com/gophercloud/gophercloud" import "github.com/gophercloud/gophercloud"
func createURL(c *gophercloud.ServiceClient) string { 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 r.Err = err
return 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}, OkCodes: []int{200, 201},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get returns public data about a previously uploaded KeyPair. // Get returns public data about a previously uploaded KeyPair.
func Get(client *gophercloud.ServiceClient, name string) (r GetResult) { 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 return
} }
// Delete requests the deletion of a previous stored KeyPair from the server. // Delete requests the deletion of a previous stored KeyPair from the server.
func Delete(client *gophercloud.ServiceClient, name string) (r DeleteResult) { 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 return
} }

View File

@ -1,19 +1,20 @@
package startstop package startstop
import "github.com/gophercloud/gophercloud" import (
"github.com/gophercloud/gophercloud"
func actionURL(client *gophercloud.ServiceClient, id string) string { "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions"
return client.ServiceURL("servers", id, "action") )
}
// Start is the operation responsible for starting a Compute server. // Start is the operation responsible for starting a Compute server.
func Start(client *gophercloud.ServiceClient, id string) (r StartResult) { 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 return
} }
// Stop is the operation responsible for stopping a Compute server. // Stop is the operation responsible for stopping a Compute server.
func Stop(client *gophercloud.ServiceClient, id string) (r StopResult) { 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 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 r.Err = err
return 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}, OkCodes: []int{200, 201},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get retrieves details of a single flavor. Use Extract to convert its // Get retrieves details of a single flavor. Use Extract to convert its
// result into a Flavor. // result into a Flavor.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Delete deletes the specified flavor ID. // Delete deletes the specified flavor ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -194,9 +197,10 @@ func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsB
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -224,20 +228,23 @@ func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAcces
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// ExtraSpecs requests all the extra-specs for the given flavor ID. // ExtraSpecs requests all the extra-specs for the given flavor ID.
func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { 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 return
} }
func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { 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 return
} }
@ -264,9 +271,10 @@ func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts C
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -302,56 +310,19 @@ func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts Up
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// DeleteExtraSpec will delete the key-value pair with the given key for the given // DeleteExtraSpec will delete the key-value pair with the given key for the given
// flavor ID. // flavor ID.
func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return 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 ( import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"github.com/gophercloud/gophercloud" "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" "github.com/gophercloud/gophercloud/pagination"
) )
@ -55,6 +54,22 @@ type ListOpts struct {
// TenantID lists servers for a particular tenant. // TenantID lists servers for a particular tenant.
// Setting "AllTenants = true" is required. // Setting "AllTenants = true" is required.
TenantID string `q:"tenant_id"` 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. // 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 is the name to assign to the newly launched server.
Name string `json:"name" required:"true"` Name string `json:"name" required:"true"`
// ImageRef [optional; required if ImageName is not provided] is the ID or // ImageRef is the ID or full URL to the image that contains the
// full URL to the image that contains the server's OS and initial state. // server's OS and initial state.
// Also optional if using the boot-from-volume extension. // Also optional if using the boot-from-volume extension.
ImageRef string `json:"imageRef"` ImageRef string `json:"imageRef"`
// ImageName [optional; required if ImageRef is not provided] is the name of // FlavorRef is the ID or full URL to the flavor that describes the server's specs.
// the image that contains the server's OS and initial state.
// Also optional if using the boot-from-volume extension.
ImageName string `json:"-"`
// FlavorRef [optional; required if FlavorName is not provided] is the ID or
// full URL to the flavor that describes the server's specs.
FlavorRef string `json:"flavorRef"` 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 // SecurityGroups lists the names of the security groups to which this server
// should belong. // should belong.
SecurityGroups []string `json:"-"` SecurityGroups []string `json:"-"`
@ -163,7 +168,9 @@ type CreateOpts struct {
// Networks dictates how this server will be attached to available networks. // Networks dictates how this server will be attached to available networks.
// By default, the server will be attached to all isolated networks for the // By default, the server will be attached to all isolated networks for the
// tenant. // 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 // Metadata contains key-value pairs (up to 255 bytes each) to attach to the
// server. // server.
@ -204,7 +211,6 @@ type CreateOpts struct {
// ToServerCreateMap assembles a request body based on the contents of a // ToServerCreateMap assembles a request body based on the contents of a
// CreateOpts. // CreateOpts.
func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
sc := opts.ServiceClient
opts.ServiceClient = nil opts.ServiceClient = nil
b, err := gophercloud.BuildRequestBody(opts, "") b, err := gophercloud.BuildRequestBody(opts, "")
if err != nil { if err != nil {
@ -229,9 +235,11 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
b["security_groups"] = securityGroups b["security_groups"] = securityGroups
} }
if len(opts.Networks) > 0 { switch v := opts.Networks.(type) {
networks := make([]map[string]interface{}, len(opts.Networks)) case []Network:
for i, net := range opts.Networks { if len(v) > 0 {
networks := make([]map[string]interface{}, len(v))
for i, net := range v {
networks[i] = make(map[string]interface{}) networks[i] = make(map[string]interface{})
if net.UUID != "" { if net.UUID != "" {
networks[i]["uuid"] = net.UUID networks[i]["uuid"] = net.UUID
@ -245,41 +253,12 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
} }
b["networks"] = networks b["networks"] = networks
} }
case string:
// If ImageRef isn't provided, check if ImageName was provided to ascertain if v == "auto" || v == "none" {
// the image ID. b["networks"] = v
if opts.ImageRef == "" { } else {
if opts.ImageName != "" { 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 sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
} }
imageID, err := images.IDFromName(sc, opts.ImageName)
if err != nil {
return nil, err
}
b["imageRef"] = imageID
}
}
// If FlavorRef isn't provided, use FlavorName to ascertain the flavor ID.
if opts.FlavorRef == "" {
if opts.FlavorName == "" {
err := ErrNeitherFlavorIDNorFlavorNameProvided{}
err.Argument = "FlavorRef/FlavorName"
return nil, err
}
if sc == nil {
err := ErrNoClientProvidedForIDByName{}
err.Argument = "ServiceClient"
return nil, err
}
flavorID, err := flavors.IDFromName(sc, opts.FlavorName)
if err != nil {
return nil, err
}
b["flavorRef"] = flavorID
} }
if opts.Min != 0 { if opts.Min != 0 {
@ -300,28 +279,32 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err r.Err = err
return 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 return
} }
// Delete requests that a server previously provisioned be removed from your // Delete requests that a server previously provisioned be removed from your
// account. // account.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// ForceDelete forces the deletion of a server. // ForceDelete forces the deletion of a server.
func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) {
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) resp, err := client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get requests details on a single server, by ID. // Get requests details on a single server, by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{ resp, err := client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203}, OkCodes: []int{200, 203},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -358,9 +341,10 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -372,7 +356,8 @@ func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword stri
"adminPass": newPassword, "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 return
} }
@ -413,7 +398,7 @@ func (opts RebootOpts) ToServerRebootMap() (map[string]interface{}, error) {
HardReboot (aka PowerCycle) starts the server instance by physically cutting HardReboot (aka PowerCycle) starts the server instance by physically cutting
power to the machine, or if a VM, terminating it at the hypervisor level. power to the machine, or if a VM, terminating it at the hypervisor level.
It's done. Caput. Full stop. 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 SoftReboot (aka OSReboot) simply tells the OS to restart under its own
procedure. procedure.
@ -426,7 +411,8 @@ func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder
r.Err = err r.Err = err
return 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 return
} }
@ -442,11 +428,8 @@ type RebuildOpts struct {
// AdminPass is the server's admin password // AdminPass is the server's admin password
AdminPass string `json:"adminPass,omitempty"` AdminPass string `json:"adminPass,omitempty"`
// ImageID is the ID of the image you want your server to be provisioned on. // ImageRef is the ID of the image you want your server to be provisioned on.
ImageID string `json:"imageRef"` ImageRef string `json:"imageRef"`
// ImageName is readable name of an image.
ImageName string `json:"-"`
// Name to set the server to // Name to set the server to
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -477,23 +460,6 @@ func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) {
return nil, err 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 return map[string]interface{}{"rebuild": b}, nil
} }
@ -505,7 +471,8 @@ func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuild
r.Err = err r.Err = err
return 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 return
} }
@ -543,23 +510,26 @@ func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder
r.Err = err r.Err = err
return 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 return
} }
// ConfirmResize confirms a previous resize operation on a server. // ConfirmResize confirms a previous resize operation on a server.
// See Resize() for more details. // See Resize() for more details.
func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { 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}, OkCodes: []int{201, 202, 204},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// RevertResize cancels a previous resize operation on a server. // RevertResize cancels a previous resize operation on a server.
// See Resize() for more details. // See Resize() for more details.
func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { 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 return
} }
@ -595,15 +565,17 @@ func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetad
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Metadata requests all the metadata for the given server ID. // Metadata requests all the metadata for the given server ID.
func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { 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 return
} }
@ -622,9 +594,10 @@ func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMet
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -662,23 +635,26 @@ func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts Metadatu
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Metadatum requests the key-value pair with the given key for the given // Metadatum requests the key-value pair with the given key for the given
// server ID. // server ID.
func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) {
_, r.Err = client.Get(metadatumURL(client, id, key), &r.Body, nil) resp, err := client.Get(metadatumURL(client, id, key), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// DeleteMetadatum will delete the key-value pair with the given key for the // DeleteMetadatum will delete the key-value pair with the given key for the
// given server ID. // given server ID.
func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) {
_, r.Err = client.Delete(metadatumURL(client, id, key), nil) resp, err := client.Delete(metadatumURL(client, id, key), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return 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{ resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{
OkCodes: []int{202}, OkCodes: []int{202},
}) })
r.Err = err _, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
r.Header = resp.Header
return 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 // GetPassword makes a request against the nova API to get the encrypted
// administrative password. // administrative password.
func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) {
_, r.Err = client.Get(passwordURL(client, serverId), &r.Body, nil) resp, err := client.Get(passwordURL(client, serverId), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -805,8 +744,9 @@ func ShowConsoleOutput(client *gophercloud.ServiceClient, id string, opts ShowCo
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }

View File

@ -212,8 +212,19 @@ type Server struct {
// to it. // to it.
SecurityGroups []map[string]interface{} `json:"security_groups"` 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 contains failure information about a server.
Fault Fault `json:"fault"` 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 { type Fault struct {

View File

@ -7,7 +7,7 @@ Example of Creating a Service Client
ao, err := openstack.AuthOptionsFromEnv() ao, err := openstack.AuthOptionsFromEnv()
provider, err := openstack.AuthenticatedClient(ao) 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"), 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 { if len(endpoints) > 1 {
err := &ErrMultipleMatchingEndpointsV2{} endpoints = endpoints[0:1]
err.Endpoints = endpoints
return "", err
} }
// Extract the appropriate URL from the matching Endpoint. // 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 { if len(endpoints) > 1 {
return "", ErrMultipleMatchingEndpointsV3{Endpoints: endpoints} endpoints = endpoints[0:1]
} }
// Extract the URL from the matching Endpoint. // Extract the URL from the matching Endpoint.

View File

@ -4,8 +4,6 @@ import (
"fmt" "fmt"
"github.com/gophercloud/gophercloud" "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 // 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) 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 // ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not
// found // found
type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput }

View File

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

View File

@ -60,15 +60,17 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err r.Err = err
return 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}, OkCodes: []int{200, 201},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get requests details on a single tenant by ID. // Get requests details on a single tenant by ID.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -103,14 +105,16 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err r.Err = err
return 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Delete is the operation responsible for permanently deleting a tenant. // Delete is the operation responsible for permanently deleting a tenant.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }

View File

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

View File

@ -109,6 +109,13 @@ func (r CreateResult) ExtractTokenID() (string, error) {
return r.Header.Get("X-Subject-Token"), r.Err 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 // ExtractServiceCatalog returns the ServiceCatalog that was generated along
// with the user's Token. // with the user's Token.
func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
@ -144,6 +151,15 @@ func (r commonResult) ExtractProject() (*Project, error) {
return s.Project, err 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() // CreateResult is the response from a Create request. Use ExtractToken()
// to interpret it as a Token, or ExtractServiceCatalog() to interpret it // to interpret it as a Token, or ExtractServiceCatalog() to interpret it
// as a service catalog. // as a service catalog.

View File

@ -206,19 +206,22 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
r.Err = err r.Err = err
return r 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 return
} }
// Delete implements image delete request. // Delete implements image delete request.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) resp, err := client.Delete(deleteURL(client, id), nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Get implements image get request. // Get implements image get request.
func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { func Get(client *gophercloud.ServiceClient, id string) (r GetResult) {
_, r.Err = client.Get(getURL(client, id), &r.Body, nil) resp, err := client.Get(getURL(client, id), &r.Body, nil)
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -229,10 +232,11 @@ func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder
r.Err = err r.Err = err
return r 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}, OkCodes: []int{200},
MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return 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. // UpdateOp represents a valid update operation.
type UpdateOp string type UpdateOp string
@ -358,7 +376,7 @@ func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} {
"path": fmt.Sprintf("/%s", r.Name), "path": fmt.Sprintf("/%s", r.Name),
} }
if r.Value != "" { if r.Op != RemoveOp {
updateMap["value"] = r.Value updateMap["value"] = r.Value
} }

View File

@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"time" "time"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
@ -86,6 +87,13 @@ type Image struct {
// VirtualSize is the virtual size of the image // VirtualSize is the virtual size of the image
VirtualSize int64 `json:"virtual_size"` 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 { func (r *Image) UnmarshalJSON(b []byte) error {
@ -93,6 +101,8 @@ func (r *Image) UnmarshalJSON(b []byte) error {
var s struct { var s struct {
tmp 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) err := json.Unmarshal(b, &s)
if err != nil { if err != nil {
@ -120,9 +130,18 @@ func (r *Image) UnmarshalJSON(b []byte) error {
if resultMap, ok := result.(map[string]interface{}); ok { if resultMap, ok := result.(map[string]interface{}); ok {
delete(resultMap, "self") delete(resultMap, "self")
delete(resultMap, "size") delete(resultMap, "size")
delete(resultMap, "openstack-image-import-methods")
delete(resultMap, "openstack-image-store-ids")
r.Properties = internal.RemainingKeys(Image{}, resultMap) 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 return err
} }
@ -133,6 +152,20 @@ type commonResult struct {
// Extract interprets any commonResult as an Image. // Extract interprets any commonResult as an Image.
func (r commonResult) Extract() (*Image, error) { func (r commonResult) Extract() (*Image, error) {
var s *Image 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) err := r.ExtractInto(&s)
return s, err return s, err
} }
@ -200,3 +233,8 @@ func ExtractImages(r pagination.Page) ([]Image, error) {
err := (r.(ImagePage)).ExtractInto(&s) err := (r.(ImagePage)).ExtractInto(&s)
return s.Images, err 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 // ImageStatusDeactivated denotes that access to image data is not allowed to
// any non-admin user. // any non-admin user.
ImageStatusDeactivated ImageStatus = "deactivated" 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. // 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) { func Create(client *gophercloud.ServiceClient, id string, member string) (r CreateResult) {
b := map[string]interface{}{"member": member} 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}, OkCodes: []int{200},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
@ -40,13 +41,15 @@ func List(client *gophercloud.ServiceClient, id string) pagination.Pager {
// Get image member details. // Get image member details.
func Get(client *gophercloud.ServiceClient, imageID string, memberID string) (r DetailsResult) { 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 return
} }
// Delete membership for given image. Callee should be image owner. // Delete membership for given image. Callee should be image owner.
func Delete(client *gophercloud.ServiceClient, imageID string, memberID string) (r DeleteResult) { 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 return
} }
@ -75,7 +78,8 @@ func Update(client *gophercloud.ServiceClient, imageID string, memberID string,
r.Err = err r.Err = err
return 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}}) &gophercloud.RequestOpts{OkCodes: []int{200}})
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }

View File

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

View File

@ -1,6 +1,9 @@
package floatingips package floatingips
import ( import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
@ -37,6 +40,11 @@ type FloatingIP struct {
// specify a project identifier other than its own. // specify a project identifier other than its own.
TenantID string `json:"tenant_id"` 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 is the project owner of the floating IP.
ProjectID string `json:"project_id"` ProjectID string `json:"project_id"`
@ -50,6 +58,44 @@ type FloatingIP struct {
Tags []string `json:"tags"` 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 { type commonResult struct {
gophercloud.Result 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. // Get retrieves a specific network based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { 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 return
} }
@ -99,7 +100,8 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
r.Err = err r.Err = err
return 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 return
} }
@ -130,51 +132,16 @@ func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuild
r.Err = err r.Err = err
return 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}, OkCodes: []int{200, 201},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Delete accepts a unique ID and deletes the network associated with it. // Delete accepts a unique ID and deletes the network associated with it.
func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) { 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 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 package networks
import ( import (
"encoding/json"
"time"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
@ -70,6 +73,11 @@ type Network struct {
// TenantID is the project owner of the network. // TenantID is the project owner of the network.
TenantID string `json:"tenant_id"` 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 is the project owner of the network.
ProjectID string `json:"project_id"` ProjectID string `json:"project_id"`
@ -84,6 +92,44 @@ type Network struct {
Tags []string `json:"tags"` 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 // NetworkPage is the page returned by a pager when traversing over a
// collection of networks. // collection of networks.
type NetworkPage struct { 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. // Get retrieves a specific subnet based on its unique ID.
func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { 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 return
} }
@ -160,7 +161,8 @@ func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResul
r.Err = err r.Err = err
return 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 return
} }
@ -219,51 +221,16 @@ func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r
r.Err = err r.Err = err
return 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}, OkCodes: []int{200, 201},
}) })
_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
return return
} }
// Delete accepts a unique ID and deletes the subnet associated with it. // Delete accepts a unique ID and deletes the subnet associated with it.
func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { 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 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

@ -56,5 +56,6 @@ func Request(client *gophercloud.ServiceClient, headers map[string]string, url s
return client.Get(url, nil, &gophercloud.RequestOpts{ return client.Get(url, nil, &gophercloud.RequestOpts{
MoreHeaders: headers, MoreHeaders: headers,
OkCodes: []int{200, 204, 300}, 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() optsMap[tags[0]] = v.String()
case reflect.Int: case reflect.Int:
optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
case reflect.Int64:
optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10)
case reflect.Bool: case reflect.Bool:
optsMap[tags[0]] = strconv.FormatBool(v.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. // reauthlock represents a set of attributes used to help in the reauthentication process.
type reauthlock struct { type reauthlock struct {
sync.RWMutex sync.RWMutex
reauthing bool ongoing *reauthFuture
reauthingErr error }
done *sync.Cond
// 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 // 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 return
} }
if client.reauthmut != nil { if client.reauthmut != nil {
// If a Reauthenticate is in progress, wait for it to complete.
client.reauthmut.Lock() client.reauthmut.Lock()
for client.reauthmut.reauthing { ongoing := client.reauthmut.ongoing
client.reauthmut.done.Wait()
}
client.reauthmut.Unlock() client.reauthmut.Unlock()
if ongoing != nil {
_ = ongoing.Get()
}
} }
t := client.Token() t := client.Token()
if t == "" { if t == "" {
@ -223,7 +248,7 @@ func (client *ProviderClient) SetThrowaway(v bool) {
// this case, the reauthentication can be skipped if another thread has already // this case, the reauthentication can be skipped if another thread has already
// reauthenticated in the meantime. If no previous token is known, an empty // reauthenticated in the meantime. If no previous token is known, an empty
// string should be passed instead to force unconditional reauthentication. // 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 { if client.ReauthFunc == nil {
return nil return nil
} }
@ -232,33 +257,36 @@ func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
return client.ReauthFunc() return client.ReauthFunc()
} }
future := newReauthFuture()
// Check if a Reauthenticate is in progress, or start one if not.
client.reauthmut.Lock() client.reauthmut.Lock()
if client.reauthmut.reauthing { ongoing := client.reauthmut.ongoing
for !client.reauthmut.reauthing { if ongoing == nil {
client.reauthmut.done.Wait() client.reauthmut.ongoing = future
}
err = client.reauthmut.reauthingErr
client.reauthmut.Unlock()
return err
} }
client.reauthmut.Unlock() client.reauthmut.Unlock()
client.reauthmut.Lock() // If Reauthenticate is running elsewhere, wait for its result.
client.reauthmut.reauthing = true if ongoing != nil {
client.reauthmut.done = sync.NewCond(client.reauthmut) return ongoing.Get()
client.reauthmut.reauthingErr = nil }
client.reauthmut.Unlock()
// Perform the actual reauthentication.
var err error
if previousToken == "" || client.TokenID == previousToken { if previousToken == "" || client.TokenID == previousToken {
err = client.ReauthFunc() err = client.ReauthFunc()
} else {
err = nil
} }
// Mark Reauthenticate as finished.
client.reauthmut.Lock() client.reauthmut.Lock()
client.reauthmut.reauthing = false client.reauthmut.ongoing.Set(err)
client.reauthmut.reauthingErr = err client.reauthmut.ongoing = nil
client.reauthmut.done.Broadcast()
client.reauthmut.Unlock() client.reauthmut.Unlock()
return
return err
} }
// RequestOpts customizes the behavior of the provider.Request() method. // 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. // 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. // This lets resources override default error messages based on the response status code.
ErrorContext error 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" 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 // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
// header will automatically be provided. // header will automatically be provided.
func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { 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 body io.Reader
var contentType *string var contentType *string
@ -309,6 +355,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
contentType = &applicationJSON 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 { if options.RawBody != nil {
body = options.RawBody body = options.RawBody
} }
@ -347,9 +398,6 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
req.Header.Set(k, v) 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") prereqtok := req.Header.Get("X-Auth-Token")
// Issue the request. // Issue the request.
@ -382,6 +430,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
Expected: options.OkCodes, Expected: options.OkCodes,
Actual: resp.StatusCode, Actual: resp.StatusCode,
Body: body, Body: body,
ResponseHeader: resp.Header,
} }
errType := options.ErrorContext errType := options.ErrorContext
@ -392,7 +441,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
err = error400er.Error400(respErr) err = error400er.Error400(respErr)
} }
case http.StatusUnauthorized: case http.StatusUnauthorized:
if client.ReauthFunc != nil { if client.ReauthFunc != nil && !state.hasReauthenticated {
err = client.Reauthenticate(prereqtok) err = client.Reauthenticate(prereqtok)
if err != nil { if err != nil {
e := &ErrUnableToReauthenticate{} e := &ErrUnableToReauthenticate{}
@ -404,7 +453,8 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
seeker.Seek(0, 0) 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 { if err != nil {
switch err.(type) { switch err.(type) {
case *ErrUnexpectedResponseCode: 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. // Parse the response body as JSON, if requested to do so.
if options.JSONResponse != nil { if options.JSONResponse != nil {
defer resp.Body.Close() 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 { if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
return nil, err 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 return resp, nil
} }
func defaultOkCodes(method string) []int { func defaultOkCodes(method string) []int {
switch { switch method {
case method == "GET": case "GET", "HEAD":
return []int{200} return []int{200}
case method == "POST": case "POST":
return []int{201, 202} return []int{201, 202}
case method == "PUT": case "PUT":
return []int{201, 202} return []int{201, 202}
case method == "PATCH": case "PATCH":
return []int{200, 202, 204} return []int{200, 202, 204}
case method == "DELETE": case "DELETE":
return []int{202, 204} 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 // fields of the struct or composed extension struct
// at the end of this method. // at the end of this method.
toValue.Set(newSlice) 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: 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) 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 Example to Create a Provider Client From clouds.yaml
opts := &clientconfig.ClientOpts{ opts := &clientconfig.ClientOpts{
Name: "hawaii", Cloud: "hawaii",
} }
pClient, err := clientconfig.AuthenticatedClient(opts) pClient, err := clientconfig.AuthenticatedClient(opts)

View File

@ -2,12 +2,13 @@ package clientconfig
import ( import (
"fmt" "fmt"
"os" "net/http"
"reflect" "reflect"
"strings" "strings"
"github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/utils/env"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -56,11 +57,62 @@ type ClientOpts struct {
// This will override a region in clouds.yaml or can be used // This will override a region in clouds.yaml or can be used
// when authenticating directly with AuthInfo. // when authenticating directly with AuthInfo.
RegionName string 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. // 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) { func LoadCloudsYAML() (map[string]Cloud, error) {
content, err := findAndReadCloudsYAML() _, content, err := FindAndReadCloudsYAML()
if err != nil { if err != nil {
return nil, err 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. // 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) { func LoadSecureCloudsYAML() (map[string]Cloud, error) {
var secureClouds Clouds var secureClouds Clouds
content, err := findAndReadSecureCloudsYAML() _, content, err := FindAndReadSecureCloudsYAML()
if err != nil { if err != nil {
if err.Error() == "no secure.yaml file found" { if err.Error() == "no secure.yaml file found" {
// secure.yaml is optional so just ignore read error // 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. // 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) { func LoadPublicCloudsYAML() (map[string]Cloud, error) {
var publicClouds PublicClouds var publicClouds PublicClouds
content, err := findAndReadPublicCloudsYAML() _, content, err := FindAndReadPublicCloudsYAML()
if err != nil { if err != nil {
if err.Error() == "no clouds-public.yaml file found" { if err.Error() == "no clouds-public.yaml file found" {
// clouds-public.yaml is optional so just ignore read error // 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. // GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) { 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 { if err != nil {
return nil, fmt.Errorf("unable to load clouds.yaml: %s", err) return nil, fmt.Errorf("unable to load clouds.yaml: %s", err)
} }
@ -138,7 +204,7 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
envPrefix = opts.EnvPrefix envPrefix = opts.EnvPrefix
} }
if v := os.Getenv(envPrefix + "CLOUD"); v != "" { if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v cloudName = v
} }
@ -159,32 +225,33 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
} }
} }
var cloudIsInCloudsYaml bool if cloud != nil {
if cloud == nil { // A profile points to a public cloud entry.
// not an immediate error as it might still be defined in secure.yaml // If one was specified, load a list of public clouds
cloudIsInCloudsYaml = false // and then merge the information with the current cloud data.
} else { profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud)
cloudIsInCloudsYaml = true
}
publicClouds, err := LoadPublicCloudsYAML() if profileName != "" {
publicClouds, err := yamlOpts.LoadPublicCloudsYAML()
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to load clouds-public.yaml: %s", err) 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] publicCloud, ok := publicClouds[profileName]
if !ok { if !ok {
return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName) return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
} }
cloud, err = mergeClouds(cloud, publicCloud) cloud, err = mergeClouds(cloud, publicCloud)
if err != nil { if err != nil {
return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName) 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 { if err != nil {
return nil, fmt.Errorf("unable to load secure.yaml: %s", err) 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 secureClouds != nil {
// If no entry was found in clouds.yaml, no cloud name was specified, // 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. // 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 { for _, v := range secureClouds {
cloud = &v cloud = &v
} }
} }
// Otherwise, see if the provided cloud name exists in the secure yaml file.
secureCloud, ok := secureClouds[cloudName] secureCloud, ok := secureClouds[cloudName]
if !ok && cloud == nil { if !ok && cloud == nil {
// cloud == nil serves two purposes here: // 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 // Default is to verify SSL API requests
if cloud.Verify == nil { if cloud.Verify == nil {
iTrue := true iTrue := true
@ -227,6 +301,17 @@ func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
// clouds-public.yml // clouds-public.yml
// https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors // 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 return cloud, nil
} }
@ -260,7 +345,7 @@ func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
envPrefix = opts.EnvPrefix envPrefix = opts.EnvPrefix
} }
if v := os.Getenv(envPrefix + "CLOUD"); v != "" { if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v cloudName = v
} }
@ -310,7 +395,7 @@ func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
envPrefix = opts.EnvPrefix envPrefix = opts.EnvPrefix
} }
if v := os.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" { if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
identityAPI = v identityAPI = v
} }
@ -359,49 +444,49 @@ func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
} }
if cloud.AuthInfo.AuthURL == "" { if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" { if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v cloud.AuthInfo.AuthURL = v
} }
} }
if cloud.AuthInfo.Token == "" { if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" { if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.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 cloud.AuthInfo.Token = v
} }
} }
if cloud.AuthInfo.Username == "" { if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" { if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v cloud.AuthInfo.Username = v
} }
} }
if cloud.AuthInfo.Password == "" { if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" { if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v cloud.AuthInfo.Password = v
} }
} }
if cloud.AuthInfo.ProjectID == "" { if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" { if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = 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 cloud.AuthInfo.ProjectID = v
} }
} }
if cloud.AuthInfo.ProjectName == "" { if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" { if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = 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 cloud.AuthInfo.ProjectName = v
} }
} }
@ -427,115 +512,115 @@ func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
} }
if cloud.AuthInfo.AuthURL == "" { if cloud.AuthInfo.AuthURL == "" {
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" { if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
cloud.AuthInfo.AuthURL = v cloud.AuthInfo.AuthURL = v
} }
} }
if cloud.AuthInfo.Token == "" { if cloud.AuthInfo.Token == "" {
if v := os.Getenv(envPrefix + "TOKEN"); v != "" { if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
cloud.AuthInfo.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 cloud.AuthInfo.Token = v
} }
} }
if cloud.AuthInfo.Username == "" { if cloud.AuthInfo.Username == "" {
if v := os.Getenv(envPrefix + "USERNAME"); v != "" { if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
cloud.AuthInfo.Username = v cloud.AuthInfo.Username = v
} }
} }
if cloud.AuthInfo.UserID == "" { if cloud.AuthInfo.UserID == "" {
if v := os.Getenv(envPrefix + "USER_ID"); v != "" { if v := env.Getenv(envPrefix + "USER_ID"); v != "" {
cloud.AuthInfo.UserID = v cloud.AuthInfo.UserID = v
} }
} }
if cloud.AuthInfo.Password == "" { if cloud.AuthInfo.Password == "" {
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" { if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
cloud.AuthInfo.Password = v cloud.AuthInfo.Password = v
} }
} }
if cloud.AuthInfo.ProjectID == "" { if cloud.AuthInfo.ProjectID == "" {
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" { if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
cloud.AuthInfo.ProjectID = 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 cloud.AuthInfo.ProjectID = v
} }
} }
if cloud.AuthInfo.ProjectName == "" { if cloud.AuthInfo.ProjectName == "" {
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" { if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
cloud.AuthInfo.ProjectName = 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 cloud.AuthInfo.ProjectName = v
} }
} }
if cloud.AuthInfo.DomainID == "" { if cloud.AuthInfo.DomainID == "" {
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" { if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
cloud.AuthInfo.DomainID = v cloud.AuthInfo.DomainID = v
} }
} }
if cloud.AuthInfo.DomainName == "" { if cloud.AuthInfo.DomainName == "" {
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" { if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
cloud.AuthInfo.DomainName = v cloud.AuthInfo.DomainName = v
} }
} }
if cloud.AuthInfo.DefaultDomain == "" { if cloud.AuthInfo.DefaultDomain == "" {
if v := os.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" { if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
cloud.AuthInfo.DefaultDomain = v cloud.AuthInfo.DefaultDomain = v
} }
} }
if cloud.AuthInfo.ProjectDomainID == "" { 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 cloud.AuthInfo.ProjectDomainID = v
} }
} }
if cloud.AuthInfo.ProjectDomainName == "" { 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 cloud.AuthInfo.ProjectDomainName = v
} }
} }
if cloud.AuthInfo.UserDomainID == "" { 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 cloud.AuthInfo.UserDomainID = v
} }
} }
if cloud.AuthInfo.UserDomainName == "" { 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 cloud.AuthInfo.UserDomainName = v
} }
} }
if cloud.AuthInfo.ApplicationCredentialID == "" { 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 cloud.AuthInfo.ApplicationCredentialID = v
} }
} }
if cloud.AuthInfo.ApplicationCredentialName == "" { 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 cloud.AuthInfo.ApplicationCredentialName = v
} }
} }
if cloud.AuthInfo.ApplicationCredentialSecret == "" { 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 cloud.AuthInfo.ApplicationCredentialSecret = v
} }
} }
@ -545,7 +630,11 @@ func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
scope := new(gophercloud.AuthScope) scope := new(gophercloud.AuthScope)
// Application credentials don't support scope // 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 !isProjectScoped(cloud.AuthInfo) {
if cloud.AuthInfo.DomainID != "" { if cloud.AuthInfo.DomainID != "" {
scope.DomainID = cloud.AuthInfo.DomainID scope.DomainID = cloud.AuthInfo.DomainID
@ -639,7 +728,7 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
envPrefix = opts.EnvPrefix envPrefix = opts.EnvPrefix
} }
if v := os.Getenv(envPrefix + "CLOUD"); v != "" { if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
cloudName = v cloudName = v
} }
@ -659,10 +748,15 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
return nil, err return nil, err
} }
// If an HTTPClient was specified, use it.
if opts.HTTPClient != nil {
pClient.HTTPClient = *opts.HTTPClient
}
// Determine the region to use. // Determine the region to use.
// First, check if the REGION_NAME environment variable is set. // First, check if the REGION_NAME environment variable is set.
var region string var region string
if v := os.Getenv(envPrefix + "REGION_NAME"); v != "" { if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" {
region = v region = v
} }
@ -677,8 +771,27 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
region = v 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{ eo := gophercloud.EndpointOpts{
Region: region, Region: region,
Availability: GetEndpointType(endpointType),
} }
switch service { switch service {
@ -688,6 +801,8 @@ func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceCli
return openstack.NewComputeV2(pClient, eo) return openstack.NewComputeV2(pClient, eo)
case "container": case "container":
return openstack.NewContainerV1(pClient, eo) return openstack.NewContainerV1(pClient, eo)
case "container-infra":
return openstack.NewContainerInfraV1(pClient, eo)
case "database": case "database":
return openstack.NewDBV1(pClient, eo) return openstack.NewDBV1(pClient, eo)
case "dns": case "dns":

View File

@ -4,118 +4,124 @@ package clientconfig
// The format of the clouds-public.yml is documented at // The format of the clouds-public.yml is documented at
// https://docs.openstack.org/python-openstackclient/latest/configuration/ // https://docs.openstack.org/python-openstackclient/latest/configuration/
type PublicClouds struct { 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. // Clouds represents a collection of Cloud entries in a clouds.yaml file.
// The format of clouds.yaml is documented at // The format of clouds.yaml is documented at
// https://docs.openstack.org/os-client-config/latest/user/configuration.html. // https://docs.openstack.org/os-client-config/latest/user/configuration.html.
type Clouds struct { 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. // Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file.
type Cloud struct { type Cloud struct {
Cloud string `yaml:"cloud"` Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"`
Profile string `yaml:"profile"` Profile string `yaml:"profile,omitempty" json:"profile,omitempty"`
AuthInfo *AuthInfo `yaml:"auth"` AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"`
AuthType AuthType `yaml:"auth_type"` AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"`
RegionName string `yaml:"region_name"` RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"`
Regions []interface{} `yaml:"regions"` 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. // API Version overrides.
IdentityAPIVersion string `yaml:"identity_api_version"` IdentityAPIVersion string `yaml:"identity_api_version,omitempty" json:"identity_api_version,omitempty"`
VolumeAPIVersion string `yaml:"volume_api_version"` VolumeAPIVersion string `yaml:"volume_api_version,omitempty" json:"volume_api_version,omitempty"`
// Verify whether or not SSL API requests should be verified. // 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 // CACertFile a path to a CA Cert bundle that can be used as part of
// verifying SSL API requests. // 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 // ClientCertFile a path to a client certificate to use as part of the SSL
// transaction. // 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 // ClientKeyFile a path to a client key to use as part of the SSL
// transaction. // transaction.
ClientKeyFile string `yaml:"key"` ClientKeyFile string `yaml:"key,omitempty" json:"key,omitempty"`
} }
// AuthInfo represents the auth section of a cloud entry or // AuthInfo represents the auth section of a cloud entry or
// auth options entered explicitly in ClientOpts. // auth options entered explicitly in ClientOpts.
type AuthInfo struct { type AuthInfo struct {
// AuthURL is the keystone/identity endpoint URL. // 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 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 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 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 is the password of the user.
Password string `yaml:"password"` Password string `yaml:"password,omitempty" json:"password,omitempty"`
// Application Credential ID to login with. // 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. // 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. // 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. // ProjectName is the common/human-readable name of a project.
// Users can be scoped to a project. // Users can be scoped to a project.
// ProjectName on its own is not enough to ensure a unique scope. It must // ProjectName on its own is not enough to ensure a unique scope. It must
// also be combined with either a ProjectDomainName or ProjectDomainID. // also be combined with either a ProjectDomainName or ProjectDomainID.
// ProjectName cannot be combined with ProjectID in a scope. // 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. // ProjectID is the unique ID of a project.
// It can be used to scope a user to a specific 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. // UserDomainName is the name of the domain where a user resides.
// It is used to identify the source domain of a user. // 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. // UserDomainID is the unique ID of the domain where a user resides.
// It is used to identify the source domain of a user. // 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. // ProjectDomainName is the name of the domain where a project resides.
// It is used to identify the source domain of a project. // It is used to identify the source domain of a project.
// ProjectDomainName can be used in addition to a ProjectName when scoping // ProjectDomainName can be used in addition to a ProjectName when scoping
// a user to a specific project. // 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. // ProjectDomainID is the name of the domain where a project resides.
// It is used to identify the source domain of a project. // It is used to identify the source domain of a project.
// ProjectDomainID can be used in addition to a ProjectName when scoping // ProjectDomainID can be used in addition to a ProjectName when scoping
// a user to a specific project. // 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 // DomainName is the name of a domain which can be used to identify the
// source domain of either a user or a project. // source domain of either a user or a project.
// If UserDomainName and ProjectDomainName are not specified, then DomainName // If UserDomainName and ProjectDomainName are not specified, then DomainName
// is used as a default choice. // is used as a default choice.
// It can also be used be used to specify a domain-only scope. // 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 // DomainID is the unique ID of a domain which can be used to identify the
// source domain of eitehr a user or a project. // source domain of eitehr a user or a project.
// If UserDomainID and ProjectDomainID are not specified, then DomainID is // If UserDomainID and ProjectDomainID are not specified, then DomainID is
// used as a default choice. // used as a default choice.
// It can also be used be used to specify a domain-only scope. // 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 // DefaultDomain is the domain ID to fall back on if no other domain has
// been specified and a domain is required for scope. // 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" "os/user"
"path/filepath" "path/filepath"
"reflect" "reflect"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/utils/env"
) )
// defaultIfEmpty is a helper function to make it cleaner to set default value // 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: // locations:
// //
// 1. OS_CLIENT_CONFIG_FILE // 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) // 4. unix-specific site_config_dir (/etc/openstack/clouds.yaml)
// //
// If found, the contents of the file is returned. // If found, the contents of the file is returned.
func findAndReadCloudsYAML() ([]byte, error) { func FindAndReadCloudsYAML() (string, []byte, error) {
// OS_CLIENT_CONFIG_FILE // 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 { 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) { func FindAndReadPublicCloudsYAML() (string, []byte, error) {
return findAndReadYAML("clouds-public.yaml") return FindAndReadYAML("clouds-public.yaml")
} }
func findAndReadSecureCloudsYAML() ([]byte, error) { func FindAndReadSecureCloudsYAML() (string, []byte, error) {
return findAndReadYAML("secure.yaml") return FindAndReadYAML("secure.yaml")
} }
func findAndReadYAML(yamlFile string) ([]byte, error) { func FindAndReadYAML(yamlFile string) (string, []byte, error) {
// current directory // current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { 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) filename := filepath.Join(cwd, yamlFile)
if ok := fileExists(filename); ok { if ok := fileExists(filename); ok {
return ioutil.ReadFile(filename) content, err := ioutil.ReadFile(filename)
return filename, content, err
} }
// unix user config directory: ~/.config/openstack. // unix user config directory: ~/.config/openstack.
@ -133,17 +138,20 @@ func findAndReadYAML(yamlFile string) ([]byte, error) {
if homeDir != "" { if homeDir != "" {
filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile) filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile)
if ok := fileExists(filename); ok { 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. // unix-specific site config directory: /etc/openstack.
if ok := fileExists("/etc/openstack/" + yamlFile); ok { filename = "/etc/openstack/" + yamlFile
return ioutil.ReadFile("/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. // fileExists checks for the existence of a file at a given location.
@ -153,3 +161,15 @@ func fileExists(filename string) bool {
} }
return false 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/google/uuid
# github.com/googleapis/gax-go/v2 v2.0.5 # github.com/googleapis/gax-go/v2 v2.0.5
github.com/googleapis/gax-go/v2 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
github.com/gophercloud/gophercloud/internal github.com/gophercloud/gophercloud/internal
github.com/gophercloud/gophercloud/openstack github.com/gophercloud/gophercloud/openstack
github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions
github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes 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/attachinterfaces
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume 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/keypairs
github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/startstop
github.com/gophercloud/gophercloud/openstack/compute/v2/flavors 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/compute/v2/servers
github.com/gophercloud/gophercloud/openstack/identity/v2/tenants github.com/gophercloud/gophercloud/openstack/identity/v2/tenants
github.com/gophercloud/gophercloud/openstack/identity/v2/tokens 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/identity/v3/tokens
github.com/gophercloud/gophercloud/openstack/imageservice/v2/images github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
github.com/gophercloud/gophercloud/openstack/imageservice/v2/members 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/networking/v2/subnets
github.com/gophercloud/gophercloud/openstack/utils github.com/gophercloud/gophercloud/openstack/utils
github.com/gophercloud/gophercloud/pagination 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/clientconfig
github.com/gophercloud/utils/openstack/compute/v2/flavors
# github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777 # github.com/gorilla/websocket v0.0.0-20170319172727-a91eba7f9777
github.com/gorilla/websocket github.com/gorilla/websocket
# github.com/grpc-ecosystem/go-grpc-middleware v1.1.0 # github.com/grpc-ecosystem/go-grpc-middleware v1.1.0