diff --git a/.github/semver-labels.yaml b/.github/semver-labels.yaml new file mode 100644 index 0000000000..7e9c8811b5 --- /dev/null +++ b/.github/semver-labels.yaml @@ -0,0 +1,9 @@ +- name: semver:major + description: Breaking change + color: '9E1957' +- name: semver:minor + description: Backwards-compatible change + color: 'FBCA04' +- name: semver:patch + description: No API change + color: '6E7624' diff --git a/.github/workflows/functional-baremetal.yaml b/.github/workflows/functional-baremetal.yaml index a4a4038017..f9f0925d75 100644 --- a/.github/workflows/functional-baremetal.yaml +++ b/.github/workflows/functional-baremetal.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-basic.yaml b/.github/workflows/functional-basic.yaml index 78b0f9760b..778f7bcad2 100644 --- a/.github/workflows/functional-basic.yaml +++ b/.github/workflows/functional-basic.yaml @@ -41,7 +41,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} enabled_services: 's-account,s-container,s-object,s-proxy' diff --git a/.github/workflows/functional-blockstorage.yaml b/.github/workflows/functional-blockstorage.yaml index 6878da6d94..8baed7dc45 100644 --- a/.github/workflows/functional-blockstorage.yaml +++ b/.github/workflows/functional-blockstorage.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-clustering.yaml b/.github/workflows/functional-clustering.yaml index 0c6247887b..9bd52ee967 100644 --- a/.github/workflows/functional-clustering.yaml +++ b/.github/workflows/functional-clustering.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-compute.yaml b/.github/workflows/functional-compute.yaml index 8e9ad33b49..f15786a2b0 100644 --- a/.github/workflows/functional-compute.yaml +++ b/.github/workflows/functional-compute.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-containerinfra.yaml b/.github/workflows/functional-containerinfra.yaml index 6cccd1cb3b..8e320a1ff5 100644 --- a/.github/workflows/functional-containerinfra.yaml +++ b/.github/workflows/functional-containerinfra.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-dns.yaml b/.github/workflows/functional-dns.yaml index 6e9f6ea389..782c9e05d4 100644 --- a/.github/workflows/functional-dns.yaml +++ b/.github/workflows/functional-dns.yaml @@ -39,7 +39,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-identity.yaml b/.github/workflows/functional-identity.yaml index fcddd1db5a..58b7c08434 100644 --- a/.github/workflows/functional-identity.yaml +++ b/.github/workflows/functional-identity.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} - name: Checkout go diff --git a/.github/workflows/functional-imageservice.yaml b/.github/workflows/functional-imageservice.yaml index 83a17356e1..5059267da8 100644 --- a/.github/workflows/functional-imageservice.yaml +++ b/.github/workflows/functional-imageservice.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} - name: Checkout go diff --git a/.github/workflows/functional-keymanager.yaml b/.github/workflows/functional-keymanager.yaml index 994aa3fe9b..53f0f0e15f 100644 --- a/.github/workflows/functional-keymanager.yaml +++ b/.github/workflows/functional-keymanager.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-loadbalancer.yaml b/.github/workflows/functional-loadbalancer.yaml index 827be255b1..e73fb303b7 100644 --- a/.github/workflows/functional-loadbalancer.yaml +++ b/.github/workflows/functional-loadbalancer.yaml @@ -38,13 +38,13 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | enable_plugin octavia https://opendev.org/openstack/octavia ${{ matrix.openstack_version }} enable_plugin neutron https://opendev.org/openstack/neutron ${{ matrix.openstack_version }} - enabled_services: 'octavia,o-api,o-cw,o-hk,o-hm,o-da' + enabled_services: 'octavia,o-api,o-cw,o-hk,o-hm,o-da,neutron-qos' - name: Checkout go uses: actions/setup-go@v3 with: diff --git a/.github/workflows/functional-messaging.yaml b/.github/workflows/functional-messaging.yaml index 6a8adf9fac..8215fa4c38 100644 --- a/.github/workflows/functional-messaging.yaml +++ b/.github/workflows/functional-messaging.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-networking.yaml b/.github/workflows/functional-networking.yaml index 315ed5a2ae..dd1ccd7ff5 100644 --- a/.github/workflows/functional-networking.yaml +++ b/.github/workflows/functional-networking.yaml @@ -61,7 +61,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-objectstorage.yaml b/.github/workflows/functional-objectstorage.yaml index 85e6107828..f87be4b502 100644 --- a/.github/workflows/functional-objectstorage.yaml +++ b/.github/workflows/functional-objectstorage.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-orchestration.yaml b/.github/workflows/functional-orchestration.yaml index 28d3b291a6..f3030bf2db 100644 --- a/.github/workflows/functional-orchestration.yaml +++ b/.github/workflows/functional-orchestration.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/functional-placement.yaml b/.github/workflows/functional-placement.yaml index ea514716ff..5f6ba1fcb7 100644 --- a/.github/workflows/functional-placement.yaml +++ b/.github/workflows/functional-placement.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} - name: Checkout go diff --git a/.github/workflows/functional-sharedfilesystems.yaml b/.github/workflows/functional-sharedfilesystems.yaml index 7884bda683..83c9d97519 100644 --- a/.github/workflows/functional-sharedfilesystems.yaml +++ b/.github/workflows/functional-sharedfilesystems.yaml @@ -38,7 +38,7 @@ jobs: - name: Checkout Gophercloud uses: actions/checkout@v3 - name: Deploy devstack - uses: EmilienM/devstack-action@v0.7 + uses: EmilienM/devstack-action@v0.9 with: branch: ${{ matrix.openstack_version }} conf_overrides: | diff --git a/.github/workflows/semver-labels.yaml b/.github/workflows/semver-labels.yaml new file mode 100644 index 0000000000..ccaf44522b --- /dev/null +++ b/.github/workflows/semver-labels.yaml @@ -0,0 +1,18 @@ +name: Ensure labels +on: + push: + branches: + - master + paths: + - .github/semver-labels.yaml + - .github/workflows/semver-labels.yaml +jobs: + semver: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: micnncim/action-label-syncer@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/semver-labels.yaml diff --git a/.github/workflows/semver-require.yaml b/.github/workflows/semver-require.yaml new file mode 100644 index 0000000000..58cc23b54c --- /dev/null +++ b/.github/workflows/semver-require.yaml @@ -0,0 +1,17 @@ +name: Verify PR Labels +on: + pull_request: + types: + - opened + - labeled + - unlabeled + - synchronize +jobs: + semver: + runs-on: ubuntu-latest + steps: + - uses: mheap/github-action-required-labels@v2 + with: + mode: exactly + count: 1 + labels: "semver:patch, semver:minor, semver:major" diff --git a/.github/workflows/semver-unlabel.yaml b/.github/workflows/semver-unlabel.yaml new file mode 100644 index 0000000000..9fdf5558e4 --- /dev/null +++ b/.github/workflows/semver-unlabel.yaml @@ -0,0 +1,21 @@ +name: Reset PR labels on push + +# **What it does**: When the content of a PR changes, this workflow removes the semver label +# **Why we have it**: To make sure semver labels are up-to-date. +# **Who does it impact**: Pull requests. + +on: + pull_request_target: + types: + - synchronize + +jobs: + semver: + runs-on: ubuntu-latest + steps: + - name: Remove the semver label + uses: andymckay/labeler@1.0.4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + remove-labels: "semver:patch, semver:minor, semver:major" diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index bca05da058..dd6832b2af 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -29,6 +29,10 @@ jobs: - name: Setup environment run: | + # Changing into a different directory to avoid polluting go.sum with "go get" + cd "$(mktemp -d)" + + # we use "go get" for Go v1.14 go install github.com/wadey/gocovmerge@master || go get github.com/wadey/gocovmerge go install golang.org/x/tools/cmd/goimports@latest || go get golang.org/x/tools/cmd/goimports diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8137c10d..2cef7d88e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## v1.1.0 (2022-11-24) + +* [GH-2513](https://github.com/gophercloud/gophercloud/pull/2513) objectstorage: Do not parse NoContent responses +* [GH-2503](https://github.com/gophercloud/gophercloud/pull/2503) Bump golang.org/x/crypto +* [GH-2501](https://github.com/gophercloud/gophercloud/pull/2501) Staskraev/l3 agent scheduler +* [GH-2496](https://github.com/gophercloud/gophercloud/pull/2496) Manila: add Get for share-access-rules API +* [GH-2491](https://github.com/gophercloud/gophercloud/pull/2491) Add VipQosPolicyID to loadbalancer Create and Update +* [GH-2488](https://github.com/gophercloud/gophercloud/pull/2488) Add Persistance for octavia pools.UpdateOpts +* [GH-2487](https://github.com/gophercloud/gophercloud/pull/2487) Add Prometheus protocol for octavia listeners +* [GH-2482](https://github.com/gophercloud/gophercloud/pull/2482) Add createdAt, updatedAt and provisionUpdatedAt fields in Baremetal V1 nodes +* [GH-2479](https://github.com/gophercloud/gophercloud/pull/2479) Add service_types support for neutron subnet +* [GH-2477](https://github.com/gophercloud/gophercloud/pull/2477) Port CreatedAt and UpdatedAt: add back JSON tags +* [GH-2475](https://github.com/gophercloud/gophercloud/pull/2475) Support old time format for port CreatedAt and UpdatedAt +* [GH-2474](https://github.com/gophercloud/gophercloud/pull/2474) Implementing re-image volumeaction +* [GH-2470](https://github.com/gophercloud/gophercloud/pull/2470) keystone: add v3 limits GetEnforcementModel operation +* [GH-2468](https://github.com/gophercloud/gophercloud/pull/2468) keystone: add v3 OS-FEDERATION extension List Mappings +* [GH-2458](https://github.com/gophercloud/gophercloud/pull/2458) Fix typo in blockstorage/v3/attachments docs +* [GH-2456](https://github.com/gophercloud/gophercloud/pull/2456) Add support for Update for flavors +* [GH-2453](https://github.com/gophercloud/gophercloud/pull/2453) Add description to flavor +* [GH-2417](https://github.com/gophercloud/gophercloud/pull/2417) Neutron v2: ScheduleBGPSpeakerOpts, RemoveBGPSpeaker, Lis… + ## 1.0.0 (2022-08-29) UPGRADE NOTES + PROMISE OF COMPATIBILITY diff --git a/acceptance/openstack/blockstorage/extensions/extensions.go b/acceptance/openstack/blockstorage/extensions/extensions.go index d9713bf9c1..d15e4b652d 100644 --- a/acceptance/openstack/blockstorage/extensions/extensions.go +++ b/acceptance/openstack/blockstorage/extensions/extensions.go @@ -313,3 +313,38 @@ func ChangeVolumeType(t *testing.T, client *gophercloud.ServiceClient, volume *v return nil } + +// ReImage will re-image a volume +func ReImage(t *testing.T, client *gophercloud.ServiceClient, volume *volumes.Volume, imageID string) error { + t.Logf("Attempting to re-image volume %s", volume.ID) + + reimageOpts := volumeactions.ReImageOpts{ + ImageID: imageID, + ReImageReserved: false, + } + + err := volumeactions.ReImage(client, volume.ID, reimageOpts).ExtractErr() + if err != nil { + return err + } + + err = volumes.WaitForStatus(client, volume.ID, "available", 60) + if err != nil { + return err + } + + vol, err := v3.Get(client, volume.ID).Extract() + if err != nil { + return err + } + + if vol.VolumeImageMetadata == nil { + return fmt.Errorf("volume does not have VolumeImageMetadata map") + } + + if strings.ToLower(vol.VolumeImageMetadata["image_id"]) != imageID { + return fmt.Errorf("volume image id '%s', expected '%s'", vol.VolumeImageMetadata["image_id"], imageID) + } + + return nil +} diff --git a/acceptance/openstack/blockstorage/extensions/volumeactions_test.go b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go index 264d0efc94..3c69d17a46 100644 --- a/acceptance/openstack/blockstorage/extensions/volumeactions_test.go +++ b/acceptance/openstack/blockstorage/extensions/volumeactions_test.go @@ -145,6 +145,26 @@ func TestVolumeActionsChangeType(t *testing.T) { tools.PrintResource(t, newVolume) } +func TestVolumeActionsReImage(t *testing.T) { + clients.SkipReleasesBelow(t, "stable/yoga") + + choices, err := clients.AcceptanceTestChoicesFromEnv() + if err != nil { + t.Fatal(err) + } + + blockClient, err := clients.NewBlockStorageV3Client() + th.AssertNoErr(t, err) + blockClient.Microversion = "3.68" + + volume, err := blockstorage.CreateVolume(t, blockClient) + th.AssertNoErr(t, err) + defer blockstorage.DeleteVolume(t, blockClient, volume) + + err = ReImage(t, blockClient, volume, choices.ImageID) + th.AssertNoErr(t, err) +} + // Note(jtopjian): I plan to work on this at some point, but it requires // setting up a server with iscsi utils. /* diff --git a/acceptance/openstack/compute/v2/compute.go b/acceptance/openstack/compute/v2/compute.go index 21c94192d0..c34a5f5774 100644 --- a/acceptance/openstack/compute/v2/compute.go +++ b/acceptance/openstack/compute/v2/compute.go @@ -208,15 +208,20 @@ func CreateDefaultRule(t *testing.T, client *gophercloud.ServiceClient) (dsr.Def // An error will be returned if the flavor could not be created. func CreateFlavor(t *testing.T, client *gophercloud.ServiceClient) (*flavors.Flavor, error) { flavorName := tools.RandomString("flavor_", 5) + flavorDescription := fmt.Sprintf("I am %s and i am a yummy flavor", flavorName) + + // Microversion 2.55 is required to add description to flavor + client.Microversion = "2.55" t.Logf("Attempting to create flavor %s", flavorName) isPublic := true createOpts := flavors.CreateOpts{ - Name: flavorName, - RAM: 1, - VCPUs: 1, - Disk: gophercloud.IntToPointer(1), - IsPublic: &isPublic, + Name: flavorName, + RAM: 1, + VCPUs: 1, + Disk: gophercloud.IntToPointer(1), + IsPublic: &isPublic, + Description: flavorDescription, } flavor, err := flavors.Create(client, createOpts).Extract() @@ -231,6 +236,7 @@ func CreateFlavor(t *testing.T, client *gophercloud.ServiceClient) (*flavors.Fla th.AssertEquals(t, flavor.Disk, 1) th.AssertEquals(t, flavor.VCPUs, 1) th.AssertEquals(t, flavor.IsPublic, true) + th.AssertEquals(t, flavor.Description, flavorDescription) return flavor, nil } diff --git a/acceptance/openstack/compute/v2/flavors_test.go b/acceptance/openstack/compute/v2/flavors_test.go index c58f99452e..9e3ec1db45 100644 --- a/acceptance/openstack/compute/v2/flavors_test.go +++ b/acceptance/openstack/compute/v2/flavors_test.go @@ -90,6 +90,30 @@ func TestFlavorsCreateDelete(t *testing.T) { tools.PrintResource(t, flavor) } +func TestFlavorsCreateUpdateDelete(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewComputeV2Client() + th.AssertNoErr(t, err) + + flavor, err := CreateFlavor(t, client) + th.AssertNoErr(t, err) + defer DeleteFlavor(t, client, flavor) + + tools.PrintResource(t, flavor) + + newFlavorDescription := "This is the new description" + updateOpts := flavors.UpdateOpts{ + Description: newFlavorDescription, + } + + flavor, err = flavors.Update(client, flavor.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, flavor.Description, newFlavorDescription) + + tools.PrintResource(t, flavor) +} + func TestFlavorsAccessesList(t *testing.T) { clients.RequireAdmin(t) diff --git a/acceptance/openstack/identity/v3/domains_test.go b/acceptance/openstack/identity/v3/domains_test.go index 48ad8247c1..5e9d06b266 100644 --- a/acceptance/openstack/identity/v3/domains_test.go +++ b/acceptance/openstack/identity/v3/domains_test.go @@ -12,6 +12,23 @@ import ( th "github.com/gophercloud/gophercloud/testhelper" ) +func TestDomainsListAvailable(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + allPages, err := domains.ListAvailable(client).AllPages() + th.AssertNoErr(t, err) + + allDomains, err := domains.ExtractDomains(allPages) + th.AssertNoErr(t, err) + + for _, domain := range allDomains { + tools.PrintResource(t, domain) + } +} + func TestDomainsList(t *testing.T) { clients.RequireAdmin(t) diff --git a/acceptance/openstack/identity/v3/federation_test.go b/acceptance/openstack/identity/v3/federation_test.go new file mode 100644 index 0000000000..8afc7f9ad2 --- /dev/null +++ b/acceptance/openstack/identity/v3/federation_test.go @@ -0,0 +1,26 @@ +//go:build acceptance +// +build acceptance + +package v3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/federation" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestListMappings(t *testing.T) { + client, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + allPages, err := federation.ListMappings(client).AllPages() + th.AssertNoErr(t, err) + + mappings, err := federation.ExtractMappings(allPages) + th.AssertNoErr(t, err) + + tools.PrintResource(t, mappings) +} diff --git a/acceptance/openstack/identity/v3/limits_test.go b/acceptance/openstack/identity/v3/limits_test.go index 1bb7b1e921..2fe5787c58 100644 --- a/acceptance/openstack/identity/v3/limits_test.go +++ b/acceptance/openstack/identity/v3/limits_test.go @@ -7,10 +7,23 @@ import ( "testing" "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" "github.com/gophercloud/gophercloud/openstack/identity/v3/limits" th "github.com/gophercloud/gophercloud/testhelper" ) +func TestGetEnforcementModel(t *testing.T) { + clients.RequireAdmin(t) + + client, err := clients.NewIdentityV3Client() + th.AssertNoErr(t, err) + + model, err := limits.GetEnforcementModel(client).Extract() + th.AssertNoErr(t, err) + + tools.PrintResource(t, model) +} + func TestLimitsList(t *testing.T) { clients.RequireAdmin(t) diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/acceptance/openstack/loadbalancer/v2/loadbalancer.go index f8d44a8b17..8d5efb011d 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancer.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancer.go @@ -110,7 +110,7 @@ func CreateListenerHTTP(t *testing.T, client *gophercloud.ServiceClient, lb *loa // CreateLoadBalancer will create a load balancer with a random name on a given // subnet. An error will be returned if the loadbalancer could not be created. -func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetID string, tags []string) (*loadbalancers.LoadBalancer, error) { +func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetID string, tags []string, policyID string) (*loadbalancers.LoadBalancer, error) { lbName := tools.RandomString("TESTACCT-", 8) lbDescription := tools.RandomString("TESTACCT-DESC-", 8) @@ -126,6 +126,10 @@ func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetI createOpts.Tags = tags } + if len(policyID) > 0 { + createOpts.VipQosPolicyID = policyID + } + lb, err := loadbalancers.Create(client, createOpts).Extract() if err != nil { return lb, err @@ -149,6 +153,10 @@ func CreateLoadBalancer(t *testing.T, client *gophercloud.ServiceClient, subnetI th.AssertDeepEquals(t, lb.Tags, tags) } + if len(policyID) > 0 { + th.AssertEquals(t, lb.VipQosPolicyID, policyID) + } + return lb, nil } diff --git a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go index c21663ef35..2a987bd9b9 100644 --- a/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go +++ b/acceptance/openstack/loadbalancer/v2/loadbalancers_test.go @@ -8,6 +8,7 @@ import ( "github.com/gophercloud/gophercloud/acceptance/clients" networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/qos/policies" "github.com/gophercloud/gophercloud/acceptance/tools" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/l7policies" "github.com/gophercloud/gophercloud/openstack/loadbalancer/v2/listeners" @@ -50,7 +51,7 @@ func TestLoadbalancersListByTags(t *testing.T) { // Add "test" tag intentionally to test the "not-tags" parameter. Because "test" tag is also used in other test // cases, we use "test" tag to exclude load balancers created by other test case. tags := []string{"tag1", "tag2", "test"} - lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags) + lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags, "") th.AssertNoErr(t, err) defer DeleteLoadBalancer(t, lbClient, lb.ID) @@ -110,7 +111,7 @@ func TestLoadbalancerHTTPCRUD(t *testing.T) { th.AssertNoErr(t, err) defer networking.DeleteSubnet(t, netClient, subnet.ID) - lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, nil) + lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, nil, "") th.AssertNoErr(t, err) defer DeleteLoadBalancer(t, lbClient, lb.ID) @@ -239,6 +240,12 @@ func TestLoadbalancersCRUD(t *testing.T) { netClient, err := clients.NewNetworkV2Client() th.AssertNoErr(t, err) + // Create QoS policy first as the loadbalancer and its port + //needs to be deleted before the QoS policy can be deleted + policy2, err := policies.CreateQoSPolicy(t, netClient) + th.AssertNoErr(t, err) + defer policies.DeleteQoSPolicy(t, netClient, policy2.ID) + lbClient, err := clients.NewLoadBalancerV2Client() th.AssertNoErr(t, err) @@ -250,14 +257,20 @@ func TestLoadbalancersCRUD(t *testing.T) { th.AssertNoErr(t, err) defer networking.DeleteSubnet(t, netClient, subnet.ID) + policy1, err := policies.CreateQoSPolicy(t, netClient) + th.AssertNoErr(t, err) + defer policies.DeleteQoSPolicy(t, netClient, policy1.ID) + tags := []string{"test"} - lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags) + lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags, policy1.ID) th.AssertNoErr(t, err) + th.AssertEquals(t, lb.VipQosPolicyID, policy1.ID) defer DeleteLoadBalancer(t, lbClient, lb.ID) lbDescription := "" updateLoadBalancerOpts := loadbalancers.UpdateOpts{ - Description: &lbDescription, + Description: &lbDescription, + VipQosPolicyID: &policy2.ID, } _, err = loadbalancers.Update(lbClient, lb.ID, updateLoadBalancerOpts).Extract() th.AssertNoErr(t, err) @@ -272,6 +285,7 @@ func TestLoadbalancersCRUD(t *testing.T) { tools.PrintResource(t, newLB) th.AssertEquals(t, newLB.Description, lbDescription) + th.AssertEquals(t, newLB.VipQosPolicyID, policy2.ID) lbStats, err := loadbalancers.GetStats(lbClient, lb.ID).Extract() th.AssertNoErr(t, err) @@ -449,7 +463,7 @@ func TestLoadbalancersCascadeCRUD(t *testing.T) { defer networking.DeleteSubnet(t, netClient, subnet.ID) tags := []string{"test"} - lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags) + lb, err := CreateLoadBalancer(t, lbClient, subnet.ID, tags, "") th.AssertNoErr(t, err) defer CascadeDeleteLoadBalancer(t, lbClient, lb.ID) diff --git a/acceptance/openstack/networking/v2/extensions/agents/agents_test.go b/acceptance/openstack/networking/v2/extensions/agents/agents_test.go index 3be89554b8..db34e75a1c 100644 --- a/acceptance/openstack/networking/v2/extensions/agents/agents_test.go +++ b/acceptance/openstack/networking/v2/extensions/agents/agents_test.go @@ -5,11 +5,14 @@ package agents import ( "testing" + "time" "github.com/gophercloud/gophercloud/acceptance/clients" networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + spk "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2/extensions/bgp/speakers" "github.com/gophercloud/gophercloud/acceptance/tools" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/agents" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/bgp/speakers" th "github.com/gophercloud/gophercloud/testhelper" ) @@ -92,3 +95,106 @@ func TestAgentsRUD(t *testing.T) { err = agents.Delete(client, allAgents[0].ID).ExtractErr() th.AssertNoErr(t, err) } + +func TestBGPAgentRUD(t *testing.T) { + timeout := 120 * time.Second + clients.RequireAdmin(t) + + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // List BGP Agents + listOpts := &agents.ListOpts{ + AgentType: "BGP Dynamic Routing Agent", + } + allPages, err := agents.List(client, listOpts).AllPages() + th.AssertNoErr(t, err) + + allAgents, err := agents.ExtractAgents(allPages) + th.AssertNoErr(t, err) + + t.Logf("Retrieved BGP agents") + tools.PrintResource(t, allAgents) + + // Create a BGP Speaker + bgpSpeaker, err := spk.CreateBGPSpeaker(t, client) + th.AssertNoErr(t, err) + pages, err := agents.ListDRAgentHostingBGPSpeakers(client, bgpSpeaker.ID).AllPages() + th.AssertNoErr(t, err) + bgpAgents, err := agents.ExtractAgents(pages) + th.AssertNoErr(t, err) + th.AssertIntGreaterOrEqual(t, len(bgpAgents), 1) + + // List the BGP Agents that accommodate the BGP Speaker + err = tools.WaitForTimeout( + func() (bool, error) { + flag := true + for _, agt := range bgpAgents { + t.Logf("BGP Speaker %s has been scheduled to agent %s", bgpSpeaker.ID, agt.ID) + bgpAgent, err := agents.Get(client, agt.ID).Extract() + th.AssertNoErr(t, err) + numOfSpeakers := int(bgpAgent.Configurations["bgp_speakers"].(float64)) + flag = flag && (numOfSpeakers == 1) + } + return flag, nil + }, timeout) + th.AssertNoErr(t, err) + + // Remove the BGP Speaker from the first agent + err = agents.RemoveBGPSpeaker(client, bgpAgents[0].ID, bgpSpeaker.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("BGP Speaker %s has been removed from agent %s", bgpSpeaker.ID, bgpAgents[0].ID) + err = tools.WaitForTimeout( + func() (bool, error) { + bgpAgent, err := agents.Get(client, bgpAgents[0].ID).Extract() + th.AssertNoErr(t, err) + agentConf := bgpAgent.Configurations + numOfSpeakers := int(agentConf["bgp_speakers"].(float64)) + t.Logf("Agent %s has %d speakers", bgpAgent.ID, numOfSpeakers) + return numOfSpeakers == 0, nil + }, timeout) + th.AssertNoErr(t, err) + + // Remove all BGP Speakers from the agent + pages, err = agents.ListBGPSpeakers(client, bgpAgents[0].ID).AllPages() + th.AssertNoErr(t, err) + allSpeakers, err := agents.ExtractBGPSpeakers(pages) + th.AssertNoErr(t, err) + for _, speaker := range allSpeakers { + th.AssertNoErr(t, agents.RemoveBGPSpeaker(client, bgpAgents[0].ID, speaker.ID).ExtractErr()) + } + + // Schedule a BGP Speaker to an agent + opts := agents.ScheduleBGPSpeakerOpts{ + SpeakerID: bgpSpeaker.ID, + } + err = agents.ScheduleBGPSpeaker(client, bgpAgents[0].ID, opts).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully scheduled speaker %s to agent %s", bgpSpeaker.ID, bgpAgents[0].ID) + + err = tools.WaitForTimeout( + func() (bool, error) { + bgpAgent, err := agents.Get(client, bgpAgents[0].ID).Extract() + th.AssertNoErr(t, err) + agentConf := bgpAgent.Configurations + numOfSpeakers := int(agentConf["bgp_speakers"].(float64)) + t.Logf("Agent %s has %d speakers", bgpAgent.ID, numOfSpeakers) + return 1 == numOfSpeakers, nil + }, timeout) + th.AssertNoErr(t, err) + + // Delete the BGP Speaker + speakers.Delete(client, bgpSpeaker.ID).ExtractErr() + th.AssertNoErr(t, err) + t.Logf("Successfully deleted the BGP Speaker, %s", bgpSpeaker.ID) + err = tools.WaitForTimeout( + func() (bool, error) { + bgpAgent, err := agents.Get(client, bgpAgents[0].ID).Extract() + th.AssertNoErr(t, err) + agentConf := bgpAgent.Configurations + numOfSpeakers := int(agentConf["bgp_speakers"].(float64)) + t.Logf("Agent %s has %d speakers", bgpAgent.ID, numOfSpeakers) + return 0 == numOfSpeakers, nil + }, timeout) + th.AssertNoErr(t, err) +} diff --git a/acceptance/openstack/networking/v2/extensions/layer3/l3_scheduling_test.go b/acceptance/openstack/networking/v2/extensions/layer3/l3_scheduling_test.go new file mode 100644 index 0000000000..ec32dfda11 --- /dev/null +++ b/acceptance/openstack/networking/v2/extensions/layer3/l3_scheduling_test.go @@ -0,0 +1,82 @@ +//go:build acceptance || networking || layer3 || router +// +build acceptance networking layer3 router + +package layer3 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + networking "github.com/gophercloud/gophercloud/acceptance/openstack/networking/v2" + "github.com/gophercloud/gophercloud/acceptance/tools" + "github.com/gophercloud/gophercloud/openstack/common/extensions" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/agents" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestLayer3RouterScheduling(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + _, err = extensions.Get(client, "l3_agent_scheduler").Extract() + if err != nil { + t.Skip("Extension l3_agent_scheduler not present") + } + + network, err := networking.CreateNetwork(t, client) + th.AssertNoErr(t, err) + defer networking.DeleteNetwork(t, client, network.ID) + + subnet, err := networking.CreateSubnet(t, client, network.ID) + th.AssertNoErr(t, err) + defer networking.DeleteSubnet(t, client, subnet.ID) + + router, err := CreateRouter(t, client, network.ID) + th.AssertNoErr(t, err) + defer DeleteRouter(t, client, router.ID) + tools.PrintResource(t, router) + + routerInterface, err := CreateRouterInterfaceOnSubnet(t, client, subnet.ID, router.ID) + tools.PrintResource(t, routerInterface) + th.AssertNoErr(t, err) + defer DeleteRouterInterface(t, client, routerInterface.PortID, router.ID) + + // List hosting agent + allPages, err := routers.ListL3Agents(client, router.ID).AllPages() + th.AssertNoErr(t, err) + hostingAgents, err := routers.ExtractL3Agents(allPages) + th.AssertNoErr(t, err) + th.AssertIntGreaterOrEqual(t, len(hostingAgents), 1) + hostingAgent := hostingAgents[0] + t.Logf("Router %s is scheduled on %s", router.ID, hostingAgent.ID) + + // remove from hosting agent + err = agents.RemoveL3Router(client, hostingAgent.ID, router.ID).ExtractErr() + th.AssertNoErr(t, err) + + containsRouterFunc := func(rs []routers.Router, routerID string) bool { + for _, r := range rs { + if r.ID == router.ID { + return true + } + } + return false + } + + // List routers on hosting agent + routersOnHostingAgent, err := agents.ListL3Routers(client, hostingAgent.ID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, containsRouterFunc(routersOnHostingAgent, router.ID), false) + t.Logf("Router %s is not scheduled on %s", router.ID, hostingAgent.ID) + + // schedule back + err = agents.ScheduleL3Router(client, hostingAgents[0].ID, agents.ScheduleL3RouterOpts{RouterID: router.ID}).ExtractErr() + th.AssertNoErr(t, err) + + // List hosting agent after readding + routersOnHostingAgent, err = agents.ListL3Routers(client, hostingAgent.ID).Extract() + th.AssertNoErr(t, err) + th.AssertEquals(t, containsRouterFunc(routersOnHostingAgent, router.ID), true) + t.Logf("Router %s is scheduled on %s", router.ID, hostingAgent.ID) +} diff --git a/acceptance/openstack/networking/v2/networking.go b/acceptance/openstack/networking/v2/networking.go index 9362c9b623..aae456b198 100644 --- a/acceptance/openstack/networking/v2/networking.go +++ b/acceptance/openstack/networking/v2/networking.go @@ -312,6 +312,45 @@ func CreateSubnet(t *testing.T, client *gophercloud.ServiceClient, networkID str return subnet, nil } +// CreateSubnet will create a subnet on the specified Network ID and service types. +// +// An error will be returned if the subnet could not be created. +func CreateSubnetWithServiceTypes(t *testing.T, client *gophercloud.ServiceClient, networkID string) (*subnets.Subnet, error) { + subnetName := tools.RandomString("TESTACC-", 8) + subnetDescription := tools.RandomString("TESTACC-DESC-", 8) + subnetOctet := tools.RandomInt(1, 250) + subnetCIDR := fmt.Sprintf("192.168.%d.0/24", subnetOctet) + subnetGateway := fmt.Sprintf("192.168.%d.1", subnetOctet) + serviceTypes := []string{"network:routed"} + createOpts := subnets.CreateOpts{ + NetworkID: networkID, + CIDR: subnetCIDR, + IPVersion: 4, + Name: subnetName, + Description: subnetDescription, + EnableDHCP: gophercloud.Disabled, + GatewayIP: &subnetGateway, + ServiceTypes: serviceTypes, + } + + t.Logf("Attempting to create subnet: %s", subnetName) + + subnet, err := subnets.Create(client, createOpts).Extract() + if err != nil { + return subnet, err + } + + t.Logf("Successfully created subnet.") + + th.AssertEquals(t, subnet.Name, subnetName) + th.AssertEquals(t, subnet.Description, subnetDescription) + th.AssertEquals(t, subnet.GatewayIP, subnetGateway) + th.AssertEquals(t, subnet.CIDR, subnetCIDR) + th.AssertDeepEquals(t, subnet.ServiceTypes, serviceTypes) + + return subnet, nil +} + // CreateSubnetWithDefaultGateway will create a subnet on the specified Network // ID and have Neutron set the gateway by default An error will be returned if // the subnet could not be created. diff --git a/acceptance/openstack/networking/v2/subnets_test.go b/acceptance/openstack/networking/v2/subnets_test.go index 95f5a6833a..6b45d7c6a0 100644 --- a/acceptance/openstack/networking/v2/subnets_test.go +++ b/acceptance/openstack/networking/v2/subnets_test.go @@ -65,6 +65,33 @@ func TestSubnetCRUD(t *testing.T) { th.AssertEquals(t, found, true) } +func TestSubnetsServiceType(t *testing.T) { + client, err := clients.NewNetworkV2Client() + th.AssertNoErr(t, err) + + // Create Network + network, err := CreateNetwork(t, client) + th.AssertNoErr(t, err) + defer DeleteNetwork(t, client, network.ID) + + // Create Subnet + subnet, err := CreateSubnetWithServiceTypes(t, client, network.ID) + th.AssertNoErr(t, err) + defer DeleteSubnet(t, client, subnet.ID) + + tools.PrintResource(t, subnet) + + serviceTypes := []string{"network:floatingip"} + updateOpts := subnets.UpdateOpts{ + ServiceTypes: &serviceTypes, + } + + newSubnet, err := subnets.Update(client, subnet.ID, updateOpts).Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, newSubnet.ServiceTypes, serviceTypes) +} + func TestSubnetsDefaultGateway(t *testing.T) { client, err := clients.NewNetworkV2Client() th.AssertNoErr(t, err) diff --git a/acceptance/openstack/sharedfilesystems/v2/shareaccessrules.go b/acceptance/openstack/sharedfilesystems/v2/shareaccessrules.go new file mode 100644 index 0000000000..8a8c746503 --- /dev/null +++ b/acceptance/openstack/sharedfilesystems/v2/shareaccessrules.go @@ -0,0 +1,18 @@ +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shareaccessrules" +) + +func ShareAccessRuleGet(t *testing.T, client *gophercloud.ServiceClient, accessID string) (*shareaccessrules.ShareAccess, error) { + accessRule, err := shareaccessrules.Get(client, accessID).Extract() + if err != nil { + t.Logf("Failed to get share access rule %s: %v", accessID, err) + return nil, err + } + + return accessRule, nil +} diff --git a/acceptance/openstack/sharedfilesystems/v2/shareaccessrules_test.go b/acceptance/openstack/sharedfilesystems/v2/shareaccessrules_test.go new file mode 100644 index 0000000000..5da64a7252 --- /dev/null +++ b/acceptance/openstack/sharedfilesystems/v2/shareaccessrules_test.go @@ -0,0 +1,48 @@ +//go:build acceptance +// +build acceptance + +package v2 + +import ( + "testing" + + "github.com/gophercloud/gophercloud/acceptance/clients" + "github.com/gophercloud/gophercloud/acceptance/tools" + th "github.com/gophercloud/gophercloud/testhelper" +) + +func TestShareAccessRulesGet(t *testing.T) { + client, err := clients.NewSharedFileSystemV2Client() + if err != nil { + t.Fatalf("Unable to create a shared file system client: %v", err) + } + + client.Microversion = "2.49" + + share, err := CreateShare(t, client) + if err != nil { + t.Fatalf("Unable to create a share: %v", err) + } + + defer DeleteShare(t, client, share) + + shareAccessRight, err := GrantAccess(t, client, share) + if err != nil { + t.Fatalf("Unable to grant access to share %s: %v", share.ID, err) + } + + accessRule, err := ShareAccessRuleGet(t, client, shareAccessRight.ID) + if err != nil { + t.Logf("Unable to get share access rule for share %s: %v", share.ID, err) + } + + tools.PrintResource(t, accessRule) + + th.AssertEquals(t, shareAccessRight.ID, accessRule.ID) + th.AssertEquals(t, shareAccessRight.ShareID, accessRule.ShareID) + th.AssertEquals(t, shareAccessRight.AccessType, accessRule.AccessType) + th.AssertEquals(t, shareAccessRight.AccessLevel, accessRule.AccessLevel) + th.AssertEquals(t, shareAccessRight.AccessTo, accessRule.AccessTo) + th.AssertEquals(t, shareAccessRight.AccessKey, accessRule.AccessKey) + th.AssertEquals(t, shareAccessRight.State, accessRule.State) +} diff --git a/go.mod b/go.mod index c51d7daaaf..0c7f0517e6 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/gophercloud/gophercloud go 1.14 require ( - golang.org/x/crypto v0.0.0-20211202192323-5770296d904e + golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index dec4af3cdc..0d5a1cde5a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= -golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/openstack/baremetal/v1/nodes/results.go b/openstack/baremetal/v1/nodes/results.go index 2f3d39f3d8..b2ec8696b0 100644 --- a/openstack/baremetal/v1/nodes/results.go +++ b/openstack/baremetal/v1/nodes/results.go @@ -1,6 +1,8 @@ package nodes import ( + "time" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) @@ -234,6 +236,15 @@ type Node struct { // Static network configuration to use during deployment and cleaning. NetworkData map[string]interface{} `json:"network_data"` + + // The UTC date and time when the resource was created, ISO 8601 format. + CreatedAt time.Time `json:"created_at"` + + // The UTC date and time when the resource was updated, ISO 8601 format. May be “null”. + UpdatedAt time.Time `json:"updated_at"` + + // The UTC date and time when the provision state was updated, ISO 8601 format. May be “null”. + ProvisionUpdatedAt time.Time `json:"provision_updated_at"` } // NodePage abstracts the raw results of making a List() request against diff --git a/openstack/baremetal/v1/nodes/testing/fixtures.go b/openstack/baremetal/v1/nodes/testing/fixtures.go index 7dc7c991b2..963a9dc234 100644 --- a/openstack/baremetal/v1/nodes/testing/fixtures.go +++ b/openstack/baremetal/v1/nodes/testing/fixtures.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "testing" + "time" "github.com/gophercloud/gophercloud/openstack/baremetal/v1/nodes" th "github.com/gophercloud/gophercloud/testhelper" @@ -143,7 +144,7 @@ const NodeListDetailBody = ` "power_state": null, "properties": {}, "provision_state": "enroll", - "provision_updated_at": null, + "provision_updated_at": "2019-02-15T17:21:29+00:00", "raid_config": {}, "raid_interface": "no-raid", "rescue_interface": "no-rescue", @@ -164,7 +165,7 @@ const NodeListDetailBody = ` "target_provision_state": null, "target_raid_config": {}, "traits": [], - "updated_at": null, + "updated_at": "2019-02-15T19:59:29+00:00", "uuid": "d2630783-6ec8-4836-b556-ab427c4b581e", "vendor_interface": "ipmitool", "volume": [ @@ -261,7 +262,7 @@ const NodeListDetailBody = ` "target_provision_state": null, "target_raid_config": {}, "traits": [], - "updated_at": null, + "updated_at": "2019-02-15T19:59:29+00:00", "uuid": "08c84581-58f5-4ea2-a0c6-dd2e5d2b3662", "vendor_interface": "ipmitool", "volume": [ @@ -358,7 +359,7 @@ const NodeListDetailBody = ` "target_provision_state": null, "target_raid_config": {}, "traits": [], - "updated_at": null, + "updated_at": "2019-02-15T19:59:29+00:00", "uuid": "c9afd385-5d89-4ecb-9e1c-68194da6b474", "vendor_interface": "ipmitool", "volume": [ @@ -447,7 +448,7 @@ const SingleNodeBody = ` "power_state": null, "properties": {}, "provision_state": "enroll", - "provision_updated_at": null, + "provision_updated_at": "2019-02-15T17:21:29+00:00", "raid_config": {}, "raid_interface": "no-raid", "rescue_interface": "no-rescue", @@ -468,7 +469,7 @@ const SingleNodeBody = ` "target_provision_state": null, "target_raid_config": {}, "traits": [], - "updated_at": null, + "updated_at": "2019-02-15T19:59:29+00:00", "uuid": "d2630783-6ec8-4836-b556-ab427c4b581e", "vendor_interface": "ipmitool", "volume": [ @@ -813,6 +814,12 @@ const NodeSetMaintenanceBody = ` ` var ( + createdAtFoo, _ = time.Parse(time.RFC3339, "2019-01-31T19:59:28+00:00") + createdAtBar, _ = time.Parse(time.RFC3339, "2019-01-31T19:59:29+00:00") + createdAtBaz, _ = time.Parse(time.RFC3339, "2019-01-31T19:59:30+00:00") + updatedAt, _ = time.Parse(time.RFC3339, "2019-02-15T19:59:29+00:00") + provisonUpdatedAt, _ = time.Parse(time.RFC3339, "2019-02-15T17:21:29+00:00") + NodeFoo = nodes.Node{ UUID: "d2630783-6ec8-4836-b556-ab427c4b581e", Name: "foo", @@ -862,6 +869,9 @@ var ( ConductorGroup: "", Protected: false, ProtectedReason: "", + CreatedAt: createdAtFoo, + UpdatedAt: updatedAt, + ProvisionUpdatedAt: provisonUpdatedAt, } NodeFooValidation = nodes.NodeValidation{ @@ -959,6 +969,8 @@ var ( ConductorGroup: "", Protected: false, ProtectedReason: "", + CreatedAt: createdAtBar, + UpdatedAt: updatedAt, } NodeBaz = nodes.Node{ @@ -1003,6 +1015,8 @@ var ( ConductorGroup: "", Protected: false, ProtectedReason: "", + CreatedAt: createdAtBaz, + UpdatedAt: updatedAt, } ConfigDriveMap = nodes.ConfigDrive{ diff --git a/openstack/blockstorage/extensions/volumeactions/requests.go b/openstack/blockstorage/extensions/volumeactions/requests.go index 1c33c1785e..09dfb9ed2f 100644 --- a/openstack/blockstorage/extensions/volumeactions/requests.go +++ b/openstack/blockstorage/extensions/volumeactions/requests.go @@ -391,3 +391,30 @@ func ChangeType(client *gophercloud.ServiceClient, id string, opts ChangeTypeOpt _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return } + +// ReImageOpts contains options for Re-image a volume. +type ReImageOpts struct { + // New image id + ImageID string `json:"image_id"` + // set true to re-image volumes in reserved state + ReImageReserved bool `json:"reimage_reserved"` +} + +// ToReImageMap assembles a request body based on the contents of a ReImageOpts. +func (opts ReImageOpts) ToReImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-reimage") +} + +// ReImage will re-image a volume based on the values in ReImageOpts +func ReImage(client *gophercloud.ServiceClient, id string, opts ReImageOpts) (r ReImageResult) { + b, err := opts.ToReImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/blockstorage/extensions/volumeactions/results.go b/openstack/blockstorage/extensions/volumeactions/results.go index c4bd91a7ff..95b5bac1cb 100644 --- a/openstack/blockstorage/extensions/volumeactions/results.go +++ b/openstack/blockstorage/extensions/volumeactions/results.go @@ -214,3 +214,8 @@ type ForceDeleteResult struct { type ChangeTypeResult struct { gophercloud.ErrResult } + +// ReImageResult contains the response body and error from a ReImage request. +type ReImageResult struct { + gophercloud.ErrResult +} diff --git a/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go index 0ec9105251..378a120bc6 100644 --- a/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go +++ b/openstack/blockstorage/extensions/volumeactions/testing/fixtures.go @@ -327,6 +327,26 @@ func MockSetBootableResponse(t *testing.T) { }) } +func MockReImageResponse(t *testing.T) { + th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ` +{ + "os-reimage": { + "image_id": "71543ced-a8af-45b6-a5c4-a46282108a90", + "reimage_reserved": false + } +} + `) + w.Header().Add("Content-Type", "application/json") + w.Header().Add("Content-Length", "0") + w.WriteHeader(http.StatusAccepted) + }) +} + func MockChangeTypeResponse(t *testing.T) { th.Mux.HandleFunc("/volumes/cd281d77-8217-4830-be95-9528227c105c/action", func(w http.ResponseWriter, r *http.Request) { diff --git a/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go index 2b5bd9ca0a..2191a8a788 100644 --- a/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go +++ b/openstack/blockstorage/extensions/volumeactions/testing/requests_test.go @@ -195,6 +195,21 @@ func TestSetBootable(t *testing.T) { th.AssertNoErr(t, err) } +func TestReImage(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockReImageResponse(t) + + options := volumeactions.ReImageOpts{ + ImageID: "71543ced-a8af-45b6-a5c4-a46282108a90", + ReImageReserved: false, + } + + err := volumeactions.ReImage(client.ServiceClient(), "cd281d77-8217-4830-be95-9528227c105c", options).ExtractErr() + th.AssertNoErr(t, err) +} + func TestChangeType(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/blockstorage/v3/attachments/doc.go b/openstack/blockstorage/v3/attachments/doc.go index b3962d37f5..838d8962fc 100644 --- a/openstack/blockstorage/v3/attachments/doc.go +++ b/openstack/blockstorage/v3/attachments/doc.go @@ -29,7 +29,7 @@ Example to List Attachments Example to Create Attachment createOpts := &attachments.CreateOpts{ - InstanceiUUID: "uuid", + InstanceUUID: "uuid", VolumeUUID: "uuid" } diff --git a/openstack/compute/v2/flavors/doc.go b/openstack/compute/v2/flavors/doc.go index 34d8764fad..747966d8d9 100644 --- a/openstack/compute/v2/flavors/doc.go +++ b/openstack/compute/v2/flavors/doc.go @@ -42,6 +42,19 @@ Example to Create a Flavor panic(err) } +Example to Update a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.UpdateOpts{ + Description: "This is a good description" + } + + flavor, err := flavors.Update(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + Example to List Flavor Access flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go index 2c527b79fe..3887cdfdca 100644 --- a/openstack/compute/v2/flavors/requests.go +++ b/openstack/compute/v2/flavors/requests.go @@ -128,6 +128,11 @@ type CreateOpts struct { // Ephemeral is the amount of ephemeral disk space, measured in GB. Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` } // ToFlavorCreateMap constructs a request body from CreateOpts. @@ -149,6 +154,37 @@ func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r Create return } +type UpdateOptsBuilder interface { + ToFlavorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies parameters used for updating a flavor. +type UpdateOpts struct { + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorUpdateMap constructs a request body from UpdateOpts. +func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Update requests the update of a new flavor. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFlavorUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + // Get retrieves details of a single flavor. Use Extract to convert its // result into a Flavor. func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { diff --git a/openstack/compute/v2/flavors/results.go b/openstack/compute/v2/flavors/results.go index 92fe1b1809..0234402ed2 100644 --- a/openstack/compute/v2/flavors/results.go +++ b/openstack/compute/v2/flavors/results.go @@ -18,6 +18,12 @@ type CreateResult struct { commonResult } +// UpdateResult is the response of a Put operation. Call its Extract method to +// interpret it as a Flavor. +type UpdateResult struct { + commonResult +} + // GetResult is the response of a Get operations. Call its Extract method to // interpret it as a Flavor. type GetResult struct { @@ -69,6 +75,11 @@ type Flavor struct { // Ephemeral is the amount of ephemeral disk space, measured in GB. Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description"` } func (r *Flavor) UnmarshalJSON(b []byte) error { diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go index 3dddcd34fe..05b7fbb57a 100644 --- a/openstack/compute/v2/flavors/testing/requests_test.go +++ b/openstack/compute/v2/flavors/testing/requests_test.go @@ -38,7 +38,8 @@ func TestListFlavors(t *testing.T) { "ram": 9216000, "swap":"", "os-flavor-access:is_public": true, - "OS-FLV-EXT-DATA:ephemeral": 10 + "OS-FLV-EXT-DATA:ephemeral": 10, + "description": "foo" }, { "id": "2", @@ -87,7 +88,7 @@ func TestListFlavors(t *testing.T) { } expected := []flavors.Flavor{ - {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 9216000, Swap: 0, IsPublic: true, Ephemeral: 10}, + {ID: "1", Name: "m1.tiny", VCPUs: 1, Disk: 1, RAM: 9216000, Swap: 0, IsPublic: true, Ephemeral: 10, Description: "foo"}, {ID: "2", Name: "m1.small", VCPUs: 1, Disk: 20, RAM: 2048, Swap: 1000, IsPublic: true, Ephemeral: 0}, {ID: "3", Name: "m1.medium", VCPUs: 2, Disk: 40, RAM: 4096, Swap: 1000, IsPublic: false, Ephemeral: 0}, } @@ -124,7 +125,8 @@ func TestGetFlavor(t *testing.T) { "ram": 512, "vcpus": 1, "rxtx_factor": 1, - "swap": "" + "swap": "", + "description": "foo" } } `) @@ -136,13 +138,14 @@ func TestGetFlavor(t *testing.T) { } expected := &flavors.Flavor{ - ID: "1", - Name: "m1.tiny", - Disk: 1, - RAM: 512, - VCPUs: 1, - RxTxFactor: 1, - Swap: 0, + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + Description: "foo", } if !reflect.DeepEqual(expected, actual) { t.Errorf("Expected %#v, but was %#v", expected, actual) @@ -167,7 +170,8 @@ func TestCreateFlavor(t *testing.T) { "ram": 512, "vcpus": 1, "rxtx_factor": 1, - "swap": "" + "swap": "", + "description": "foo" } } `) @@ -175,12 +179,13 @@ func TestCreateFlavor(t *testing.T) { disk := 1 opts := &flavors.CreateOpts{ - ID: "1", - Name: "m1.tiny", - Disk: &disk, - RAM: 512, - VCPUs: 1, - RxTxFactor: 1.0, + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + Description: "foo", } actual, err := flavors.Create(fake.ServiceClient(), opts).Extract() if err != nil { @@ -188,19 +193,69 @@ func TestCreateFlavor(t *testing.T) { } expected := &flavors.Flavor{ - ID: "1", - Name: "m1.tiny", - Disk: 1, - RAM: 512, - VCPUs: 1, - RxTxFactor: 1, - Swap: 0, + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + Description: "foo", } if !reflect.DeepEqual(expected, actual) { t.Errorf("Expected %#v, but was %#v", expected, actual) } } +func TestUpdateFlavor(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/flavors/12345678", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "PUT") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + fmt.Fprintf(w, ` + { + "flavor": { + "id": "1", + "name": "m1.tiny", + "disk": 1, + "ram": 512, + "vcpus": 1, + "rxtx_factor": 1, + "swap": "", + "description": "foo" + } + } + `) + }) + + opts := &flavors.UpdateOpts{ + Description: "foo", + } + actual, err := flavors.Update(fake.ServiceClient(), "12345678", opts).Extract() + if err != nil { + t.Fatalf("Unable to update flavor: %v", err) + } + + expected := &flavors.Flavor{ + ID: "1", + Name: "m1.tiny", + Disk: 1, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1, + Swap: 0, + Description: "foo", + } + + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected %#v, but was %#v", expected, actual) + } +} + func TestDeleteFlavor(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/compute/v2/flavors/urls.go b/openstack/compute/v2/flavors/urls.go index 8620dd78ad..65bbb65401 100644 --- a/openstack/compute/v2/flavors/urls.go +++ b/openstack/compute/v2/flavors/urls.go @@ -16,6 +16,10 @@ func createURL(client *gophercloud.ServiceClient) string { return client.ServiceURL("flavors") } +func updateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + func deleteURL(client *gophercloud.ServiceClient, id string) string { return client.ServiceURL("flavors", id) } diff --git a/openstack/db/v1/instances/requests.go b/openstack/db/v1/instances/requests.go index f5243e70b1..73c7acc74e 100644 --- a/openstack/db/v1/instances/requests.go +++ b/openstack/db/v1/instances/requests.go @@ -53,6 +53,8 @@ type CreateOpts struct { // Specifies the volume size in gigabytes (GB). The value must be between 1 // and 300. Required. Size int + // Specifies the volume type. + VolumeType string // Name of the instance to create. The length of the name is limited to // 255 characters and any characters are permitted. Optional. Name string @@ -82,7 +84,6 @@ func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) { } instance := map[string]interface{}{ - "volume": map[string]int{"size": opts.Size}, "flavorRef": opts.FlavorRef, } @@ -123,6 +124,16 @@ func (opts CreateOpts) ToInstanceCreateMap() (map[string]interface{}, error) { instance["nics"] = networks } + volume := map[string]interface{}{ + "size": opts.Size, + } + + if opts.VolumeType != "" { + volume["type"] = opts.VolumeType + } + + instance["volume"] = volume + return map[string]interface{}{"instance": instance}, nil } diff --git a/openstack/db/v1/instances/testing/fixtures.go b/openstack/db/v1/instances/testing/fixtures.go index c7e019e271..782c048dc3 100644 --- a/openstack/db/v1/instances/testing/fixtures.go +++ b/openstack/db/v1/instances/testing/fixtures.go @@ -128,7 +128,8 @@ var createReq = ` } ], "volume": { - "size": 2 + "size": 2, + "type": "ssd" } } } diff --git a/openstack/db/v1/instances/testing/requests_test.go b/openstack/db/v1/instances/testing/requests_test.go index 815793cbb6..a1575a4bb1 100644 --- a/openstack/db/v1/instances/testing/requests_test.go +++ b/openstack/db/v1/instances/testing/requests_test.go @@ -32,7 +32,8 @@ func TestCreate(t *testing.T) { }, }, }, - Size: 2, + Size: 2, + VolumeType: "ssd", } instance, err := instances.Create(fake.ServiceClient(), opts).Extract() @@ -62,7 +63,8 @@ func TestCreateWithFault(t *testing.T) { }, }, }, - Size: 2, + Size: 2, + VolumeType: "ssd", } instance, err := instances.Create(fake.ServiceClient(), opts).Extract() diff --git a/openstack/identity/v3/domains/requests.go b/openstack/identity/v3/domains/requests.go index 78847c8794..bf911d05c7 100644 --- a/openstack/identity/v3/domains/requests.go +++ b/openstack/identity/v3/domains/requests.go @@ -41,6 +41,14 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa }) } +// ListAvailable enumerates the domains which are available to a specific user. +func ListAvailable(client *gophercloud.ServiceClient) pagination.Pager { + url := listAvailableURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DomainPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + // Get retrieves details on a single domain, by ID. func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { resp, err := client.Get(getURL(client, id), &r.Body, nil) diff --git a/openstack/identity/v3/domains/testing/fixtures.go b/openstack/identity/v3/domains/testing/fixtures.go index 87ac561b5a..db328831b2 100644 --- a/openstack/identity/v3/domains/testing/fixtures.go +++ b/openstack/identity/v3/domains/testing/fixtures.go @@ -10,6 +10,37 @@ import ( "github.com/gophercloud/gophercloud/testhelper/client" ) +// ListAvailableOutput provides a single page of available domain results. +const ListAvailableOutput = ` +{ + "domains": [ + { + "id": "52af04aec5f84182b06959d2775d2000", + "name": "TestDomain", + "description": "Testing domain", + "enabled": false, + "links": { + "self": "https://example.com/v3/domains/52af04aec5f84182b06959d2775d2000" + } + }, + { + "id": "a720688fb87f4575a4c000d818061eae", + "name": "ProdDomain", + "description": "Production domain", + "enabled": true, + "links": { + "self": "https://example.com/v3/domains/a720688fb87f4575a4c000d818061eae" + } + } + ], + "links": { + "next": null, + "self": "https://example.com/v3/auth/domains", + "previous": null + } +} +` + // ListOutput provides a single page of Domain results. const ListOutput = ` { @@ -87,6 +118,28 @@ const UpdateOutput = ` } ` +// ProdDomain is a domain fixture. +var ProdDomain = domains.Domain{ + Enabled: true, + ID: "a720688fb87f4575a4c000d818061eae", + Links: map[string]interface{}{ + "self": "https://example.com/v3/domains/a720688fb87f4575a4c000d818061eae", + }, + Name: "ProdDomain", + Description: "Production domain", +} + +// TestDomain is a domain fixture. +var TestDomain = domains.Domain{ + Enabled: false, + ID: "52af04aec5f84182b06959d2775d2000", + Links: map[string]interface{}{ + "self": "https://example.com/v3/domains/52af04aec5f84182b06959d2775d2000", + }, + Name: "TestDomain", + Description: "Testing domain", +} + // FirstDomain is the first domain in the List request. var FirstDomain = domains.Domain{ Enabled: true, @@ -119,9 +172,27 @@ var SecondDomainUpdated = domains.Domain{ Description: "Staging Domain", } +// ExpectedAvailableDomainsSlice is the slice of domains expected to be returned +// from ListAvailableOutput. +var ExpectedAvailableDomainsSlice = []domains.Domain{TestDomain, ProdDomain} + // ExpectedDomainsSlice is the slice of domains expected to be returned from ListOutput. var ExpectedDomainsSlice = []domains.Domain{FirstDomain, SecondDomain} +// HandleListAvailableDomainsSuccessfully creates an HTTP handler at `/auth/domains` +// on the test handler mux that responds with a list of two domains. +func HandleListAvailableDomainsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/auth/domains", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListAvailableOutput) + }) +} + // HandleListDomainsSuccessfully creates an HTTP handler at `/domains` on the // test handler mux that responds with a list of two domains. func HandleListDomainsSuccessfully(t *testing.T) { diff --git a/openstack/identity/v3/domains/testing/requests_test.go b/openstack/identity/v3/domains/testing/requests_test.go index 07eeb06ca0..0f8d6fc7cf 100644 --- a/openstack/identity/v3/domains/testing/requests_test.go +++ b/openstack/identity/v3/domains/testing/requests_test.go @@ -9,6 +9,26 @@ import ( "github.com/gophercloud/gophercloud/testhelper/client" ) +func TestListAvailableDomains(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListAvailableDomainsSuccessfully(t) + + count := 0 + err := domains.ListAvailable(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := domains.ExtractDomains(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedAvailableDomainsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + func TestListDomains(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/identity/v3/domains/urls.go b/openstack/identity/v3/domains/urls.go index b0c21b80be..902532cc39 100644 --- a/openstack/identity/v3/domains/urls.go +++ b/openstack/identity/v3/domains/urls.go @@ -2,6 +2,10 @@ package domains import "github.com/gophercloud/gophercloud" +func listAvailableURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("auth", "domains") +} + func listURL(client *gophercloud.ServiceClient) string { return client.ServiceURL("domains") } diff --git a/openstack/identity/v3/extensions/federation/doc.go b/openstack/identity/v3/extensions/federation/doc.go new file mode 100644 index 0000000000..e8b9369aaf --- /dev/null +++ b/openstack/identity/v3/extensions/federation/doc.go @@ -0,0 +1,16 @@ +/* +Package federation provides information and interaction with OS-FEDERATION API for the +Openstack Identity service. + +Example to List Mappings + + allPages, err := federation.ListMappings(identityClient).AllPages() + if err != nil { + panic(err) + } + allMappings, err := federation.ExtractMappings(allPages) + if err != nil { + panic(err) + } +*/ +package federation diff --git a/openstack/identity/v3/extensions/federation/requests.go b/openstack/identity/v3/extensions/federation/requests.go new file mode 100644 index 0000000000..d42caf2e0c --- /dev/null +++ b/openstack/identity/v3/extensions/federation/requests.go @@ -0,0 +1,13 @@ +package federation + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListMappings enumerates the mappings. +func ListMappings(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, mappingsRootURL(client), func(r pagination.PageResult) pagination.Page { + return MappingsPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/openstack/identity/v3/extensions/federation/results.go b/openstack/identity/v3/extensions/federation/results.go new file mode 100644 index 0000000000..745546fd7b --- /dev/null +++ b/openstack/identity/v3/extensions/federation/results.go @@ -0,0 +1,167 @@ +package federation + +import ( + "github.com/gophercloud/gophercloud/pagination" +) + +type UserType string + +const ( + UserTypeEphemeral UserType = "ephemeral" + UserTypeLocal UserType = "local" +) + +// Mapping a set of rules to map federation protocol attributes to +// Identity API objects. +type Mapping struct { + // The Federation Mapping unique ID + ID string `json:"id"` + + // Links contains referencing links to the limit. + Links map[string]interface{} `json:"links"` + + // The list of rules used to map remote users into local users + Rules []MappingRule `json:"rules"` +} + +type MappingRule struct { + // References a local Identity API resource, such as a group or user to which the remote attributes will be mapped. + Local []RuleLocal `json:"local"` + + // Each object contains a rule for mapping remote attributes to Identity API concepts. + Remote []RuleRemote `json:"remote"` +} + +type RuleRemote struct { + // Type represents an assertion type keyword. + Type string `json:"type"` + + // If true, then each string will be evaluated as a regular expression search against the remote attribute type. + Regex *bool `json:"regex,omitempty"` + + // The rule is matched only if any of the specified strings appear in the remote attribute type. + // This is mutually exclusive with NotAnyOf. + AnyOneOf []string `json:"any_one_of,omitempty"` + + // The rule is not matched if any of the specified strings appear in the remote attribute type. + // This is mutually exclusive with AnyOneOf. + NotAnyOf []string `json:"not_any_of,omitempty"` + + // The rule works as a filter, removing any specified strings that are listed there from the remote attribute type. + // This is mutually exclusive with Whitelist. + Blacklist []string `json:"blacklist,omitempty"` + + // The rule works as a filter, allowing only the specified strings in the remote attribute type to be passed ahead. + // This is mutually exclusive with Blacklist. + Whitelist []string `json:"whitelist,omitempty"` +} + +type RuleLocal struct { + // Domain to which the remote attributes will be matched. + Domain *Domain `json:"domain,omitempty"` + + // Group to which the remote attributes will be matched. + Group *Group `json:"group,omitempty"` + + // Group IDs to which the remote attributes will be matched. + GroupIDs string `json:"group_ids,omitempty"` + + // Groups to which the remote attributes will be matched. + Groups string `json:"groups,omitempty"` + + // Projects to which the remote attributes will be matched. + Projects []RuleProject `json:"projects,omitempty"` + + // User to which the remote attributes will be matched. + User *RuleUser `json:"user,omitempty"` +} + +type Domain struct { + // Domain ID + // This is mutually exclusive with Name. + ID string `json:"id,omitempty"` + + // Domain Name + // This is mutually exclusive with ID. + Name string `json:"name,omitempty"` +} + +type Group struct { + // Group ID to which the rule should match. + // This is mutually exclusive with Name and Domain. + ID string `json:"id,omitempty"` + + // Group Name to which the rule should match. + // This is mutually exclusive with ID. + Name string `json:"name,omitempty"` + + // Group Domain to which the rule should match. + // This is mutually exclusive with ID. + Domain *Domain `json:"domain,omitempty"` +} + +type RuleProject struct { + // Project name + Name string `json:"name,omitempty"` + + // Project roles + Roles []RuleProjectRole `json:"roles,omitempty"` +} + +type RuleProjectRole struct { + // Role name + Name string `json:"name,omitempty"` +} + +type RuleUser struct { + // User domain + Domain *Domain `json:"domain,omitempty"` + + // User email + Email string `json:"email,omitempty"` + + // User ID + ID string `json:"id,omitempty"` + + // User name + Name string `json:"name,omitempty"` + + // User type + Type *UserType `json:"type,omitempty"` +} + +// MappingsPage is a single page of Mapping results. +type MappingsPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Mappings contains any results. +func (c MappingsPage) IsEmpty() (bool, error) { + mappings, err := ExtractMappings(c) + return len(mappings) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (c MappingsPage) 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 +} + +// ExtractMappings returns a slice of Mappings contained in a single page of +// results. +func ExtractMappings(r pagination.Page) ([]Mapping, error) { + var s struct { + Mappings []Mapping `json:"mappings"` + } + err := (r.(MappingsPage)).ExtractInto(&s) + return s.Mappings, err +} diff --git a/openstack/identity/v3/extensions/federation/testing/fixtures.go b/openstack/identity/v3/extensions/federation/testing/fixtures.go new file mode 100644 index 0000000000..59785dfed3 --- /dev/null +++ b/openstack/identity/v3/extensions/federation/testing/fixtures.go @@ -0,0 +1,109 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/federation" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ListOutput = ` +{ + "links": { + "next": null, + "previous": null, + "self": "http://example.com/identity/v3/OS-FEDERATION/mappings" + }, + "mappings": [ + { + "id": "ACME", + "links": { + "self": "http://example.com/identity/v3/OS-FEDERATION/mappings/ACME" + }, + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}" + } + }, + { + "group": { + "id": "0cd5e9" + } + } + ], + "remote": [ + { + "type": "UserName" + }, + { + "type": "orgPersonType", + "not_any_of": [ + "Contractor", + "Guest" + ] + } + ] + } + ] + } + ] +} +` + +var MappingACME = federation.Mapping{ + ID: "ACME", + Links: map[string]interface{}{ + "self": "http://example.com/identity/v3/OS-FEDERATION/mappings/ACME", + }, + Rules: []federation.MappingRule{ + { + Local: []federation.RuleLocal{ + { + User: &federation.RuleUser{ + Name: "{0}", + }, + }, + { + Group: &federation.Group{ + ID: "0cd5e9", + }, + }, + }, + Remote: []federation.RuleRemote{ + { + Type: "UserName", + }, + { + Type: "orgPersonType", + NotAnyOf: []string{ + "Contractor", + "Guest", + }, + }, + }, + }, + }, +} + +// ExpectedMappingsSlice is the slice of mappings expected to be returned from ListOutput. +var ExpectedMappingsSlice = []federation.Mapping{MappingACME} + +// HandleListMappingsSuccessfully creates an HTTP handler at `/mappings` on the +// test handler mux that responds with a list of two mappings. +func HandleListMappingsSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/OS-FEDERATION/mappings", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListOutput) + }) +} diff --git a/openstack/identity/v3/extensions/federation/testing/requests_test.go b/openstack/identity/v3/extensions/federation/testing/requests_test.go new file mode 100644 index 0000000000..0bf53a4529 --- /dev/null +++ b/openstack/identity/v3/extensions/federation/testing/requests_test.go @@ -0,0 +1,42 @@ +package testing + +import ( + "testing" + + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/federation" + "github.com/gophercloud/gophercloud/pagination" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestListMappings(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListMappingsSuccessfully(t) + + count := 0 + err := federation.ListMappings(client.ServiceClient()).EachPage(func(page pagination.Page) (bool, error) { + count++ + + actual, err := federation.ExtractMappings(page) + th.AssertNoErr(t, err) + + th.CheckDeepEquals(t, ExpectedMappingsSlice, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, count, 1) +} + +func TestListMappingsAllPages(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListMappingsSuccessfully(t) + + allPages, err := federation.ListMappings(client.ServiceClient()).AllPages() + th.AssertNoErr(t, err) + actual, err := federation.ExtractMappings(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, ExpectedMappingsSlice, actual) +} diff --git a/openstack/identity/v3/extensions/federation/urls.go b/openstack/identity/v3/extensions/federation/urls.go new file mode 100644 index 0000000000..8841262dca --- /dev/null +++ b/openstack/identity/v3/extensions/federation/urls.go @@ -0,0 +1,12 @@ +package federation + +import "github.com/gophercloud/gophercloud" + +const ( + rootPath = "OS-FEDERATION" + mappingsPath = "mappings" +) + +func mappingsRootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, mappingsPath) +} diff --git a/openstack/identity/v3/limits/doc.go b/openstack/identity/v3/limits/doc.go index db2fc8a2ed..4f97669eff 100644 --- a/openstack/identity/v3/limits/doc.go +++ b/openstack/identity/v3/limits/doc.go @@ -2,6 +2,13 @@ Package limits provides information and interaction with limits for the Openstack Identity service. +Example to Get EnforcementModel + + model, err := limits.GetEnforcementModel(identityClient).Extract() + if err != nil { + panic(err) + } + Example to List Limits listOpts := limits.ListOpts{ diff --git a/openstack/identity/v3/limits/requests.go b/openstack/identity/v3/limits/requests.go index f9e78e7339..1bdac9dc30 100644 --- a/openstack/identity/v3/limits/requests.go +++ b/openstack/identity/v3/limits/requests.go @@ -5,6 +5,13 @@ import ( "github.com/gophercloud/gophercloud/pagination" ) +// Get retrieves details on a single limit, by ID. +func GetEnforcementModel(client *gophercloud.ServiceClient) (r EnforcementModelResult) { + resp, err := client.Get(enforcementModelURL(client), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + // ListOptsBuilder allows extensions to add additional parameters to // the List request type ListOptsBuilder interface { diff --git a/openstack/identity/v3/limits/results.go b/openstack/identity/v3/limits/results.go index ba57ad9dae..16ba63bc77 100644 --- a/openstack/identity/v3/limits/results.go +++ b/openstack/identity/v3/limits/results.go @@ -1,9 +1,34 @@ package limits import ( + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/pagination" ) +// A model describing the configured enforcement model used by the deployment. +type EnforcementModel struct { + // The name of the enforcement model. + Name string `json:"name"` + + // A short description of the enforcement model used. + Description string `json:"description"` +} + +// EnforcementModelResult is the response from a GetEnforcementModel operation. Call its Extract method +// to interpret it as a EnforcementModel. +type EnforcementModelResult struct { + gophercloud.Result +} + +// Extract interprets EnforcementModelResult as a EnforcementModel. +func (r EnforcementModelResult) Extract() (*EnforcementModel, error) { + var out struct { + Model *EnforcementModel `json:"model"` + } + err := r.ExtractInto(&out) + return out.Model, err +} + // A limit is the limit that override the registered limit for each project. type Limit struct { // ID is the unique ID of the limit. diff --git a/openstack/identity/v3/limits/testing/fixtures.go b/openstack/identity/v3/limits/testing/fixtures.go index 69ab12ad45..ed502b3d01 100644 --- a/openstack/identity/v3/limits/testing/fixtures.go +++ b/openstack/identity/v3/limits/testing/fixtures.go @@ -10,6 +10,15 @@ import ( "github.com/gophercloud/gophercloud/testhelper/client" ) +const GetEnforcementModelOutput = ` +{ + "model": { + "description": "Limit enforcement and validation does not take project hierarchy into consideration.", + "name": "flat" + } +} +` + // ListOutput provides a single page of List results. const ListOutput = ` { @@ -49,6 +58,12 @@ const ListOutput = ` } ` +// Model is the enforcement model in the GetEnforcementModel request. +var Model = limits.EnforcementModel{ + Name: "flat", + Description: "Limit enforcement and validation does not take project hierarchy into consideration.", +} + // FirstLimit is the first limit in the List request. var FirstLimit = limits.Limit{ ResourceName: "volume", @@ -78,6 +93,20 @@ var SecondLimit = limits.Limit{ // ExpectedLimitsSlice is the slice of limits expected to be returned from ListOutput. var ExpectedLimitsSlice = []limits.Limit{FirstLimit, SecondLimit} +// HandleGetEnforcementModelSuccessfully creates an HTTP handler at `/limits/model` on the +// test handler mux that responds with a enforcement model. +func HandleGetEnforcementModelSuccessfully(t *testing.T) { + th.Mux.HandleFunc("/limits/model", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "Accept", "application/json") + th.TestHeader(t, r, "X-Auth-Token", client.TokenID) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, GetEnforcementModelOutput) + }) +} + // HandleListLimitsSuccessfully creates an HTTP handler at `/limits` on the // test handler mux that responds with a list of two limits. func HandleListLimitsSuccessfully(t *testing.T) { diff --git a/openstack/identity/v3/limits/testing/requests_test.go b/openstack/identity/v3/limits/testing/requests_test.go index 42e55d691a..ec990357c3 100644 --- a/openstack/identity/v3/limits/testing/requests_test.go +++ b/openstack/identity/v3/limits/testing/requests_test.go @@ -9,6 +9,16 @@ import ( "github.com/gophercloud/gophercloud/testhelper/client" ) +func TestGetEnforcementModel(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleGetEnforcementModelSuccessfully(t) + + actual, err := limits.GetEnforcementModel(client.ServiceClient()).Extract() + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, Model, *actual) +} + func TestListLimits(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/identity/v3/limits/urls.go b/openstack/identity/v3/limits/urls.go index 1892614541..022c8d9e52 100644 --- a/openstack/identity/v3/limits/urls.go +++ b/openstack/identity/v3/limits/urls.go @@ -2,6 +2,10 @@ package limits import "github.com/gophercloud/gophercloud" +func enforcementModelURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("limits", "model") +} + func listURL(client *gophercloud.ServiceClient) string { return client.ServiceURL("limits") } diff --git a/openstack/loadbalancer/v2/listeners/requests.go b/openstack/loadbalancer/v2/listeners/requests.go index 54e968ce51..a0a06f6448 100644 --- a/openstack/loadbalancer/v2/listeners/requests.go +++ b/openstack/loadbalancer/v2/listeners/requests.go @@ -18,7 +18,9 @@ const ( ProtocolHTTP Protocol = "HTTP" ProtocolHTTPS Protocol = "HTTPS" // Protocol SCTP requires octavia microversion 2.23 - ProtocolSCTP Protocol = "SCTP" + ProtocolSCTP Protocol = "SCTP" + // Protocol Prometheus requires octavia microversion 2.25 + ProtocolPrometheus Protocol = "PROMETHEUS" ProtocolTerminatedHTTPS Protocol = "TERMINATED_HTTPS" ) diff --git a/openstack/loadbalancer/v2/loadbalancers/requests.go b/openstack/loadbalancer/v2/loadbalancers/requests.go index 42179ce7e3..099113c418 100644 --- a/openstack/loadbalancer/v2/loadbalancers/requests.go +++ b/openstack/loadbalancer/v2/loadbalancers/requests.go @@ -107,6 +107,9 @@ type CreateOpts struct { // The IP address of the Loadbalancer. VipAddress string `json:"vip_address,omitempty"` + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id,omitempty"` + // The administrative state of the Loadbalancer. A valid value is true (UP) // or false (DOWN). AdminStateUp *bool `json:"admin_state_up,omitempty"` @@ -185,6 +188,9 @@ type UpdateOpts struct { // or false (DOWN). AdminStateUp *bool `json:"admin_state_up,omitempty"` + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID *string `json:"vip_qos_policy_id,omitempty"` + // Tags is a set of resource tags. Tags *[]string `json:"tags,omitempty"` } diff --git a/openstack/loadbalancer/v2/loadbalancers/results.go b/openstack/loadbalancer/v2/loadbalancers/results.go index 9a385363f2..739337c4d6 100644 --- a/openstack/loadbalancer/v2/loadbalancers/results.go +++ b/openstack/loadbalancer/v2/loadbalancers/results.go @@ -47,6 +47,9 @@ type LoadBalancer struct { // Loadbalancer address. VipNetworkID string `json:"vip_network_id"` + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id"` + // The unique ID for the LoadBalancer. ID string `json:"id"` diff --git a/openstack/loadbalancer/v2/pools/requests.go b/openstack/loadbalancer/v2/pools/requests.go index 41d7dd9a41..69e6a2a763 100644 --- a/openstack/loadbalancer/v2/pools/requests.go +++ b/openstack/loadbalancer/v2/pools/requests.go @@ -190,6 +190,9 @@ type UpdateOpts struct { // or false (DOWN). AdminStateUp *bool `json:"admin_state_up,omitempty"` + // Persistence is the session persistence of the pool. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + // Tags is a set of resource tags. New in version 2.5 Tags *[]string `json:"tags,omitempty"` } diff --git a/openstack/networking/v2/extensions/agents/doc.go b/openstack/networking/v2/extensions/agents/doc.go index e20b58c797..83bc09cfdb 100644 --- a/openstack/networking/v2/extensions/agents/doc.go +++ b/openstack/networking/v2/extensions/agents/doc.go @@ -97,6 +97,66 @@ Example to List BGP speakers by dragent log.Printf("%v", s) } +Example to Schedule bgp speaker to dragent + + var opts agents.ScheduleBGPSpeakerOpts + opts.SpeakerID = speakerID + err := agents.ScheduleBGPSpeaker(c, agentID, opts).ExtractErr() + if err != nil { + log.Panic(err) + } + +Example to Remove bgp speaker from dragent + + err := agents.RemoveBGPSpeaker(c, agentID, speakerID).ExtractErr() + if err != nil { + log.Panic(err) + } + +Example to list dragents hosting specific bgp speaker + + pages, err := agents.ListDRAgentHostingBGPSpeakers(client, speakerID).AllPages() + if err != nil { + log.Panic(err) + } + allAgents, err := agents.ExtractAgents(pages) + if err != nil { + log.Panic(err) + } + for _, a := range allAgents { + log.Printf("%+v", a) + } + +Example to list routers scheduled to L3 agent + + routers, err := agents.ListL3Routers(neutron, "655967f5-d6f3-4732-88f5-617b0ff5c356").Extract() + if err != nil { + log.Panic(err) + } + + for _, r := range routers { + log.Printf("%+v", r) + } + +Example to remove router from L3 agent + + agentID := "0e1095ae-6f36-40f3-8322-8e1c9a5e68ca" + routerID := "e6fa0457-efc2-491d-ac12-17ab60417efd" + err = agents.RemoveL3Router(neutron, agentID, routerID).ExtractErr() + if err != nil { + log.Panic(err) + } + +Example to schedule router to L3 agent + + agentID := "0e1095ae-6f36-40f3-8322-8e1c9a5e68ca" + routerID := "e6fa0457-efc2-491d-ac12-17ab60417efd" + err = agents.ScheduleL3Router(neutron, agentID, agents.ScheduleL3RouterOpts{RouterID: routerID}).ExtractErr() + if err != nil { + log.Panic(err) + } + + */ package agents diff --git a/openstack/networking/v2/extensions/agents/requests.go b/openstack/networking/v2/extensions/agents/requests.go index 0aff95af0e..5a3c4c35c3 100644 --- a/openstack/networking/v2/extensions/agents/requests.go +++ b/openstack/networking/v2/extensions/agents/requests.go @@ -158,3 +158,96 @@ func ListBGPSpeakers(c *gophercloud.ServiceClient, agentID string) pagination.Pa return ListBGPSpeakersResult{pagination.SinglePageBase(r)} }) } + +// ScheduleBGPSpeakerOptsBuilder declare a function that build ScheduleBGPSpeakerOpts into a request body +type ScheduleBGPSpeakerOptsBuilder interface { + ToAgentScheduleBGPSpeakerMap() (map[string]interface{}, error) +} + +// ScheduleBGPSpeakerOpts represents the data that would be POST to the endpoint +type ScheduleBGPSpeakerOpts struct { + SpeakerID string `json:"bgp_speaker_id" required:"true"` +} + +// ToAgentScheduleBGPSpeakerMap builds a request body from ScheduleBGPSpeakerOpts +func (opts ScheduleBGPSpeakerOpts) ToAgentScheduleBGPSpeakerMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// ScheduleBGPSpeaker schedule a BGP speaker to a BGP agent +// POST /v2.0/agents/{agent-id}/bgp-drinstances +func ScheduleBGPSpeaker(c *gophercloud.ServiceClient, agentID string, opts ScheduleBGPSpeakerOptsBuilder) (r ScheduleBGPSpeakerResult) { + b, err := opts.ToAgentScheduleBGPSpeakerMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(scheduleBGPSpeakersURL(c, agentID), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveBGPSpeaker removes a BGP speaker from a BGP agent +// DELETE /v2.0/agents/{agent-id}/bgp-drinstances +func RemoveBGPSpeaker(c *gophercloud.ServiceClient, agentID string, speakerID string) (r RemoveBGPSpeakerResult) { + resp, err := c.Delete(removeBGPSpeakersURL(c, agentID, speakerID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListDRAgentHostingBGPSpeakers the dragents that are hosting a specific bgp speaker +// GET /v2.0/bgp-speakers/{bgp-speaker-id}/bgp-dragents +func ListDRAgentHostingBGPSpeakers(c *gophercloud.ServiceClient, bgpSpeakerID string) pagination.Pager { + url := listDRAgentHostingBGPSpeakersURL(c, bgpSpeakerID) + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return AgentPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListL3Routers returns a list of routers scheduled to a specific +// L3 agent. +func ListL3Routers(c *gophercloud.ServiceClient, id string) (r ListL3RoutersResult) { + resp, err := c.Get(listL3RoutersURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ScheduleL3RouterOptsBuilder allows extensions to add additional parameters +// to the ScheduleL3Router request. +type ScheduleL3RouterOptsBuilder interface { + ToAgentScheduleL3RouterMap() (map[string]interface{}, error) +} + +// ScheduleL3RouterOpts represents the attributes used when scheduling a +// router to a L3 agent. +type ScheduleL3RouterOpts struct { + RouterID string `json:"router_id" required:"true"` +} + +// ToAgentScheduleL3RouterMap builds a request body from ScheduleL3RouterOpts. +func (opts ScheduleL3RouterOpts) ToAgentScheduleL3RouterMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// ScheduleL3Router schedule a router to a L3 agent. +func ScheduleL3Router(c *gophercloud.ServiceClient, id string, opts ScheduleL3RouterOptsBuilder) (r ScheduleL3RouterResult) { + b, err := opts.ToAgentScheduleL3RouterMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(scheduleL3RouterURL(c, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveL3Router removes a router from a L3 agent. +func RemoveL3Router(c *gophercloud.ServiceClient, id string, routerID string) (r RemoveL3RouterResult) { + resp, err := c.Delete(removeL3RouterURL(c, id, routerID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/networking/v2/extensions/agents/results.go b/openstack/networking/v2/extensions/agents/results.go index 4e84da2ef6..0af9e0fdd0 100644 --- a/openstack/networking/v2/extensions/agents/results.go +++ b/openstack/networking/v2/extensions/agents/results.go @@ -6,6 +6,7 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/bgp/speakers" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/pagination" ) @@ -55,6 +56,20 @@ type RemoveDHCPNetworkResult struct { gophercloud.ErrResult } +// ScheduleBGPSpeakerResult represents the result of adding a BGP speaker to a +// BGP DR Agent. ExtractErr method to determine if the request succeeded or +// failed. +type ScheduleBGPSpeakerResult struct { + gophercloud.ErrResult +} + +// RemoveBGPSpeakerResult represents the result of removing a BGP speaker from a +// BGP DR Agent. ExtractErr method to determine if the request succeeded or +// failed. +type RemoveBGPSpeakerResult struct { + gophercloud.ErrResult +} + // Agent represents a Neutron agent. type Agent struct { // ID is the id of the agent. @@ -194,3 +209,33 @@ func ExtractBGPSpeakers(r pagination.Page) ([]speakers.BGPSpeaker, error) { err := (r.(ListBGPSpeakersResult)).ExtractInto(&s) return s.Speakers, err } + +// ListL3RoutersResult is the response from a List operation. +// Call its Extract method to interpret it as routers. +type ListL3RoutersResult struct { + gophercloud.Result +} + +// ScheduleL3RouterResult represents the result of a schedule a router to +// a L3 agent operation. ExtractErr method to determine if the request +// succeeded or failed. +type ScheduleL3RouterResult struct { + gophercloud.ErrResult +} + +// RemoveL3RouterResult represents the result of a remove a router from a +// L3 agent operation. ExtractErr method to determine if the request succeeded +// or failed. +type RemoveL3RouterResult struct { + gophercloud.ErrResult +} + +// Extract interprets any ListL3RoutesResult as an array of routers. +func (r ListL3RoutersResult) Extract() ([]routers.Router, error) { + var s struct { + Routers []routers.Router `json:"routers"` + } + + err := r.ExtractInto(&s) + return s.Routers, err +} diff --git a/openstack/networking/v2/extensions/agents/testing/fixtures.go b/openstack/networking/v2/extensions/agents/testing/fixtures.go index 9ab0c91a62..d40bc6ecdb 100644 --- a/openstack/networking/v2/extensions/agents/testing/fixtures.go +++ b/openstack/networking/v2/extensions/agents/testing/fixtures.go @@ -255,3 +255,179 @@ const ListBGPSpeakersResult = ` ] } ` +const ScheduleBGPSpeakerRequest = ` +{ + "bgp_speaker_id": "8edb2c68-0654-49a9-b3fe-030f92e3ddf6" +} +` + +var BGPAgent1 = agents.Agent{ + ID: "60d78b78-b56b-4d91-a174-2c03159f6bb6", + AdminStateUp: true, + AgentType: "BGP dynamic routing agent", + Alive: true, + Binary: "neutron-bgp-dragent", + Configurations: map[string]interface{}{ + "advertise_routes": float64(2), + "bgp_peers": float64(2), + "bgp_speakers": float64(1), + }, + CreatedAt: time.Date(2020, 9, 17, 20, 8, 58, 0, time.UTC), + StartedAt: time.Date(2021, 5, 4, 11, 13, 12, 0, time.UTC), + HeartbeatTimestamp: time.Date(2021, 9, 13, 19, 55, 1, 0, time.UTC), + Host: "agent1.example.com", + Topic: "bgp_dragent", +} + +var BGPAgent2 = agents.Agent{ + ID: "d0bdcea2-1d02-4c1d-9e79-b827e77acc22", + AdminStateUp: true, + AgentType: "BGP dynamic routing agent", + Alive: true, + Binary: "neutron-bgp-dragent", + Configurations: map[string]interface{}{ + "advertise_routes": float64(2), + "bgp_peers": float64(2), + "bgp_speakers": float64(1), + }, + CreatedAt: time.Date(2020, 9, 17, 20, 8, 15, 0, time.UTC), + StartedAt: time.Date(2021, 5, 4, 11, 13, 13, 0, time.UTC), + HeartbeatTimestamp: time.Date(2021, 9, 13, 19, 54, 47, 0, time.UTC), + Host: "agent2.example.com", + Topic: "bgp_dragent", +} + +const ListDRAgentHostingBGPSpeakersResult = ` +{ + "agents": [ + { + "binary": "neutron-bgp-dragent", + "description": null, + "availability_zone": null, + "heartbeat_timestamp": "2021-09-13 19:55:01", + "admin_state_up": true, + "resources_synced": null, + "alive": true, + "topic": "bgp_dragent", + "host": "agent1.example.com", + "agent_type": "BGP dynamic routing agent", + "resource_versions": {}, + "created_at": "2020-09-17 20:08:58", + "started_at": "2021-05-04 11:13:12", + "id": "60d78b78-b56b-4d91-a174-2c03159f6bb6", + "configurations": { + "advertise_routes": 2, + "bgp_peers": 2, + "bgp_speakers": 1 + } + }, + { + "binary": "neutron-bgp-dragent", + "description": null, + "availability_zone": null, + "heartbeat_timestamp": "2021-09-13 19:54:47", + "admin_state_up": true, + "resources_synced": null, + "alive": true, + "topic": "bgp_dragent", + "host": "agent2.example.com", + "agent_type": "BGP dynamic routing agent", + "resource_versions": {}, + "created_at": "2020-09-17 20:08:15", + "started_at": "2021-05-04 11:13:13", + "id": "d0bdcea2-1d02-4c1d-9e79-b827e77acc22", + "configurations": { + "advertise_routes": 2, + "bgp_peers": 2, + "bgp_speakers": 1 + } + } + ] +} +` + +// AgentL3ListListResult represents raw response for the ListL3Routers request. +const AgentL3RoutersListResult = ` +{ + "routers": [ + { + "admin_state_up": true, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "description": "", + "distributed": false, + "external_gateway_info": { + "enable_snat": true, + "external_fixed_ips": [ + { + "ip_address": "172.24.4.3", + "subnet_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf" + }, + { + "ip_address": "2001:db8::c", + "subnet_id": "0c56df5d-ace5-46c8-8f4c-45fa4e334d18" + } + ], + "network_id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3" + }, + "flavor_id": "f7b14d9a-b0dc-4fbe-bb14-a0f4970a69e0", + "ha": false, + "id": "915a14a6-867b-4af7-83d1-70efceb146f9", + "name": "router2", + "revision_number": 1, + "routes": [ + { + "destination": "179.24.1.0/24", + "nexthop": "172.24.3.99" + } + ], + "status": "ACTIVE", + "project_id": "0bd18306d801447bb457a46252d82d13", + "tenant_id": "0bd18306d801447bb457a46252d82d13", + "service_type_id": null + }, + { + "admin_state_up": true, + "availability_zone_hints": [], + "availability_zones": [ + "nova" + ], + "description": "", + "distributed": false, + "external_gateway_info": { + "enable_snat": true, + "external_fixed_ips": [ + { + "ip_address": "172.24.4.6", + "subnet_id": "b930d7f6-ceb7-40a0-8b81-a425dd994ccf" + }, + { + "ip_address": "2001:db8::9", + "subnet_id": "0c56df5d-ace5-46c8-8f4c-45fa4e334d18" + } + ], + "network_id": "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3" + }, + "flavor_id": "f7b14d9a-b0dc-4fbe-bb14-a0f4970a69e0", + "ha": false, + "id": "f8a44de0-fc8e-45df-93c7-f79bf3b01c95", + "name": "router1", + "revision_number": 1, + "routes": [], + "status": "ACTIVE", + "project_id": "0bd18306d801447bb457a46252d82d13", + "tenant_id": "0bd18306d801447bb457a46252d82d13", + "service_type_id": null + } + ] +} +` + +// ScheduleL3RouterRequest represents raw request for the ScheduleL3Router request. +const ScheduleL3RouterRequest = ` +{ + "router_id": "43e66290-79a4-415d-9eb9-7ff7919839e1" +} +` diff --git a/openstack/networking/v2/extensions/agents/testing/requests_test.go b/openstack/networking/v2/extensions/agents/testing/requests_test.go index 323e750c4a..d1afbc5c98 100644 --- a/openstack/networking/v2/extensions/agents/testing/requests_test.go +++ b/openstack/networking/v2/extensions/agents/testing/requests_test.go @@ -8,6 +8,7 @@ import ( fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/agents" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/routers" "github.com/gophercloud/gophercloud/pagination" th "github.com/gophercloud/gophercloud/testhelper" ) @@ -241,3 +242,182 @@ func TestListBGPSpeakers(t *testing.T) { t.Errorf("Expected 1 page, got %d", count) } } + +func TestScheduleBGPSpeaker(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + agentID := "30d76012-46de-4215-aaa1-a1630d01d891" + speakerID := "8edb2c68-0654-49a9-b3fe-030f92e3ddf6" + + th.Mux.HandleFunc("/v2.0/agents/"+agentID+"/bgp-drinstances", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ScheduleBGPSpeakerRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + }) + + var opts agents.ScheduleBGPSpeakerOpts + opts.SpeakerID = speakerID + err := agents.ScheduleBGPSpeaker(fake.ServiceClient(), agentID, opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveBGPSpeaker(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + agentID := "30d76012-46de-4215-aaa1-a1630d01d891" + speakerID := "8edb2c68-0654-49a9-b3fe-030f92e3ddf6" + + th.Mux.HandleFunc("/v2.0/agents/"+agentID+"/bgp-drinstances/"+speakerID, + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + err := agents.RemoveBGPSpeaker(fake.ServiceClient(), agentID, speakerID).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestListDRAgentHostingBGPSpeakers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + speakerID := "3f511b1b-d541-45f1-aa98-2e44e8183d4c" + th.Mux.HandleFunc("/v2.0/bgp-speakers/"+speakerID+"/bgp-dragents", + func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, ListDRAgentHostingBGPSpeakersResult) + }) + + count := 0 + agents.ListDRAgentHostingBGPSpeakers(fake.ServiceClient(), speakerID).EachPage( + func(page pagination.Page) (bool, error) { + count++ + actual, err := agents.ExtractAgents(page) + + if err != nil { + t.Errorf("Failed to extract agents: %v", err) + return false, nil + } + + expected := []agents.Agent{BGPAgent1, BGPAgent2} + th.CheckDeepEquals(t, expected, actual) + return true, nil + }) + + if count != 1 { + t.Errorf("Expected 1 page, got %d", count) + } +} + +func TestListL3Routers(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/agents/43583cf5-472e-4dc8-af5b-6aed4c94ee3a/l3-routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + fmt.Fprintf(w, AgentL3RoutersListResult) + }) + + s, err := agents.ListL3Routers(fake.ServiceClient(), "43583cf5-472e-4dc8-af5b-6aed4c94ee3a").Extract() + th.AssertNoErr(t, err) + + routes := []routers.Route{ + { + NextHop: "172.24.3.99", + DestinationCIDR: "179.24.1.0/24", + }, + } + + var snat bool = true + gw := routers.GatewayInfo{ + EnableSNAT: &snat, + NetworkID: "ae34051f-aa6c-4c75-abf5-50dc9ac99ef3", + ExternalFixedIPs: []routers.ExternalFixedIP{ + { + IPAddress: "172.24.4.3", + SubnetID: "b930d7f6-ceb7-40a0-8b81-a425dd994ccf", + }, + + { + IPAddress: "2001:db8::c", + SubnetID: "0c56df5d-ace5-46c8-8f4c-45fa4e334d18", + }, + }, + } + + var nilSlice []string + th.AssertEquals(t, len(s), 2) + th.AssertEquals(t, s[0].ID, "915a14a6-867b-4af7-83d1-70efceb146f9") + th.AssertEquals(t, s[0].AdminStateUp, true) + th.AssertEquals(t, s[0].ProjectID, "0bd18306d801447bb457a46252d82d13") + th.AssertEquals(t, s[0].Name, "router2") + th.AssertEquals(t, s[0].Status, "ACTIVE") + th.AssertEquals(t, s[0].TenantID, "0bd18306d801447bb457a46252d82d13") + th.AssertDeepEquals(t, s[0].AvailabilityZoneHints, []string{}) + th.AssertDeepEquals(t, s[0].Routes, routes) + th.AssertDeepEquals(t, s[0].GatewayInfo, gw) + th.AssertDeepEquals(t, s[0].Tags, nilSlice) + th.AssertEquals(t, s[1].ID, "f8a44de0-fc8e-45df-93c7-f79bf3b01c95") + th.AssertEquals(t, s[1].Name, "router1") + +} + +func TestScheduleL3Router(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/agents/43583cf5-472e-4dc8-af5b-6aed4c94ee3a/l3-routers", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "POST") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Content-Type", "application/json") + th.TestHeader(t, r, "Accept", "application/json") + th.TestJSONRequest(t, r, ScheduleL3RouterRequest) + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + }) + + opts := &agents.ScheduleL3RouterOpts{ + RouterID: "43e66290-79a4-415d-9eb9-7ff7919839e1", + } + err := agents.ScheduleL3Router(fake.ServiceClient(), "43583cf5-472e-4dc8-af5b-6aed4c94ee3a", opts).ExtractErr() + th.AssertNoErr(t, err) +} + +func TestRemoveL3Router(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + th.Mux.HandleFunc("/v2.0/agents/43583cf5-472e-4dc8-af5b-6aed4c94ee3a/l3-routers/43e66290-79a4-415d-9eb9-7ff7919839e1", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "DELETE") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) + }) + + err := agents.RemoveL3Router(fake.ServiceClient(), "43583cf5-472e-4dc8-af5b-6aed4c94ee3a", "43e66290-79a4-415d-9eb9-7ff7919839e1").ExtractErr() + th.AssertNoErr(t, err) +} diff --git a/openstack/networking/v2/extensions/agents/urls.go b/openstack/networking/v2/extensions/agents/urls.go index e357d0a08c..3ee3e02dcd 100644 --- a/openstack/networking/v2/extensions/agents/urls.go +++ b/openstack/networking/v2/extensions/agents/urls.go @@ -4,7 +4,10 @@ import "github.com/gophercloud/gophercloud" const resourcePath = "agents" const dhcpNetworksResourcePath = "dhcp-networks" +const l3RoutersResourcePath = "l3-routers" const bgpSpeakersResourcePath = "bgp-drinstances" +const bgpDRAgentSpeakersResourcePath = "bgp-speakers" +const bgpDRAgentAgentResourcePath = "bgp-dragents" func resourceURL(c *gophercloud.ServiceClient, id string) string { return c.ServiceURL(resourcePath, id) @@ -34,19 +37,50 @@ func dhcpNetworksURL(c *gophercloud.ServiceClient, id string) string { return c.ServiceURL(resourcePath, id, dhcpNetworksResourcePath) } +func l3RoutersURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, l3RoutersResourcePath) +} + func listDHCPNetworksURL(c *gophercloud.ServiceClient, id string) string { return dhcpNetworksURL(c, id) } +func listL3RoutersURL(c *gophercloud.ServiceClient, id string) string { + return l3RoutersURL(c, id) +} + func scheduleDHCPNetworkURL(c *gophercloud.ServiceClient, id string) string { return dhcpNetworksURL(c, id) } +func scheduleL3RouterURL(c *gophercloud.ServiceClient, id string) string { + return l3RoutersURL(c, id) +} + func removeDHCPNetworkURL(c *gophercloud.ServiceClient, id string, networkID string) string { return c.ServiceURL(resourcePath, id, dhcpNetworksResourcePath, networkID) } +func removeL3RouterURL(c *gophercloud.ServiceClient, id string, routerID string) string { + return c.ServiceURL(resourcePath, id, l3RoutersResourcePath, routerID) +} + // return /v2.0/agents/{agent-id}/bgp-drinstances func listBGPSpeakersURL(c *gophercloud.ServiceClient, agentID string) string { return c.ServiceURL(resourcePath, agentID, bgpSpeakersResourcePath) } + +// return /v2.0/agents/{agent-id}/bgp-drinstances +func scheduleBGPSpeakersURL(c *gophercloud.ServiceClient, id string) string { + return listBGPSpeakersURL(c, id) +} + +// return /v2.0/agents/{agent-id}/bgp-drinstances/{bgp-speaker-id} +func removeBGPSpeakersURL(c *gophercloud.ServiceClient, agentID string, speakerID string) string { + return c.ServiceURL(resourcePath, agentID, bgpSpeakersResourcePath, speakerID) +} + +// return /v2.0/bgp-speakers/{bgp-speaker-id}/bgp-dragents +func listDRAgentHostingBGPSpeakersURL(c *gophercloud.ServiceClient, speakerID string) string { + return c.ServiceURL(bgpDRAgentSpeakersResourcePath, speakerID, bgpDRAgentAgentResourcePath) +} diff --git a/openstack/networking/v2/extensions/dns/testing/requests_test.go b/openstack/networking/v2/extensions/dns/testing/requests_test.go index 3a792e97b2..931022c3ec 100644 --- a/openstack/networking/v2/extensions/dns/testing/requests_test.go +++ b/openstack/networking/v2/extensions/dns/testing/requests_test.go @@ -57,6 +57,8 @@ func TestPortList(t *testing.T) { ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", SecurityGroups: []string{}, DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + CreatedAt: time.Date(2019, time.June, 30, 4, 15, 37, 0, time.UTC), + UpdatedAt: time.Date(2019, time.June, 30, 5, 18, 49, 0, time.UTC), }, PortDNSExt: dns.PortDNSExt{ DNSName: "test-port", diff --git a/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go b/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go index 8f3da66743..666f406ca4 100644 --- a/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go +++ b/openstack/networking/v2/extensions/portsbinding/testing/requests_test.go @@ -2,6 +2,7 @@ package testing import ( "testing" + "time" fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/portsbinding" @@ -40,6 +41,8 @@ func TestList(t *testing.T) { ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", SecurityGroups: []string{}, DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + CreatedAt: time.Date(2019, time.June, 30, 4, 15, 37, 0, time.UTC), + UpdatedAt: time.Date(2019, time.June, 30, 5, 18, 49, 0, time.UTC), }, PortsBindingExt: portsbinding.PortsBindingExt{ VNICType: "normal", diff --git a/openstack/networking/v2/ports/results.go b/openstack/networking/v2/ports/results.go index 05ba595619..0883534ac3 100644 --- a/openstack/networking/v2/ports/results.go +++ b/openstack/networking/v2/ports/results.go @@ -1,6 +1,7 @@ package ports import ( + "encoding/json" "time" "github.com/gophercloud/gophercloud" @@ -120,6 +121,44 @@ type Port struct { UpdatedAt time.Time `json:"updated_at"` } +func (r *Port) UnmarshalJSON(b []byte) error { + type tmp Port + + // 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 = Port(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 = Port(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + // PortPage is the page returned by a pager when traversing over a collection // of network ports. type PortPage struct { diff --git a/openstack/networking/v2/ports/testing/fixtures.go b/openstack/networking/v2/ports/testing/fixtures.go index 97ad08ac2d..83dbbd8f11 100644 --- a/openstack/networking/v2/ports/testing/fixtures.go +++ b/openstack/networking/v2/ports/testing/fixtures.go @@ -30,7 +30,9 @@ const ListResponse = ` } ], "device_id": "9ae135f4-b6e0-4dad-9e91-3c223e385824", - "port_security_enabled": false + "port_security_enabled": false, + "created_at": "2019-06-30T04:15:37", + "updated_at": "2019-06-30T05:18:49" } ] } @@ -73,7 +75,9 @@ const GetResponse = ` "fqdn": "test-port.openstack.local." } ], - "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e" + "device_id": "5e3898d7-11be-483e-9732-b2f5eccd2b2e", + "created_at": "2019-06-30T04:15:37Z", + "updated_at": "2019-06-30T05:18:49Z" } } ` diff --git a/openstack/networking/v2/ports/testing/requests_test.go b/openstack/networking/v2/ports/testing/requests_test.go index 7b4dd4a68c..54f2ec0c51 100644 --- a/openstack/networking/v2/ports/testing/requests_test.go +++ b/openstack/networking/v2/ports/testing/requests_test.go @@ -5,6 +5,7 @@ import ( "net/http" "net/url" "testing" + "time" fake "github.com/gophercloud/gophercloud/openstack/networking/v2/common" "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/extradhcpopts" @@ -30,7 +31,7 @@ func TestList(t *testing.T) { count := 0 - ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { + err := ports.List(fake.ServiceClient(), ports.ListOpts{}).EachPage(func(page pagination.Page) (bool, error) { count++ actual, err := ports.ExtractPorts(page) if err != nil { @@ -56,6 +57,8 @@ func TestList(t *testing.T) { ID: "d80b1a3b-4fc1-49f3-952e-1e2ab7081d8b", SecurityGroups: []string{}, DeviceID: "9ae135f4-b6e0-4dad-9e91-3c223e385824", + CreatedAt: time.Date(2019, time.June, 30, 4, 15, 37, 0, time.UTC), + UpdatedAt: time.Date(2019, time.June, 30, 5, 18, 49, 0, time.UTC), }, } @@ -64,6 +67,8 @@ func TestList(t *testing.T) { return true, nil }) + th.AssertNoErr(t, err) + if count != 1 { t.Errorf("Expected 1 page, got %d", count) } @@ -131,6 +136,8 @@ func TestGet(t *testing.T) { th.AssertDeepEquals(t, n.SecurityGroups, []string{}) th.AssertEquals(t, n.Status, "ACTIVE") th.AssertEquals(t, n.DeviceID, "5e3898d7-11be-483e-9732-b2f5eccd2b2e") + th.AssertEquals(t, n.CreatedAt, time.Date(2019, time.June, 30, 4, 15, 37, 0, time.UTC)) + th.AssertEquals(t, n.UpdatedAt, time.Date(2019, time.June, 30, 5, 18, 49, 0, time.UTC)) } func TestGetWithExtensions(t *testing.T) { diff --git a/openstack/networking/v2/subnets/doc.go b/openstack/networking/v2/subnets/doc.go index 7d3a1b9b65..8bb4468c4e 100644 --- a/openstack/networking/v2/subnets/doc.go +++ b/openstack/networking/v2/subnets/doc.go @@ -44,6 +44,7 @@ Example to Create a Subnet With Specified Gateway }, }, DNSNameservers: []string{"foo"}, + ServiceTypes: []string{"network:floatingip"}, } subnet, err := subnets.Create(networkClient, createOpts).Extract() @@ -98,11 +99,13 @@ Example to Update a Subnet subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" dnsNameservers := []string{"8.8.8.8"} + serviceTypes := []string{"network:floatingip", "network:routed"} name := "new_name" updateOpts := subnets.UpdateOpts{ Name: &name, DNSNameservers: &dnsNameservers, + ServiceTypes: &serviceTypes, } subnet, err := subnets.Update(networkClient, subnetID, updateOpts).Extract() diff --git a/openstack/networking/v2/subnets/requests.go b/openstack/networking/v2/subnets/requests.go index 7d97fb259d..2e87907587 100644 --- a/openstack/networking/v2/subnets/requests.go +++ b/openstack/networking/v2/subnets/requests.go @@ -122,6 +122,9 @@ type CreateOpts struct { // DNSNameservers are the nameservers to be set via DHCP. DNSNameservers []string `json:"dns_nameservers,omitempty"` + // ServiceTypes are the service types associated with the subnet. + ServiceTypes []string `json:"service_types,omitempty"` + // HostRoutes are any static host routes to be set via DHCP. HostRoutes []HostRoute `json:"host_routes,omitempty"` @@ -194,6 +197,9 @@ type UpdateOpts struct { // DNSNameservers are the nameservers to be set via DHCP. DNSNameservers *[]string `json:"dns_nameservers,omitempty"` + // ServiceTypes are the service types associated with the subnet. + ServiceTypes *[]string `json:"service_types,omitempty"` + // HostRoutes are any static host routes to be set via DHCP. HostRoutes *[]HostRoute `json:"host_routes,omitempty"` diff --git a/openstack/networking/v2/subnets/results.go b/openstack/networking/v2/subnets/results.go index e04d486fd4..63b98f7248 100644 --- a/openstack/networking/v2/subnets/results.go +++ b/openstack/networking/v2/subnets/results.go @@ -83,6 +83,9 @@ type Subnet struct { // DNS name servers used by hosts in this subnet. DNSNameservers []string `json:"dns_nameservers"` + // Service types associated with the subnet. + ServiceTypes []string `json:"service_types"` + // Sub-ranges of CIDR available for dynamic allocation to ports. // See AllocationPool. AllocationPools []AllocationPool `json:"allocation_pools"` diff --git a/openstack/networking/v2/subnets/testing/fixtures.go b/openstack/networking/v2/subnets/testing/fixtures.go index 38cdbc8559..af8512e549 100644 --- a/openstack/networking/v2/subnets/testing/fixtures.go +++ b/openstack/networking/v2/subnets/testing/fixtures.go @@ -193,6 +193,7 @@ const SubnetCreateRequest = ` "gateway_ip": "192.168.199.1", "cidr": "192.168.199.0/24", "dns_nameservers": ["foo"], + "service_types": ["network:routed"], "allocation_pools": [ { "start": "192.168.199.2", @@ -212,7 +213,8 @@ const SubnetCreateResult = ` "enable_dhcp": true, "network_id": "d32019d3-bc6e-4319-9c1d-6722fc136a22", "tenant_id": "4fd44f30292945e481c7b8a0c8908869", - "dns_nameservers": [], + "dns_nameservers": ["foo"], + "service_types": ["network:routed"], "allocation_pools": [ { "start": "192.168.199.2", diff --git a/openstack/networking/v2/subnets/testing/requests_test.go b/openstack/networking/v2/subnets/testing/requests_test.go index 34a008599f..7e82d5855d 100644 --- a/openstack/networking/v2/subnets/testing/requests_test.go +++ b/openstack/networking/v2/subnets/testing/requests_test.go @@ -118,6 +118,7 @@ func TestCreate(t *testing.T) { }, }, DNSNameservers: []string{"foo"}, + ServiceTypes: []string{"network:routed"}, HostRoutes: []subnets.HostRoute{ {NextHop: "bar"}, }, @@ -130,7 +131,8 @@ func TestCreate(t *testing.T) { th.AssertEquals(t, s.EnableDHCP, true) th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") - th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.DNSNameservers, []string{"foo"}) + th.AssertDeepEquals(t, s.ServiceTypes, []string{"network:routed"}) th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ { Start: "192.168.199.2", @@ -319,7 +321,7 @@ func TestCreateWithNoCIDR(t *testing.T) { th.AssertEquals(t, s.EnableDHCP, true) th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") - th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.DNSNameservers, []string{"foo"}) th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ { Start: "192.168.199.2", @@ -368,7 +370,7 @@ func TestCreateWithPrefixlen(t *testing.T) { th.AssertEquals(t, s.EnableDHCP, true) th.AssertEquals(t, s.NetworkID, "d32019d3-bc6e-4319-9c1d-6722fc136a22") th.AssertEquals(t, s.TenantID, "4fd44f30292945e481c7b8a0c8908869") - th.AssertDeepEquals(t, s.DNSNameservers, []string{}) + th.AssertDeepEquals(t, s.DNSNameservers, []string{"foo"}) th.AssertDeepEquals(t, s.AllocationPools, []subnets.AllocationPool{ { Start: "192.168.199.2", diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go index e6e6a0487b..c6dc61fa9f 100644 --- a/openstack/objectstorage/v1/containers/results.go +++ b/openstack/objectstorage/v1/containers/results.go @@ -30,6 +30,10 @@ type ContainerPage struct { // IsEmpty returns true if a ListResult contains no container names. func (r ContainerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + names, err := ExtractNames(r) return len(names) == 0, err } diff --git a/openstack/objectstorage/v1/containers/testing/fixtures.go b/openstack/objectstorage/v1/containers/testing/fixtures.go index 042e39d2fa..bc623b3149 100644 --- a/openstack/objectstorage/v1/containers/testing/fixtures.go +++ b/openstack/objectstorage/v1/containers/testing/fixtures.go @@ -94,6 +94,19 @@ func HandleListContainerNamesSuccessfully(t *testing.T) { }) } +// HandleListZeroContainerNames204 creates an HTTP handler at `/` on the test handler mux that +// responds with "204 No Content" when container names are requested. This happens on some, but not all, +// objectstorage instances. This case is peculiar in that the server sends no `content-type` header. +func HandleListZeroContainerNames204(t *testing.T) { + th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.WriteHeader(http.StatusNoContent) + }) +} + // HandleCreateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that // responds with a `Create` response. func HandleCreateContainerSuccessfully(t *testing.T) { diff --git a/openstack/objectstorage/v1/containers/testing/requests_test.go b/openstack/objectstorage/v1/containers/testing/requests_test.go index dcfd1de753..3596346e63 100644 --- a/openstack/objectstorage/v1/containers/testing/requests_test.go +++ b/openstack/objectstorage/v1/containers/testing/requests_test.go @@ -30,7 +30,7 @@ func TestListContainerInfo(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) } func TestListAllContainerInfo(t *testing.T) { @@ -64,7 +64,7 @@ func TestListContainerNames(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) } func TestListAllContainerNames(t *testing.T) { @@ -79,6 +79,18 @@ func TestListAllContainerNames(t *testing.T) { th.CheckDeepEquals(t, ExpectedListNames, actual) } +func TestListZeroContainerNames(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListZeroContainerNames204(t) + + allPages, err := containers.List(fake.ServiceClient(), &containers.ListOpts{Full: false}).AllPages() + th.AssertNoErr(t, err) + actual, err := containers.ExtractNames(allPages) + th.AssertNoErr(t, err) + th.CheckDeepEquals(t, []string{}, actual) +} + func TestCreateContainer(t *testing.T) { th.SetupHTTP() defer th.TeardownHTTP() diff --git a/openstack/objectstorage/v1/objects/results.go b/openstack/objectstorage/v1/objects/results.go index 75367d8349..0afc7e7bf9 100644 --- a/openstack/objectstorage/v1/objects/results.go +++ b/openstack/objectstorage/v1/objects/results.go @@ -70,6 +70,10 @@ type ObjectPage struct { // IsEmpty returns true if a ListResult contains no object names. func (r ObjectPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + names, err := ExtractNames(r) return len(names) == 0, err } diff --git a/openstack/objectstorage/v1/objects/testing/fixtures.go b/openstack/objectstorage/v1/objects/testing/fixtures.go index 21da11450c..0149d40e1e 100644 --- a/openstack/objectstorage/v1/objects/testing/fixtures.go +++ b/openstack/objectstorage/v1/objects/testing/fixtures.go @@ -162,6 +162,19 @@ func HandleListObjectNamesSuccessfully(t *testing.T) { }) } +// HandleListZeroObjectNames204 creates an HTTP handler at `/testContainer` on the test handler mux that +// responds with "204 No Content" when object names are requested. This happens on some, but not all, objectstorage +// instances. This case is peculiar in that the server sends no `content-type` header. +func HandleListZeroObjectNames204(t *testing.T) { + th.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "text/plain") + + w.WriteHeader(http.StatusNoContent) + }) +} + // HandleCreateTextObjectSuccessfully creates an HTTP handler at `/testContainer/testObject` on the test handler mux // that responds with a `Create` response. A Content-Type of "text/plain" is expected. func HandleCreateTextObjectSuccessfully(t *testing.T, content string) { diff --git a/openstack/objectstorage/v1/objects/testing/requests_test.go b/openstack/objectstorage/v1/objects/testing/requests_test.go index 3b7156d0be..ab86073182 100644 --- a/openstack/objectstorage/v1/objects/testing/requests_test.go +++ b/openstack/objectstorage/v1/objects/testing/requests_test.go @@ -75,7 +75,7 @@ func TestDownloadWithLastModified(t *testing.T) { response2 := objects.Download(fake.ServiceClient(), "testContainer", "testObject", options2) content, err2 := response2.ExtractContent() th.AssertNoErr(t, err2) - th.AssertEquals(t, len(content), 0) + th.AssertEquals(t, 0, len(content)) } func TestListObjectInfo(t *testing.T) { @@ -95,7 +95,7 @@ func TestListObjectInfo(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) } func TestListObjectSubdir(t *testing.T) { @@ -115,7 +115,7 @@ func TestListObjectSubdir(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) } func TestListObjectNames(t *testing.T) { @@ -139,7 +139,7 @@ func TestListObjectNames(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) // Check with delimiter. count = 0 @@ -157,7 +157,30 @@ func TestListObjectNames(t *testing.T) { return true, nil }) th.AssertNoErr(t, err) - th.CheckEquals(t, count, 1) + th.CheckEquals(t, 1, count) +} + +func TestListZeroObjectNames204(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + HandleListZeroObjectNames204(t) + + count := 0 + options := &objects.ListOpts{Full: false} + err := objects.List(fake.ServiceClient(), "testContainer", options).EachPage(func(page pagination.Page) (bool, error) { + count++ + actual, err := objects.ExtractNames(page) + if err != nil { + t.Errorf("Failed to extract container names: %v", err) + return false, err + } + + th.CheckDeepEquals(t, []string{}, actual) + + return true, nil + }) + th.AssertNoErr(t, err) + th.CheckEquals(t, 0, count) } func TestCreateObject(t *testing.T) { @@ -290,7 +313,7 @@ func TestGetObject(t *testing.T) { } actualHeaders, err := objects.Get(fake.ServiceClient(), "testContainer", "testObject", getOpts).Extract() th.AssertNoErr(t, err) - th.AssertEquals(t, actualHeaders.StaticLargeObject, true) + th.AssertEquals(t, true, actualHeaders.StaticLargeObject) } func TestETag(t *testing.T) { @@ -303,7 +326,7 @@ func TestETag(t *testing.T) { _, headers, _, err := createOpts.ToObjectCreateParams() th.AssertNoErr(t, err) _, ok := headers["ETag"] - th.AssertEquals(t, ok, false) + th.AssertEquals(t, false, ok) hash := md5.New() io.WriteString(hash, content) @@ -316,7 +339,7 @@ func TestETag(t *testing.T) { _, headers, _, err = createOpts.ToObjectCreateParams() th.AssertNoErr(t, err) - th.AssertEquals(t, headers["ETag"], localChecksum) + th.AssertEquals(t, localChecksum, headers["ETag"]) } func TestObjectCreateParamsWithoutSeek(t *testing.T) { @@ -329,7 +352,7 @@ func TestObjectCreateParamsWithoutSeek(t *testing.T) { th.AssertNoErr(t, err) _, ok := reader.(io.ReadSeeker) - th.AssertEquals(t, ok, true) + th.AssertEquals(t, true, ok) c, err := ioutil.ReadAll(reader) th.AssertNoErr(t, err) diff --git a/openstack/objectstorage/v1/swauth/testing/requests_test.go b/openstack/objectstorage/v1/swauth/testing/requests_test.go index 46571f6117..0850aeff45 100644 --- a/openstack/objectstorage/v1/swauth/testing/requests_test.go +++ b/openstack/objectstorage/v1/swauth/testing/requests_test.go @@ -23,7 +23,7 @@ func TestAuth(t *testing.T) { swiftClient, err := swauth.NewObjectStorageV1(providerClient, authOpts) th.AssertNoErr(t, err) - th.AssertEquals(t, swiftClient.TokenID, AuthResult.Token) + th.AssertEquals(t, AuthResult.Token, swiftClient.TokenID) } func TestBadAuth(t *testing.T) { diff --git a/openstack/sharedfilesystems/v2/shareaccessrules/requests.go b/openstack/sharedfilesystems/v2/shareaccessrules/requests.go new file mode 100644 index 0000000000..491085d5c1 --- /dev/null +++ b/openstack/sharedfilesystems/v2/shareaccessrules/requests.go @@ -0,0 +1,12 @@ +package shareaccessrules + +import ( + "github.com/gophercloud/gophercloud" +) + +// Get retrieves details about a share access rule. +func Get(client *gophercloud.ServiceClient, accessID string) (r GetResult) { + resp, err := client.Get(getURL(client, accessID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/openstack/sharedfilesystems/v2/shareaccessrules/results.go b/openstack/sharedfilesystems/v2/shareaccessrules/results.go new file mode 100644 index 0000000000..2e54f5d410 --- /dev/null +++ b/openstack/sharedfilesystems/v2/shareaccessrules/results.go @@ -0,0 +1,65 @@ +package shareaccessrules + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" +) + +// ShareAccess contains information associated with an OpenStack share access rule. +type ShareAccess struct { + // The UUID of the share to which you are granted or denied access. + ShareID string `json:"share_id"` + // The date and time stamp when the resource was created within the service’s database. + CreatedAt time.Time `json:"-"` + // The date and time stamp when the resource was last updated within the service’s database. + UpdatedAt time.Time `json:"-"` + // The access rule type. + AccessType string `json:"access_type"` + // The value that defines the access. The back end grants or denies the access to it. + AccessTo string `json:"access_to"` + // The access credential of the entity granted share access. + AccessKey string `json:"access_key"` + // The state of the access rule. + State string `json:"state"` + // The access level to the share. + AccessLevel string `json:"access_level"` + // The access rule ID. + ID string `json:"id"` + // Access rule metadata. + Metadata map[string]interface{} `json:"metadata"` +} + +func (r *ShareAccess) UnmarshalJSON(b []byte) error { + type tmp ShareAccess + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ShareAccess(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + gophercloud.Result +} + +// Extract will get the ShareAccess object from the GetResult. +func (r GetResult) Extract() (*ShareAccess, error) { + var s struct { + ShareAccess *ShareAccess `json:"access"` + } + err := r.ExtractInto(&s) + return s.ShareAccess, err +} diff --git a/openstack/sharedfilesystems/v2/shareaccessrules/testing/fixtures.go b/openstack/sharedfilesystems/v2/shareaccessrules/testing/fixtures.go new file mode 100644 index 0000000000..2f053961f9 --- /dev/null +++ b/openstack/sharedfilesystems/v2/shareaccessrules/testing/fixtures.go @@ -0,0 +1,45 @@ +package testing + +import ( + "fmt" + "net/http" + "testing" + + th "github.com/gophercloud/gophercloud/testhelper" + fake "github.com/gophercloud/gophercloud/testhelper/client" +) + +const ( + shareAccessRulesEndpoint = "/share-access-rules" + shareAccessRuleID = "507bf114-36f2-4f56-8cf4-857985ca87c1" + shareID = "fb213952-2352-41b4-ad7b-2c4c69d13eef" +) + +var getResponse = `{ + "access": { + "access_level": "rw", + "state": "error", + "id": "507bf114-36f2-4f56-8cf4-857985ca87c1", + "share_id": "fb213952-2352-41b4-ad7b-2c4c69d13eef", + "access_type": "cert", + "access_to": "example.com", + "access_key": null, + "created_at": "2018-07-17T02:01:04.000000", + "updated_at": "2018-07-17T02:01:04.000000", + "metadata": { + "key1": "value1", + "key2": "value2" + } + } +}` + +func MockGetResponse(t *testing.T) { + th.Mux.HandleFunc(shareAccessRulesEndpoint+"/"+shareAccessRuleID, func(w http.ResponseWriter, r *http.Request) { + th.TestMethod(t, r, "GET") + th.TestHeader(t, r, "X-Auth-Token", fake.TokenID) + th.TestHeader(t, r, "Accept", "application/json") + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, getResponse) + }) +} diff --git a/openstack/sharedfilesystems/v2/shareaccessrules/testing/requests_test.go b/openstack/sharedfilesystems/v2/shareaccessrules/testing/requests_test.go new file mode 100644 index 0000000000..a04b5f877b --- /dev/null +++ b/openstack/sharedfilesystems/v2/shareaccessrules/testing/requests_test.go @@ -0,0 +1,39 @@ +package testing + +import ( + "testing" + "time" + + "github.com/gophercloud/gophercloud/openstack/sharedfilesystems/v2/shareaccessrules" + th "github.com/gophercloud/gophercloud/testhelper" + "github.com/gophercloud/gophercloud/testhelper/client" +) + +func TestGet(t *testing.T) { + th.SetupHTTP() + defer th.TeardownHTTP() + + MockGetResponse(t) + + resp := shareaccessrules.Get(client.ServiceClient(), "507bf114-36f2-4f56-8cf4-857985ca87c1") + th.AssertNoErr(t, resp.Err) + + accessRule, err := resp.Extract() + th.AssertNoErr(t, err) + + th.AssertDeepEquals(t, &shareaccessrules.ShareAccess{ + ShareID: "fb213952-2352-41b4-ad7b-2c4c69d13eef", + CreatedAt: time.Date(2018, 7, 17, 2, 1, 4, 0, time.UTC), + UpdatedAt: time.Date(2018, 7, 17, 2, 1, 4, 0, time.UTC), + AccessType: "cert", + AccessTo: "example.com", + AccessKey: "", + State: "error", + AccessLevel: "rw", + ID: "507bf114-36f2-4f56-8cf4-857985ca87c1", + Metadata: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, accessRule) +} diff --git a/openstack/sharedfilesystems/v2/shareaccessrules/urls.go b/openstack/sharedfilesystems/v2/shareaccessrules/urls.go new file mode 100644 index 0000000000..02766301e4 --- /dev/null +++ b/openstack/sharedfilesystems/v2/shareaccessrules/urls.go @@ -0,0 +1,11 @@ +package shareaccessrules + +import ( + "github.com/gophercloud/gophercloud" +) + +const shareAccessRulesEndpoint = "share-access-rules" + +func getURL(c *gophercloud.ServiceClient, accessID string) string { + return c.ServiceURL(shareAccessRulesEndpoint, accessID) +} diff --git a/pagination/http.go b/pagination/http.go index df3503159a..7845cda13b 100644 --- a/pagination/http.go +++ b/pagination/http.go @@ -44,8 +44,9 @@ func PageResultFrom(resp *http.Response) (PageResult, error) { func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { return PageResult{ Result: gophercloud.Result{ - Body: body, - Header: resp.Header, + Body: body, + StatusCode: resp.StatusCode, + Header: resp.Header, }, URL: *resp.Request.URL, } diff --git a/results.go b/results.go index 1b608103b7..b3ee9d5682 100644 --- a/results.go +++ b/results.go @@ -30,6 +30,11 @@ type Result struct { // this will be the deserialized JSON structure. Body interface{} + // StatusCode is the HTTP status code of the original response. Will be + // one of the OkCodes defined on the gophercloud.RequestOpts that was + // used in the request. + StatusCode int + // Header contains the HTTP header structure from the original response. Header http.Header