fix: set openstack image metadata with use_blockstorage_volume

Signed-off-by: Pratyush singhal <psinghal20@gmail.com>
This commit is contained in:
Pratyush singhal 2019-06-25 12:47:07 +05:30
parent 049811d329
commit cf8bfa56f0
38 changed files with 699 additions and 140 deletions

View File

@ -64,6 +64,15 @@ func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) mul
ui.Error(err.Error()) ui.Error(err.Error())
return multistep.ActionHalt return multistep.ActionHalt
} }
err = volumeactions.SetImageMetadata(blockStorageClient, volume, volumeactions.ImageMetadataOpts{
Metadata: config.ImageMetadata,
}).ExtractErr()
if err != nil {
err := fmt.Errorf("Error setting image metadata: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
imageId = image.ImageID imageId = image.ImageID
} else { } else {
imageId, err = servers.CreateImage(computeClient, server.ID, servers.CreateImageOpts{ imageId, err = servers.CreateImage(computeClient, server.ID, servers.CreateImageOpts{

3
go.mod
View File

@ -36,7 +36,7 @@ require (
github.com/google/go-cmp v0.2.0 github.com/google/go-cmp v0.2.0
github.com/google/shlex v0.0.0-20150127133951-6f45313302b9 github.com/google/shlex v0.0.0-20150127133951-6f45313302b9
github.com/google/uuid v1.0.0 github.com/google/uuid v1.0.0
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06 github.com/gophercloud/gophercloud v0.2.0
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6
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
@ -129,5 +129,4 @@ require (
gopkg.in/h2non/gock.v1 v1.0.12 // indirect gopkg.in/h2non/gock.v1 v1.0.12 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 // indirect gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 // indirect
gopkg.in/yaml.v2 v2.2.2 // indirect
) )

4
go.sum
View File

@ -154,6 +154,8 @@ github.com/googleapis/gax-go/v2 v2.0.3 h1:siORttZ36U2R/WjiJuDz8znElWBiAlO9rVt+mq
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06 h1:m7Rt/8En7PLrM7PQpykdZBPKUdgZWN6MwiA/ChVIoxs= github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06 h1:m7Rt/8En7PLrM7PQpykdZBPKUdgZWN6MwiA/ChVIoxs=
github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4=
github.com/gophercloud/gophercloud v0.2.0 h1:lD2Bce2xBAMNNcFZ0dObTpXkGLlVIb33RPVUNVpw6ic=
github.com/gophercloud/gophercloud v0.2.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 h1:Cw/B8Bu7Rryomxf7bjc8zNfIyLgjxsDd91n0eGRWpuo= github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6 h1:Cw/B8Bu7Rryomxf7bjc8zNfIyLgjxsDd91n0eGRWpuo=
github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw= github.com/gophercloud/utils v0.0.0-20190124192022-a5c25e7a53a6/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -455,6 +457,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
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-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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=
golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d h1:adrbvkTDn9rGnXg2IJDKozEpXXLZN89pdIA+Syt4/u0= golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d h1:adrbvkTDn9rGnXg2IJDKozEpXXLZN89pdIA+Syt4/u0=
@ -511,6 +514,7 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/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 h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= 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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -1,21 +1,25 @@
language: go language: go
sudo: false sudo: false
install: install:
- go get golang.org/x/crypto/ssh - GO111MODULE=off go get golang.org/x/crypto/ssh
- go get -v -tags 'fixtures acceptance' ./... - GO111MODULE=off go get -v -tags 'fixtures acceptance' ./...
- go get github.com/wadey/gocovmerge - GO111MODULE=off go get github.com/wadey/gocovmerge
- go get github.com/mattn/goveralls - GO111MODULE=off go get github.com/mattn/goveralls
- go get golang.org/x/tools/cmd/goimports - GO111MODULE=off go get golang.org/x/tools/cmd/goimports
go: go:
- "1.10" - "1.10"
- "1.11"
- "1.12"
- "tip" - "tip"
env: env:
global: global:
- secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ=" - secure: "xSQsAG5wlL9emjbCdxzz/hYQsSpJ/bABO1kkbwMSISVcJ3Nk0u4ywF+LS4bgeOnwPfmFvNTOqVDu3RwEvMeWXSI76t1piCPcObutb2faKLVD/hLoAS76gYX+Z8yGWGHrSB7Do5vTPj1ERe2UljdrnsSeOXzoDwFxYRaZLX4bBOB4AyoGvRniil5QXPATiA1tsWX1VMicj8a4F8X+xeESzjt1Q5Iy31e7vkptu71bhvXCaoo5QhYwT+pLR9dN0S1b7Ro0KVvkRefmr1lUOSYd2e74h6Lc34tC1h3uYZCS4h47t7v5cOXvMNxinEj2C51RvbjvZI1RLVdkuAEJD1Iz4+Ote46nXbZ//6XRZMZz/YxQ13l7ux1PFjgEB6HAapmF5Xd8PRsgeTU9LRJxpiTJ3P5QJ3leS1va8qnziM5kYipj/Rn+V8g2ad/rgkRox9LSiR9VYZD2Pe45YCb1mTKSl2aIJnV7nkOqsShY5LNB4JZSg7xIffA+9YVDktw8dJlATjZqt7WvJJ49g6A61mIUV4C15q2JPGKTkZzDiG81NtmS7hFa7k0yaE2ELgYocbcuyUcAahhxntYTC0i23nJmEHVNiZmBO3u7EgpWe4KGVfumU+lt12tIn5b3dZRBBUk3QakKKozSK1QPHGpk/AZGrhu7H6l8to6IICKWtDcyMPQ="
- GO111MODULE=on
before_script: before_script:
- go vet ./... - go vet ./...
script: script:
- ./script/coverage - ./script/coverage
- ./script/unittest
- ./script/format - ./script/format
after_success: after_success:
- $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out - $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=cover.out

View File

@ -13,6 +13,31 @@
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
- job:
name: gophercloud-acceptance-test-ironic
parent: golang-test
description: |
Run gophercloud ironic acceptance test on master branch
run: .zuul/playbooks/gophercloud-acceptance-test-ironic/run.yaml
- job:
name: gophercloud-acceptance-test-stein
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on stein branch
vars:
global_env:
OS_BRANCH: stable/stein
- job:
name: gophercloud-acceptance-test-rocky
parent: gophercloud-acceptance-test
description: |
Run gophercloud acceptance test on rocky branch
vars:
global_env:
OS_BRANCH: stable/rocky
- job: - job:
name: gophercloud-acceptance-test-queens name: gophercloud-acceptance-test-queens
parent: gophercloud-acceptance-test parent: gophercloud-acceptance-test
@ -65,6 +90,7 @@
jobs: jobs:
- gophercloud-unittest - gophercloud-unittest
- gophercloud-acceptance-test - gophercloud-acceptance-test
- gophercloud-acceptance-test-ironic
recheck-mitaka: recheck-mitaka:
jobs: jobs:
- gophercloud-acceptance-test-mitaka - gophercloud-acceptance-test-mitaka
@ -80,7 +106,9 @@
recheck-queens: recheck-queens:
jobs: jobs:
- gophercloud-acceptance-test-queens - gophercloud-acceptance-test-queens
periodic: recheck-rocky:
jobs: jobs:
- gophercloud-unittest - gophercloud-acceptance-test-rocky
- gophercloud-acceptance-test recheck-stein:
jobs:
- gophercloud-acceptance-test-stein

View File

@ -0,0 +1,40 @@
## 0.3.0 (Unreleaesd)
## 0.2.0 (June 17, 2019)
IMPROVEMENTS
* Added `networking/v2/extensions/qos/rules.ListBandwidthLimitRules` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584)
* Added `networking/v2/extensions/qos/rules.GetBandwidthLimitRule` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584)
* Added `networking/v2/extensions/qos/rules.CreateBandwidthLimitRule` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584)
* Added `networking/v2/extensions/qos/rules.UpdateBandwidthLimitRule` [GH-1589](https://github.com/gophercloud/gophercloud/pull/1589)
* Added `networking/v2/extensions/qos/rules.DeleteBandwidthLimitRule` [GH-1590](https://github.com/gophercloud/gophercloud/pull/1590)
* Added `networking/v2/extensions/qos/policies.List` [GH-1591](https://github.com/gophercloud/gophercloud/pull/1591)
* Added `networking/v2/extensions/qos/policies.Get` [GH-1593](https://github.com/gophercloud/gophercloud/pull/1593)
* Added `networking/v2/extensions/qos/rules.ListDSCPMarkingRules` [GH-1594](https://github.com/gophercloud/gophercloud/pull/1594)
* Added `networking/v2/extensions/qos/policies.Create` [GH-1595](https://github.com/gophercloud/gophercloud/pull/1595)
* Added `compute/v2/extensions/diagnostics.Get` [GH-1592](https://github.com/gophercloud/gophercloud/pull/1592)
* Added `networking/v2/extensions/qos/policies.Update` [GH-1603](https://github.com/gophercloud/gophercloud/pull/1603)
* Added `networking/v2/extensions/qos/policies.Delete` [GH-1603](https://github.com/gophercloud/gophercloud/pull/1603)
* Added `networking/v2/extensions/qos/rules.CreateDSCPMarkingRule` [GH-1605](https://github.com/gophercloud/gophercloud/pull/1605)
* Added `networking/v2/extensions/qos/rules.UpdateDSCPMarkingRule` [GH-1605](https://github.com/gophercloud/gophercloud/pull/1605)
* Added `networking/v2/extensions/qos/rules.GetDSCPMarkingRule` [GH-1609](https://github.com/gophercloud/gophercloud/pull/1609)
* Added `networking/v2/extensions/qos/rules.DeleteDSCPMarkingRule` [GH-1609](https://github.com/gophercloud/gophercloud/pull/1609)
* Added `networking/v2/extensions/qos/rules.ListMinimumBandwidthRules` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615)
* Added `networking/v2/extensions/qos/rules.GetMinimumBandwidthRule` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615)
* Added `networking/v2/extensions/qos/rules.CreateMinimumBandwidthRule` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615)
* Added `Hostname` to `baremetalintrospection/v1/introspection.Data` [GH-1627](https://github.com/gophercloud/gophercloud/pull/1627)
* Added `networking/v2/extensions/qos/rules.UpdateMinimumBandwidthRule` [GH-1624](https://github.com/gophercloud/gophercloud/pull/1624)
* Added `networking/v2/extensions/qos/rules.DeleteMinimumBandwidthRule` [GH-1624](https://github.com/gophercloud/gophercloud/pull/1624)
* Added `networking/v2/extensions/qos/ruletypes.GetRuleType` [GH-1625](https://github.com/gophercloud/gophercloud/pull/1625)
* Added `Extra` to `baremetalintrospection/v1/introspection.Data` [GH-1611](https://github.com/gophercloud/gophercloud/pull/1611)
* Added `blockstorage/extensions/volumeactions.SetImageMetadata` [GH-1621](https://github.com/gophercloud/gophercloud/pull/1621)
BUG FIXES
* Updated `networking/v2/extensions/qos/rules.UpdateBandwidthLimitRule` to use return code 200 [GH-1606](https://github.com/gophercloud/gophercloud/pull/1606)
* Fixed bug in `compute/v2/extensions/schedulerhints.SchedulerHints.Query` where contents will now be marshalled to a string [GH-1620](https://github.com/gophercloud/gophercloud/pull/1620)
## 0.1.0 (May 27, 2019)
Initial tagged release.

View File

@ -185,7 +185,6 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// Populate the request structure based on the provided arguments. Create and return an error // Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present. // if insufficient or incompatible information is present.
var req request var req request
var userRequest userReq
if opts.Password == "" { if opts.Password == "" {
if opts.TokenID != "" { if opts.TokenID != "" {
@ -229,25 +228,44 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
if opts.ApplicationCredentialSecret == "" { if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{} return nil, ErrAppCredMissingSecret{}
} }
// make sure that only one of DomainName or DomainID were provided
if opts.DomainID == "" && opts.DomainName == "" { var userRequest *userReq
return nil, ErrDomainIDOrDomainName{}
if opts.UserID != "" {
// UserID could be used without the domain information
userRequest = &userReq{
ID: &opts.UserID,
}
} }
req.Auth.Identity.Methods = []string{"application_credential"}
if opts.DomainID != "" { if userRequest == nil && opts.Username == "" {
userRequest = userReq{ // Make sure that Username or UserID are provided
return nil, ErrUsernameOrUserID{}
}
if userRequest == nil && opts.DomainID != "" {
userRequest = &userReq{
Name: &opts.Username, Name: &opts.Username,
Domain: &domainReq{ID: &opts.DomainID}, Domain: &domainReq{ID: &opts.DomainID},
} }
} else if opts.DomainName != "" { }
userRequest = userReq{
if userRequest == nil && opts.DomainName != "" {
userRequest = &userReq{
Name: &opts.Username, Name: &opts.Username,
Domain: &domainReq{Name: &opts.DomainName}, Domain: &domainReq{Name: &opts.DomainName},
} }
} }
// Make sure that DomainID or DomainName are provided among Username
if userRequest == nil {
return nil, ErrDomainIDOrDomainName{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
Name: &opts.ApplicationCredentialName, Name: &opts.ApplicationCredentialName,
User: &userRequest, User: userRequest,
Secret: &opts.ApplicationCredentialSecret, Secret: &opts.ApplicationCredentialSecret,
} }
} else { } else {

View File

@ -0,0 +1,52 @@
package gophercloud
/*
AuthResult is the result from the request that was used to obtain a provider
client's Keystone token. It is returned from ProviderClient.GetAuthResult().
The following types satisfy this interface:
github.com/gophercloud/gophercloud/openstack/identity/v2/tokens.CreateResult
github.com/gophercloud/gophercloud/openstack/identity/v3/tokens.CreateResult
Usage example:
import (
"github.com/gophercloud/gophercloud"
tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
)
func GetAuthenticatedUserID(providerClient *gophercloud.ProviderClient) (string, error) {
r := providerClient.GetAuthResult()
if r == nil {
//ProviderClient did not use openstack.Authenticate(), e.g. because token
//was set manually with ProviderClient.SetToken()
return "", errors.New("no AuthResult available")
}
switch r := r.(type) {
case tokens2.CreateResult:
u, err := r.ExtractUser()
if err != nil {
return "", err
}
return u.ID, nil
case tokens3.CreateResult:
u, err := r.ExtractUser()
if err != nil {
return "", err
}
return u.ID, nil
default:
panic(fmt.Sprintf("got unexpected AuthResult type %t", r))
}
}
Both implementing types share a lot of methods by name, like ExtractUser() in
this example. But those methods cannot be part of the AuthResult interface
because the return types are different (in this case, type tokens2.User vs.
type tokens3.User).
*/
type AuthResult interface {
ExtractTokenID() (string, error)
}

View File

@ -9,20 +9,37 @@ Provider structs represent the cloud providers that offer and manage a
collection of services. You will generally want to create one Provider collection of services. You will generally want to create one Provider
client per OpenStack cloud. client per OpenStack cloud.
It is now recommended to use the `clientconfig` package found at
https://github.com/gophercloud/utils/tree/master/openstack/clientconfig
for all authentication purposes.
The below documentation is still relevant. clientconfig simply implements
the below and presents it in an easier and more flexible way.
Use your OpenStack credentials to create a Provider client. The Use your OpenStack credentials to create a Provider client. The
IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in
information provided by the cloud operator. Additionally, the cloud may refer to information provided by the cloud operator. Additionally, the cloud may refer to
TenantID or TenantName as project_id and project_name. Credentials are TenantID or TenantName as project_id and project_name. Credentials are
specified like so: specified like so:
opts := gophercloud.AuthOptions{ opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0", IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
Username: "{username}", Username: "{username}",
Password: "{password}", Password: "{password}",
TenantID: "{tenant_id}", TenantID: "{tenant_id}",
} }
provider, err := openstack.AuthenticatedClient(opts) provider, err := openstack.AuthenticatedClient(opts)
You can authenticate with a token by doing:
opts := gophercloud.AuthOptions{
IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
TokenID: "{token_id}",
TenantID: "{tenant_id}",
}
provider, err := openstack.AuthenticatedClient(opts)
You may also use the openstack.AuthOptionsFromEnv() helper function. This You may also use the openstack.AuthOptionsFromEnv() helper function. This
function reads in standard environment variables frequently found in an function reads in standard environment variables frequently found in an
@ -39,16 +56,16 @@ operations for a particular OpenStack service. Examples of services include:
Compute, Object Storage, Block Storage. In order to define one, you need to Compute, Object Storage, Block Storage. In order to define one, you need to
pass in the parent provider, like so: pass in the parent provider, like so:
opts := gophercloud.EndpointOpts{Region: "RegionOne"} opts := gophercloud.EndpointOpts{Region: "RegionOne"}
client := openstack.NewComputeV2(provider, opts) client, err := openstack.NewComputeV2(provider, opts)
Resources Resources
Resource structs are the domain models that services make use of in order Resource structs are the domain models that services make use of in order
to work with and represent the state of API resources: to work with and represent the state of API resources:
server, err := servers.Get(client, "{serverId}").Extract() server, err := servers.Get(client, "{serverId}").Extract()
Intermediate Result structs are returned for API operations, which allow Intermediate Result structs are returned for API operations, which allow
generic access to the HTTP headers, response body, and any errors associated generic access to the HTTP headers, response body, and any errors associated
@ -56,11 +73,11 @@ with the network transaction. To turn a result into a usable resource struct,
you must call the Extract method which is chained to the response, or an you must call the Extract method which is chained to the response, or an
Extract function from an applicable extension: Extract function from an applicable extension:
result := servers.Get(client, "{serverId}") result := servers.Get(client, "{serverId}")
// Attempt to extract the disk configuration from the OS-DCF disk config // Attempt to extract the disk configuration from the OS-DCF disk config
// extension: // extension:
config, err := diskconfig.ExtractGet(result) config, err := diskconfig.ExtractGet(result)
All requests that enumerate a collection return a Pager struct that is used to All requests that enumerate a collection return a Pager struct that is used to
iterate through the results one page at a time. Use the EachPage method on that iterate through the results one page at a time. Use the EachPage method on that
@ -68,17 +85,17 @@ Pager to handle each successive Page in a closure, then use the appropriate
extraction method from that request's package to interpret that Page as a slice extraction method from that request's package to interpret that Page as a slice
of results: of results:
err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) {
s, err := servers.ExtractServers(page) s, err := servers.ExtractServers(page)
if err != nil { if err != nil {
return false, err return false, err
} }
// Handle the []servers.Server slice. // Handle the []servers.Server slice.
// Return "false" or an error to prematurely stop fetching new pages. // Return "false" or an error to prematurely stop fetching new pages.
return true, nil return true, nil
}) })
If you want to obtain the entire collection of pages without doing any If you want to obtain the entire collection of pages without doing any
intermediary processing on each page, you can use the AllPages method: intermediary processing on each page, you can use the AllPages method:

View File

@ -122,6 +122,11 @@ type ErrDefault408 struct {
ErrUnexpectedResponseCode ErrUnexpectedResponseCode
} }
// ErrDefault409 is the default error type returned on a 409 HTTP response code.
type ErrDefault409 struct {
ErrUnexpectedResponseCode
}
// ErrDefault429 is the default error type returned on a 429 HTTP response code. // ErrDefault429 is the default error type returned on a 429 HTTP response code.
type ErrDefault429 struct { type ErrDefault429 struct {
ErrUnexpectedResponseCode ErrUnexpectedResponseCode
@ -211,6 +216,12 @@ type Err408er interface {
Error408(ErrUnexpectedResponseCode) error Error408(ErrUnexpectedResponseCode) error
} }
// Err409er is the interface resource error types implement to override the error message
// from a 409 error.
type Err409er interface {
Error409(ErrUnexpectedResponseCode) error
}
// Err429er is the interface resource error types implement to override the error message // Err429er is the interface resource error types implement to override the error message
// from a 429 error. // from a 429 error.
type Err429er interface { type Err429er interface {

7
vendor/github.com/gophercloud/gophercloud/go.mod generated vendored Normal file
View File

@ -0,0 +1,7 @@
module github.com/gophercloud/gophercloud
require (
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 // indirect
gopkg.in/yaml.v2 v2.2.2
)

8
vendor/github.com/gophercloud/gophercloud/go.sum generated vendored Normal file
View File

@ -0,0 +1,8 @@
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503 h1:5SvYFrOM3W8Mexn9/oA44Ji7vhXAZQ9hiP+1Q/DMrWg=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -13,15 +13,19 @@ AuthOptionsFromEnv fills out an identity.AuthOptions structure with the
settings found on the various OpenStack OS_* environment variables. settings found on the various OpenStack OS_* environment variables.
The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME,
OS_PASSWORD, OS_TENANT_ID, and OS_TENANT_NAME. OS_PASSWORD and OS_PROJECT_ID.
Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings,
or an error will result. OS_TENANT_ID, OS_TENANT_NAME, OS_PROJECT_ID, and or an error will result. OS_PROJECT_ID, is optional.
OS_PROJECT_NAME are optional.
OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and OS_TENANT_ID and OS_TENANT_NAME are deprecated forms of OS_PROJECT_ID and
OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will OS_PROJECT_NAME and the latter are expected against a v3 auth api.
still be referred as "tenant" in Gophercloud.
If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will still be referred
as "tenant" in Gophercloud.
If OS_PROJECT_NAME is set, it requires OS_PROJECT_ID to be set as well to
handle projects not on the default domain.
To use this function, first set the OS_* environment variables (for example, To use this function, first set the OS_* environment variables (for example,
by sourcing an `openrc` file), then: by sourcing an `openrc` file), then:
@ -59,11 +63,14 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
return nilOptions, err return nilOptions, err
} }
if username == "" && userID == "" { if userID == "" && username == "" {
err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ // Empty username and userID could be ignored, when applicationCredentialID and applicationCredentialSecret are set
EnvironmentVariables: []string{"OS_USERNAME", "OS_USERID"}, if applicationCredentialID == "" && applicationCredentialSecret == "" {
err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{
EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"},
}
return nilOptions, err
} }
return nilOptions, err
} }
if password == "" && applicationCredentialID == "" && applicationCredentialName == "" { if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
@ -80,6 +87,26 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
return nilOptions, err return nilOptions, err
} }
if domainID == "" && domainName == "" && tenantID == "" && tenantName != "" {
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_PROJECT_ID",
}
return nilOptions, err
}
if applicationCredentialID == "" && applicationCredentialName != "" && applicationCredentialSecret != "" {
if userID == "" && username == "" {
return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{
EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"},
}
}
if username != "" && domainID == "" && domainName == "" {
return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{
EnvironmentVariables: []string{"OS_DOMAIN_ID", "OS_DOMAIN_NAME"},
}
}
}
ao := gophercloud.AuthOptions{ ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL, IdentityEndpoint: authURL,
UserID: userID, UserID: userID,

View File

@ -267,3 +267,34 @@ func ForceDelete(client *gophercloud.ServiceClient, id string) (r ForceDeleteRes
_, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil) _, r.Err = client.Post(actionURL(client, id), map[string]interface{}{"os-force_delete": ""}, nil, nil)
return return
} }
// ImageMetadataOptsBuilder allows extensions to add additional parameters to the
// ImageMetadataRequest request.
type ImageMetadataOptsBuilder interface {
ToImageMetadataMap() (map[string]interface{}, error)
}
// ImageMetadataOpts contains options for setting image metadata to a volume.
type ImageMetadataOpts struct {
// The image metadata to add to the volume as a set of metadata key and value pairs.
Metadata map[string]string `json:"metadata"`
}
// ToImageMetadataMap assembles a request body based on the contents of a
// ImageMetadataOpts.
func (opts ImageMetadataOpts) ToImageMetadataMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "os-set_image_metadata")
}
// SetImageMetadata will set image metadata on a volume based on the values in ImageMetadataOptsBuilder.
func SetImageMetadata(client *gophercloud.ServiceClient, id string, opts ImageMetadataOptsBuilder) (r SetImageMetadataResult) {
b, err := opts.ToImageMetadataMap()
if err != nil {
r.Err = err
return
}
_, r.Err = client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return
}

View File

@ -29,6 +29,12 @@ type UploadImageResult struct {
gophercloud.Result gophercloud.Result
} }
// SetImageMetadataResult contains the response body and error from an SetImageMetadata
// request.
type SetImageMetadataResult 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

View File

@ -38,6 +38,8 @@ type CreateOpts struct {
ImageID string `json:"imageRef,omitempty"` ImageID string `json:"imageRef,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 bool `json:"multiattach,omitempty"`
} }
// ToVolumeCreateMap assembles a request body based on the contents of a // ToVolumeCreateMap assembles a request body based on the contents of a
@ -61,9 +63,37 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
// DeleteOptsBuilder allows extensions to add additional parameters to the
// Delete request.
type DeleteOptsBuilder interface {
ToVolumeDeleteQuery() (string, error)
}
// DeleteOpts contains options for deleting a Volume. This object is passed to
// the volumes.Delete function.
type DeleteOpts struct {
// Delete all snapshots of this volume as well.
Cascade bool `q:"cascade"`
}
// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string.
func (opts DeleteOpts) ToVolumeDeleteQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
}
// Delete will delete the existing Volume with the provided ID. // Delete will delete the existing Volume with the provided ID.
func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { func Delete(client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) {
_, r.Err = client.Delete(deleteURL(client, id), nil) url := deleteURL(client, id)
if opts != nil {
query, err := opts.ToVolumeDeleteQuery()
if err != nil {
r.Err = err
return
}
url += query
}
_, r.Err = client.Delete(url, nil)
return return
} }
@ -145,8 +175,8 @@ type UpdateOptsBuilder interface {
// to the volumes.Update function. For more information about the parameters, see // to the volumes.Update function. For more information about the parameters, see
// the Volume object. // the Volume object.
type UpdateOpts struct { type UpdateOpts struct {
Name string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Description string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
} }

View File

@ -77,6 +77,8 @@ type Volume struct {
ConsistencyGroupID string `json:"consistencygroup_id"` ConsistencyGroupID string `json:"consistencygroup_id"`
// Multiattach denotes if the volume is multi-attach capable. // Multiattach denotes if the volume is multi-attach capable.
Multiattach bool `json:"multiattach"` Multiattach bool `json:"multiattach"`
// Image metadata entries, only included for volumes that were created from an image, or from a snapshot of a volume originally created from an image.
VolumeImageMetadata map[string]string `json:"volume_image_metadata"`
} }
// UnmarshalJSON another unmarshalling function // UnmarshalJSON another unmarshalling function

View File

@ -135,7 +135,7 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
result := tokens2.Create(v2Client, v2Opts) result := tokens2.Create(v2Client, v2Opts)
token, err := result.ExtractToken() err = client.SetTokenAndAuthResult(result)
if err != nil { if err != nil {
return err return err
} }
@ -150,8 +150,9 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once // this should retry authentication only once
tac := *client tac := *client
tac.SetThrowaway(true)
tac.ReauthFunc = nil tac.ReauthFunc = nil
tac.TokenID = "" tac.SetTokenAndAuthResult(nil)
tao := options tao := options
tao.AllowReauth = false tao.AllowReauth = false
client.ReauthFunc = func() error { client.ReauthFunc = func() error {
@ -159,11 +160,10 @@ func v2auth(client *gophercloud.ProviderClient, endpoint string, options gopherc
if err != nil { if err != nil {
return err return err
} }
client.TokenID = tac.TokenID client.CopyTokenFrom(&tac)
return nil return nil
} }
} }
client.TokenID = token.ID
client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
return V2EndpointURL(catalog, opts) return V2EndpointURL(catalog, opts)
} }
@ -189,7 +189,7 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
result := tokens3.Create(v3Client, opts) result := tokens3.Create(v3Client, opts)
token, err := result.ExtractToken() err = client.SetTokenAndAuthResult(result)
if err != nil { if err != nil {
return err return err
} }
@ -199,15 +199,14 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
return err return err
} }
client.TokenID = token.ID
if opts.CanReauth() { if opts.CanReauth() {
// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
// this should retry authentication only once // this should retry authentication only once
tac := *client tac := *client
tac.SetThrowaway(true)
tac.ReauthFunc = nil tac.ReauthFunc = nil
tac.TokenID = "" tac.SetTokenAndAuthResult(nil)
var tao tokens3.AuthOptionsBuilder var tao tokens3.AuthOptionsBuilder
switch ot := opts.(type) { switch ot := opts.(type) {
case *gophercloud.AuthOptions: case *gophercloud.AuthOptions:
@ -226,7 +225,7 @@ func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.Au
if err != nil { if err != nil {
return err return err
} }
client.TokenID = tac.TokenID client.CopyTokenFrom(&tac)
return nil return nil
} }
} }
@ -305,6 +304,18 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO
return sc, nil return sc, nil
} }
// NewBareMetalV1 creates a ServiceClient that may be used with the v1
// bare metal package.
func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "baremetal")
}
// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1
// bare metal introspection package.
func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
return initClientOpts(client, eo, "baremetal-inspector")
}
// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 // NewObjectStorageV1 creates a ServiceClient that may be used with the v1
// object storage package. // object storage package.
func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {

View File

@ -68,7 +68,7 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
_, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ _, r.Err = client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{
OkCodes: []int{200}, OkCodes: []int{200, 201},
}) })
return return
} }

View File

@ -148,7 +148,7 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create
return return
} }
// Get retrieves details of a single flavor. Use ExtractFlavor 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) _, r.Err = client.Get(getURL(client, id), &r.Body, nil)

View File

@ -0,0 +1,11 @@
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

@ -183,12 +183,22 @@ type CreateOpts struct {
// AccessIPv4 specifies an IPv4 address for the instance. // AccessIPv4 specifies an IPv4 address for the instance.
AccessIPv4 string `json:"accessIPv4,omitempty"` AccessIPv4 string `json:"accessIPv4,omitempty"`
// AccessIPv6 pecifies an IPv6 address for the instance. // AccessIPv6 specifies an IPv6 address for the instance.
AccessIPv6 string `json:"accessIPv6,omitempty"` AccessIPv6 string `json:"accessIPv6,omitempty"`
// Min specifies Minimum number of servers to launch.
Min int `json:"min_count,omitempty"`
// Max specifies Maximum number of servers to launch.
Max int `json:"max_count,omitempty"`
// ServiceClient will allow calls to be made to retrieve an image or // ServiceClient will allow calls to be made to retrieve an image or
// flavor ID by name. // flavor ID by name.
ServiceClient *gophercloud.ServiceClient `json:"-"` ServiceClient *gophercloud.ServiceClient `json:"-"`
// Tags allows a server to be tagged with single-word metadata.
// Requires microversion 2.52 or later.
Tags []string `json:"tags,omitempty"`
} }
// ToServerCreateMap assembles a request body based on the contents of a // ToServerCreateMap assembles a request body based on the contents of a
@ -272,6 +282,14 @@ func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) {
b["flavorRef"] = flavorID b["flavorRef"] = flavorID
} }
if opts.Min != 0 {
b["min_count"] = opts.Min
}
if opts.Max != 0 {
b["max_count"] = opts.Max
}
return map[string]interface{}{"server": b}, nil return map[string]interface{}{"server": b}, nil
} }

View File

@ -85,7 +85,7 @@ type UpdateOpts struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// Description is the description of the tenant. // Description is the description of the tenant.
Description string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
// Enabled sets the tenant status to enabled or disabled. // Enabled sets the tenant status to enabled or disabled.
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`

View File

@ -135,6 +135,21 @@ func (r CreateResult) ExtractToken() (*Token, error) {
}, nil }, nil
} }
// 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 CreateResult) ExtractTokenID() (string, error) {
var s struct {
Access struct {
Token struct {
ID string `json:"id"`
} `json:"token"`
} `json:"access"`
}
err := r.ExtractInto(&s)
return s.Access.Token.ID, 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 CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {

View File

@ -134,9 +134,9 @@ func Get(c *gophercloud.ServiceClient, token string) (r GetResult) {
OkCodes: []int{200, 203}, OkCodes: []int{200, 203},
}) })
if resp != nil { if resp != nil {
r.Err = err
r.Header = resp.Header r.Header = resp.Header
} }
r.Err = err
return return
} }

View File

@ -102,6 +102,13 @@ func (r commonResult) ExtractToken() (*Token, error) {
return &s, err return &s, 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 CreateResult) 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) {

View File

@ -321,6 +321,20 @@ func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} {
} }
} }
// ReplaceImageMinDisk represents an updated min_disk property request.
type ReplaceImageMinDisk struct {
NewMinDisk int
}
// ToImagePatchMap assembles a request body based on ReplaceImageTags.
func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} {
return map[string]interface{}{
"op": "replace",
"path": "/min_disk",
"value": r.NewMinDisk,
}
}
// UpdateOp represents a valid update operation. // UpdateOp represents a valid update operation.
type UpdateOp string type UpdateOp string

View File

@ -52,7 +52,7 @@ Example to Disassociate a Floating IP with a Port
fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7"
updateOpts := floatingips.UpdateOpts{ updateOpts := floatingips.UpdateOpts{
PortID: nil, PortID: new(string),
} }
fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract() fip, err := floatingips.Update(networkingClient, fipID, updateOpts).Extract()

View File

@ -5,6 +5,12 @@ import (
"github.com/gophercloud/gophercloud/pagination" "github.com/gophercloud/gophercloud/pagination"
) )
// ListOptsBuilder allows extensions to add additional parameters to the
// List request.
type ListOptsBuilder interface {
ToFloatingIPListQuery() (string, error)
}
// ListOpts allows the filtering and sorting of paginated collections through // ListOpts allows the filtering and sorting of paginated collections through
// the API. Filtering is achieved by passing in struct field values that map to // the API. Filtering is achieved by passing in struct field values that map to
// the floating IP attributes you want to see returned. SortKey allows you to // the floating IP attributes you want to see returned. SortKey allows you to
@ -12,6 +18,7 @@ import (
// either `asc' or `desc'. Marker and Limit are used for pagination. // either `asc' or `desc'. Marker and Limit are used for pagination.
type ListOpts struct { type ListOpts struct {
ID string `q:"id"` ID string `q:"id"`
Description string `q:"description"`
FloatingNetworkID string `q:"floating_network_id"` FloatingNetworkID string `q:"floating_network_id"`
PortID string `q:"port_id"` PortID string `q:"port_id"`
FixedIP string `q:"fixed_ip_address"` FixedIP string `q:"fixed_ip_address"`
@ -24,18 +31,31 @@ type ListOpts struct {
SortDir string `q:"sort_dir"` SortDir string `q:"sort_dir"`
RouterID string `q:"router_id"` RouterID string `q:"router_id"`
Status string `q:"status"` Status string `q:"status"`
Tags string `q:"tags"`
TagsAny string `q:"tags-any"`
NotTags string `q:"not-tags"`
NotTagsAny string `q:"not-tags-any"`
}
// ToNetworkListQuery formats a ListOpts into a query string.
func (opts ListOpts) ToFloatingIPListQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
return q.String(), err
} }
// List returns a Pager which allows you to iterate over a collection of // List returns a Pager which allows you to iterate over a collection of
// floating IP resources. It accepts a ListOpts struct, which allows you to // floating IP resources. It accepts a ListOpts struct, which allows you to
// filter and sort the returned collection for greater efficiency. // filter and sort the returned collection for greater efficiency.
func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
q, err := gophercloud.BuildQueryString(&opts) url := rootURL(c)
if err != nil { if opts != nil {
return pagination.Pager{Err: err} query, err := opts.ToFloatingIPListQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
} }
u := rootURL(c) + q.String() return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page {
return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}}
}) })
} }
@ -50,6 +70,7 @@ type CreateOptsBuilder interface {
// resource. The only required fields are FloatingNetworkID and PortID which // resource. The only required fields are FloatingNetworkID and PortID which
// refer to the external network and internal port respectively. // refer to the external network and internal port respectively.
type CreateOpts struct { type CreateOpts struct {
Description string `json:"description,omitempty"`
FloatingNetworkID string `json:"floating_network_id" required:"true"` FloatingNetworkID string `json:"floating_network_id" required:"true"`
FloatingIP string `json:"floating_ip_address,omitempty"` FloatingIP string `json:"floating_ip_address,omitempty"`
PortID string `json:"port_id,omitempty"` PortID string `json:"port_id,omitempty"`
@ -116,13 +137,24 @@ type UpdateOptsBuilder interface {
// linked to. To associate the floating IP with a new internal port, provide its // linked to. To associate the floating IP with a new internal port, provide its
// ID. To disassociate the floating IP from all ports, provide an empty string. // ID. To disassociate the floating IP from all ports, provide an empty string.
type UpdateOpts struct { type UpdateOpts struct {
PortID *string `json:"port_id"` Description *string `json:"description,omitempty"`
PortID *string `json:"port_id,omitempty"`
FixedIP string `json:"fixed_ip_address,omitempty"`
} }
// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder // ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder
// interface // interface
func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) { func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]interface{}, error) {
return gophercloud.BuildRequestBody(opts, "floatingip") b, err := gophercloud.BuildRequestBody(opts, "floatingip")
if err != nil {
return nil, err
}
if m := b["floatingip"].(map[string]interface{}); m["port_id"] == "" {
m["port_id"] = nil
}
return b, nil
} }
// Update allows floating IP resources to be updated. Currently, the only way to // Update allows floating IP resources to be updated. Currently, the only way to

View File

@ -15,6 +15,9 @@ type FloatingIP struct {
// ID is the unique identifier for the floating IP instance. // ID is the unique identifier for the floating IP instance.
ID string `json:"id"` ID string `json:"id"`
// Description for the floating IP instance.
Description string `json:"description"`
// FloatingNetworkID is the UUID of the external network where the floating // FloatingNetworkID is the UUID of the external network where the floating
// IP is to be created. // IP is to be created.
FloatingNetworkID string `json:"floating_network_id"` FloatingNetworkID string `json:"floating_network_id"`
@ -42,6 +45,9 @@ type FloatingIP struct {
// RouterID is the ID of the router used for this floating IP. // RouterID is the ID of the router used for this floating IP.
RouterID string `json:"router_id"` RouterID string `json:"router_id"`
// Tags optionally set via extensions/attributestags
Tags []string `json:"tags"`
} }
type commonResult struct { type commonResult struct {
@ -50,11 +56,13 @@ type commonResult struct {
// Extract will extract a FloatingIP resource from a result. // Extract will extract a FloatingIP resource from a result.
func (r commonResult) Extract() (*FloatingIP, error) { func (r commonResult) Extract() (*FloatingIP, error) {
var s struct { var s FloatingIP
FloatingIP *FloatingIP `json:"floatingip"`
}
err := r.ExtractInto(&s) err := r.ExtractInto(&s)
return s.FloatingIP, err return &s, err
}
func (r commonResult) ExtractInto(v interface{}) error {
return r.Result.ExtractIntoStructPtr(v, "floatingip")
} }
// CreateResult represents the result of a create operation. Call its Extract // CreateResult represents the result of a create operation. Call its Extract
@ -117,3 +125,7 @@ func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) {
err := (r.(FloatingIPPage)).ExtractInto(&s) err := (r.(FloatingIPPage)).ExtractInto(&s)
return s.FloatingIPs, err return s.FloatingIPs, err
} }
func ExtractFloatingIPsInto(r pagination.Page, v interface{}) error {
return r.(FloatingIPPage).Result.ExtractIntoSlicePtr(v, "floatingips")
}

View File

@ -45,8 +45,9 @@ Example to Update a Network
networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78"
name := "new_name"
updateOpts := networks.UpdateOpts{ updateOpts := networks.UpdateOpts{
Name: "new_name", Name: &name,
} }
network, err := networks.Update(networkClient, networkID, updateOpts).Extract() network, err := networks.Update(networkClient, networkID, updateOpts).Extract()

View File

@ -19,6 +19,7 @@ type ListOptsBuilder interface {
type ListOpts struct { type ListOpts struct {
Status string `q:"status"` Status string `q:"status"`
Name string `q:"name"` Name string `q:"name"`
Description string `q:"description"`
AdminStateUp *bool `q:"admin_state_up"` AdminStateUp *bool `q:"admin_state_up"`
TenantID string `q:"tenant_id"` TenantID string `q:"tenant_id"`
ProjectID string `q:"project_id"` ProjectID string `q:"project_id"`
@ -28,6 +29,10 @@ type ListOpts struct {
Limit int `q:"limit"` Limit int `q:"limit"`
SortKey string `q:"sort_key"` SortKey string `q:"sort_key"`
SortDir string `q:"sort_dir"` SortDir string `q:"sort_dir"`
Tags string `q:"tags"`
TagsAny string `q:"tags-any"`
NotTags string `q:"not-tags"`
NotTagsAny string `q:"not-tags-any"`
} }
// ToNetworkListQuery formats a ListOpts into a query string. // ToNetworkListQuery formats a ListOpts into a query string.
@ -69,6 +74,7 @@ type CreateOptsBuilder interface {
type CreateOpts struct { type CreateOpts struct {
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Shared *bool `json:"shared,omitempty"` Shared *bool `json:"shared,omitempty"`
TenantID string `json:"tenant_id,omitempty"` TenantID string `json:"tenant_id,omitempty"`
ProjectID string `json:"project_id,omitempty"` ProjectID string `json:"project_id,omitempty"`
@ -105,9 +111,10 @@ type UpdateOptsBuilder interface {
// UpdateOpts represents options used to update a network. // UpdateOpts represents options used to update a network.
type UpdateOpts struct { type UpdateOpts struct {
AdminStateUp *bool `json:"admin_state_up,omitempty"` AdminStateUp *bool `json:"admin_state_up,omitempty"`
Name string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Shared *bool `json:"shared,omitempty"` Description *string `json:"description,omitempty"`
Shared *bool `json:"shared,omitempty"`
} }
// ToNetworkUpdateMap builds a request body from UpdateOpts. // ToNetworkUpdateMap builds a request body from UpdateOpts.

View File

@ -52,6 +52,9 @@ type Network struct {
// Human-readable name for the network. Might not be unique. // Human-readable name for the network. Might not be unique.
Name string `json:"name"` Name string `json:"name"`
// Description for the network
Description string `json:"description"`
// The administrative state of network. If false (down), the network does not // The administrative state of network. If false (down), the network does not
// forward packets. // forward packets.
AdminStateUp bool `json:"admin_state_up"` AdminStateUp bool `json:"admin_state_up"`
@ -76,6 +79,9 @@ type Network struct {
// Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others. // Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others.
// Used to make network resources highly available. // Used to make network resources highly available.
AvailabilityZoneHints []string `json:"availability_zone_hints"` AvailabilityZoneHints []string `json:"availability_zone_hints"`
// Tags optionally set via extensions/attributestags
Tags []string `json:"tags"`
} }
// NetworkPage is the page returned by a pager when traversing over a // NetworkPage is the page returned by a pager when traversing over a

View File

@ -120,6 +120,22 @@ func BuildRequestBody(opts interface{}, parent string) (map[string]interface{},
continue continue
} }
if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) {
sliceValue := v
if sliceValue.Kind() == reflect.Ptr {
sliceValue = sliceValue.Elem()
}
for i := 0; i < sliceValue.Len(); i++ {
element := sliceValue.Index(i)
if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) {
_, err := BuildRequestBody(element.Interface(), "")
if err != nil {
return nil, err
}
}
}
}
if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) {
if zero { if zero {
//fmt.Printf("value before change: %+v\n", optsValue.Field(i)) //fmt.Printf("value before change: %+v\n", optsValue.Field(i))

View File

@ -2,7 +2,9 @@ package gophercloud
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"errors"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -71,26 +73,44 @@ type ProviderClient struct {
// authentication functions for different Identity service versions. // authentication functions for different Identity service versions.
ReauthFunc func() error ReauthFunc func() error
// Throwaway determines whether if this client is a throw-away client. It's a copy of user's provider client
// with the token and reauth func zeroed. Such client can be used to perform reauthorization.
Throwaway bool
// Context is the context passed to the HTTP request.
Context context.Context
// mut is a mutex for the client. It protects read and write access to client attributes such as getting
// and setting the TokenID.
mut *sync.RWMutex mut *sync.RWMutex
// reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication
// attempt happens at one time.
reauthmut *reauthlock reauthmut *reauthlock
authResult AuthResult
} }
// 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 reauthing bool
reauthingErr error
done *sync.Cond
} }
// AuthenticatedHeaders returns a map of HTTP headers that are common for all // AuthenticatedHeaders returns a map of HTTP headers that are common for all
// authenticated service requests. // authenticated service requests. Blocks if Reauthenticate is in progress.
func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
if client.IsThrowaway() {
return
}
if client.reauthmut != nil { if client.reauthmut != nil {
client.reauthmut.RLock() client.reauthmut.Lock()
if client.reauthmut.reauthing { for client.reauthmut.reauthing {
client.reauthmut.RUnlock() client.reauthmut.done.Wait()
return
} }
client.reauthmut.RUnlock() client.reauthmut.Unlock()
} }
t := client.Token() t := client.Token()
if t == "" { if t == "" {
@ -106,6 +126,20 @@ func (client *ProviderClient) UseTokenLock() {
client.reauthmut = new(reauthlock) client.reauthmut = new(reauthlock)
} }
// GetAuthResult returns the result from the request that was used to obtain a
// provider client's Keystone token.
//
// The result is nil when authentication has not yet taken place, when the token
// was set manually with SetToken(), or when a ReauthFunc was used that does not
// record the AuthResult.
func (client *ProviderClient) GetAuthResult() AuthResult {
if client.mut != nil {
client.mut.RLock()
defer client.mut.RUnlock()
}
return client.authResult
}
// Token safely reads the value of the auth token from the ProviderClient. Applications should // Token safely reads the value of the auth token from the ProviderClient. Applications should
// call this method to access the token instead of the TokenID field // call this method to access the token instead of the TokenID field
func (client *ProviderClient) Token() string { func (client *ProviderClient) Token() string {
@ -117,33 +151,102 @@ func (client *ProviderClient) Token() string {
} }
// SetToken safely sets the value of the auth token in the ProviderClient. Applications may // SetToken safely sets the value of the auth token in the ProviderClient. Applications may
// use this method in a custom ReauthFunc // use this method in a custom ReauthFunc.
//
// WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead.
func (client *ProviderClient) SetToken(t string) { func (client *ProviderClient) SetToken(t string) {
if client.mut != nil { if client.mut != nil {
client.mut.Lock() client.mut.Lock()
defer client.mut.Unlock() defer client.mut.Unlock()
} }
client.TokenID = t client.TokenID = t
client.authResult = nil
} }
//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is // SetTokenAndAuthResult safely sets the value of the auth token in the
//called because of a 401 response, the caller may pass the previous token. In // ProviderClient and also records the AuthResult that was returned from the
//this case, the reauthentication can be skipped if another thread has already // token creation request. Applications may call this in a custom ReauthFunc.
//reauthenticated in the meantime. If no previous token is known, an empty func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error {
//string should be passed instead to force unconditional reauthentication. tokenID := ""
var err error
if r != nil {
tokenID, err = r.ExtractTokenID()
if err != nil {
return err
}
}
if client.mut != nil {
client.mut.Lock()
defer client.mut.Unlock()
}
client.TokenID = tokenID
client.authResult = r
return nil
}
// CopyTokenFrom safely copies the token from another ProviderClient into the
// this one.
func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) {
if client.mut != nil {
client.mut.Lock()
defer client.mut.Unlock()
}
if other.mut != nil && other.mut != client.mut {
other.mut.RLock()
defer other.mut.RUnlock()
}
client.TokenID = other.TokenID
client.authResult = other.authResult
}
// IsThrowaway safely reads the value of the client Throwaway field.
func (client *ProviderClient) IsThrowaway() bool {
if client.reauthmut != nil {
client.reauthmut.RLock()
defer client.reauthmut.RUnlock()
}
return client.Throwaway
}
// SetThrowaway safely sets the value of the client Throwaway field.
func (client *ProviderClient) SetThrowaway(v bool) {
if client.reauthmut != nil {
client.reauthmut.Lock()
defer client.reauthmut.Unlock()
}
client.Throwaway = v
}
// Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
// called because of a 401 response, the caller may pass the previous token. In
// this case, the reauthentication can be skipped if another thread has already
// reauthenticated in the meantime. If no previous token is known, an empty
// string should be passed instead to force unconditional reauthentication.
func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
if client.ReauthFunc == nil { if client.ReauthFunc == nil {
return nil return nil
} }
if client.mut == nil { if client.reauthmut == nil {
return client.ReauthFunc() return client.ReauthFunc()
} }
client.mut.Lock()
defer client.mut.Unlock() client.reauthmut.Lock()
if client.reauthmut.reauthing {
for !client.reauthmut.reauthing {
client.reauthmut.done.Wait()
}
err = client.reauthmut.reauthingErr
client.reauthmut.Unlock()
return err
}
client.reauthmut.Unlock()
client.reauthmut.Lock() client.reauthmut.Lock()
client.reauthmut.reauthing = true client.reauthmut.reauthing = true
client.reauthmut.done = sync.NewCond(client.reauthmut)
client.reauthmut.reauthingErr = nil
client.reauthmut.Unlock() client.reauthmut.Unlock()
if previousToken == "" || client.TokenID == previousToken { if previousToken == "" || client.TokenID == previousToken {
@ -152,6 +255,8 @@ func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
client.reauthmut.Lock() client.reauthmut.Lock()
client.reauthmut.reauthing = false client.reauthmut.reauthing = false
client.reauthmut.reauthingErr = err
client.reauthmut.done.Broadcast()
client.reauthmut.Unlock() client.reauthmut.Unlock()
return return
} }
@ -192,7 +297,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
// io.ReadSeeker as-is. Default the content-type to application/json. // io.ReadSeeker as-is. Default the content-type to application/json.
if options.JSONBody != nil { if options.JSONBody != nil {
if options.RawBody != nil { if options.RawBody != nil {
panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().") return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
} }
rendered, err := json.Marshal(options.JSONBody) rendered, err := json.Marshal(options.JSONBody)
@ -213,6 +318,9 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if client.Context != nil {
req = req.WithContext(client.Context)
}
// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
// modify or omit any header. // modify or omit any header.
@ -251,13 +359,14 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
} }
// Allow default OkCodes if none explicitly set // Allow default OkCodes if none explicitly set
if options.OkCodes == nil { okc := options.OkCodes
options.OkCodes = defaultOkCodes(method) if okc == nil {
okc = defaultOkCodes(method)
} }
// Validate the HTTP response status. // Validate the HTTP response status.
var ok bool var ok bool
for _, code := range options.OkCodes { for _, code := range okc {
if resp.StatusCode == code { if resp.StatusCode == code {
ok = true ok = true
break break
@ -295,11 +404,7 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
seeker.Seek(0, 0) seeker.Seek(0, 0)
} }
} }
// make a new call to request with a nil reauth func in order to avoid infinite loop
reauthFunc := client.ReauthFunc
client.ReauthFunc = nil
resp, err = client.Request(method, url, options) resp, err = client.Request(method, url, options)
client.ReauthFunc = reauthFunc
if err != nil { if err != nil {
switch err.(type) { switch err.(type) {
case *ErrUnexpectedResponseCode: case *ErrUnexpectedResponseCode:
@ -338,6 +443,11 @@ func (client *ProviderClient) Request(method, url string, options *RequestOpts)
if error408er, ok := errType.(Err408er); ok { if error408er, ok := errType.(Err408er); ok {
err = error408er.Error408(respErr) err = error408er.Error408(respErr)
} }
case http.StatusConflict:
err = ErrDefault409{respErr}
if error409er, ok := errType.(Err409er); ok {
err = error409er.Error409(respErr)
}
case 429: case 429:
err = ErrDefault429{respErr} err = ErrDefault429{respErr}
if error429er, ok := errType.(Err429er); ok { if error429er, ok := errType.(Err429er); ok {

View File

@ -90,38 +90,40 @@ func (r Result) extractIntoPtr(to interface{}, label string) error {
if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous {
newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0)
for _, v := range m[label].([]interface{}) { if mSlice, ok := m[label].([]interface{}); ok {
// For each iteration of the slice, we create a new struct. for _, v := range mSlice {
// This is to work around a bug where elements of a slice // For each iteration of the slice, we create a new struct.
// are reused and not overwritten when the same copy of the // This is to work around a bug where elements of a slice
// struct is used: // are reused and not overwritten when the same copy of the
// // struct is used:
// https://github.com/golang/go/issues/21092 //
// https://github.com/golang/go/issues/24155 // https://github.com/golang/go/issues/21092
// https://play.golang.org/p/NHo3ywlPZli // https://github.com/golang/go/issues/24155
newType := reflect.New(typeOfV).Elem() // https://play.golang.org/p/NHo3ywlPZli
newType := reflect.New(typeOfV).Elem()
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil {
return err
}
// This is needed for structs with an UnmarshalJSON method.
// Technically this is just unmarshalling the response into
// a struct that is never used, but it's good enough to
// trigger the UnmarshalJSON method.
for i := 0; i < newType.NumField(); i++ {
s := newType.Field(i).Addr().Interface()
// Unmarshal is used rather than NewDecoder to also work
// around the above-mentioned bug.
err = json.Unmarshal(b, s)
if err != nil { if err != nil {
return err return err
} }
}
newSlice = reflect.Append(newSlice, newType) // This is needed for structs with an UnmarshalJSON method.
// Technically this is just unmarshalling the response into
// a struct that is never used, but it's good enough to
// trigger the UnmarshalJSON method.
for i := 0; i < newType.NumField(); i++ {
s := newType.Field(i).Addr().Interface()
// Unmarshal is used rather than NewDecoder to also work
// around the above-mentioned bug.
err = json.Unmarshal(b, s)
if err != nil {
return err
}
}
newSlice = reflect.Append(newSlice, newType)
}
} }
// "to" should now be properly modeled to receive the // "to" should now be properly modeled to receive the

View File

@ -129,6 +129,10 @@ func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) {
opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion
case "volume": case "volume":
opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion
case "baremetal":
opts.MoreHeaders["X-OpenStack-Ironic-API-Version"] = client.Microversion
case "baremetal-introspection":
opts.MoreHeaders["X-OpenStack-Ironic-Inspector-API-Version"] = client.Microversion
} }
if client.Type != "" { if client.Type != "" {

2
vendor/modules.txt vendored
View File

@ -196,7 +196,7 @@ github.com/google/shlex
github.com/google/uuid github.com/google/uuid
# github.com/googleapis/gax-go/v2 v2.0.3 # github.com/googleapis/gax-go/v2 v2.0.3
github.com/googleapis/gax-go/v2 github.com/googleapis/gax-go/v2
# github.com/gophercloud/gophercloud v0.0.0-20180903124057-ea7289ebdf06 # github.com/gophercloud/gophercloud v0.2.0
github.com/gophercloud/gophercloud github.com/gophercloud/gophercloud
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