Repository: gophercloud/gophercloud

Issue:
hypervisors.List() is not implementing pagination correctly
<!--
Before starting a PR, please read our [contributor tutorial](https://github.com/gophercloud/gophercloud/tree/master/docs/contributor-tutorial).
-->

Since microversion 2.53, `GET /hypervisors/detail` on the Nova API is fully paginated (as is already acknowledged in `type ListOpts` having `limit` and `marker` fields), so the current implementation with `SinglePageBase` is not sufficient anymore. I got bitten by this because our organization's productive regions actually have more than 1000 hypervisors.

Hints from issue comments:

Solution patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index af0bdb979..ed777696b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -9,7 +9,7 @@
 ## New Contributor Tutorial
 
 For new contributors, we've put together a detailed tutorial
-[here](https://github.com/gophercloud/gophercloud/tree/master/docs/contributor-tutorial)!
+[here](https://github.com/gophercloud/gophercloud/tree/main/docs/contributor-tutorial)!
 
 ## 3 ways to get involved
 
@@ -89,7 +89,7 @@ fork as `origin` instead:
 4. Checkout the latest development branch:
 
    ```bash
-   git checkout master
+   git checkout main
    ```
 
 5. If you're working on something (discussed more in detail below), you will
@@ -107,7 +107,7 @@ need to checkout a new feature branch:
    git commit
    ```
 
-7. Submit your branch as a [Pull Request](https://help.github.com/articles/creating-a-pull-request/). When submitting a Pull Request, please follow our [Style Guide](https://github.com/gophercloud/gophercloud/blob/master/docs/STYLEGUIDE.md).
+7. Submit your branch as a [Pull Request](https://help.github.com/articles/creating-a-pull-request/). When submitting a Pull Request, please follow our [Style Guide](https://github.com/gophercloud/gophercloud/blob/main/docs/STYLEGUIDE.md).
 
 > Further information about using Git can be found [here](https://git-scm.com/book/en/v2).
 
diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE
index da9c8472d..d4db3bdab 100644
--- a/.github/ISSUE_TEMPLATE
+++ b/.github/ISSUE_TEMPLATE
@@ -1,3 +1,3 @@
 <!--
-Before starting a PR, please read our [contributor tutorial](https://github.com/gophercloud/gophercloud/tree/master/docs/contributor-tutorial).
+Before starting a PR, please read our [contributor tutorial](https://github.com/gophercloud/gophercloud/tree/main/docs/contributor-tutorial).
 -->
diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE
index fb271a333..03aa4bb3d 100644
--- a/.github/PULL_REQUEST_TEMPLATE
+++ b/.github/PULL_REQUEST_TEMPLATE
@@ -1,6 +1,6 @@
 <!--
 Prior to starting a PR, please make sure you have read our
-[contributor tutorial](https://github.com/gophercloud/gophercloud/tree/master/docs/contributor-tutorial).
+[contributor tutorial](https://github.com/gophercloud/gophercloud/tree/main/docs/contributor-tutorial).
 
 Prior to a PR being reviewed, there needs to be a Github issue that the PR
 addresses. Replace the brackets and text below with that issue number.
diff --git a/.github/workflows/codeql-analysis.yaml b/.github/workflows/codeql-analysis.yaml
index b9492ca44..a9695c7cf 100644
--- a/.github/workflows/codeql-analysis.yaml
+++ b/.github/workflows/codeql-analysis.yaml
@@ -20,7 +20,7 @@ jobs:
     - name: Setup Go
       uses: actions/setup-go@v5
       with:
-        go-version: "1.22"
+        go-version: "1.23"
 
     - name: Checkout repository
       uses: actions/checkout@v4
diff --git a/.github/workflows/ensure-labels.yaml b/.github/workflows/ensure-labels.yaml
index 4528512c0..3096aa010 100644
--- a/.github/workflows/ensure-labels.yaml
+++ b/.github/workflows/ensure-labels.yaml
@@ -2,7 +2,7 @@ name: Apply labels in .github/labels.yaml
 on:
   push:
     branches:
-      - master
+      - main
     paths:
       - .github/labels.yaml
       - .github/workflows/ensure-labels.yaml
diff --git a/.github/workflows/functional-baremetal.yaml b/.github/workflows/functional-baremetal.yaml
index ac2d37d6f..008f7a3d3 100644
--- a/.github/workflows/functional-baremetal.yaml
+++ b/.github/workflows/functional-baremetal.yaml
@@ -91,7 +91,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-basic.yaml b/.github/workflows/functional-basic.yaml
index 392ea1f06..0fc650a1f 100644
--- a/.github/workflows/functional-basic.yaml
+++ b/.github/workflows/functional-basic.yaml
@@ -43,7 +43,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-blockstorage.yaml b/.github/workflows/functional-blockstorage.yaml
index c0b58484a..26e81321e 100644
--- a/.github/workflows/functional-blockstorage.yaml
+++ b/.github/workflows/functional-blockstorage.yaml
@@ -42,7 +42,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-compute.yaml b/.github/workflows/functional-compute.yaml
index 0ad2dfa8d..a01dbdfc9 100644
--- a/.github/workflows/functional-compute.yaml
+++ b/.github/workflows/functional-compute.yaml
@@ -42,7 +42,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-containerinfra.yaml b/.github/workflows/functional-containerinfra.yaml
index a2ed18368..634685f60 100644
--- a/.github/workflows/functional-containerinfra.yaml
+++ b/.github/workflows/functional-containerinfra.yaml
@@ -59,7 +59,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-dns.yaml b/.github/workflows/functional-dns.yaml
index 6b5202398..f829002ba 100644
--- a/.github/workflows/functional-dns.yaml
+++ b/.github/workflows/functional-dns.yaml
@@ -43,7 +43,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-fwaas_v2.yaml b/.github/workflows/functional-fwaas_v2.yaml
index d308ade3e..297da6ff2 100644
--- a/.github/workflows/functional-fwaas_v2.yaml
+++ b/.github/workflows/functional-fwaas_v2.yaml
@@ -47,7 +47,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-identity.yaml b/.github/workflows/functional-identity.yaml
index 7a9d4ffae..6db4b9d11 100644
--- a/.github/workflows/functional-identity.yaml
+++ b/.github/workflows/functional-identity.yaml
@@ -40,7 +40,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-image.yaml b/.github/workflows/functional-image.yaml
index c6b956b88..3b66c8598 100644
--- a/.github/workflows/functional-image.yaml
+++ b/.github/workflows/functional-image.yaml
@@ -40,7 +40,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-keymanager.yaml b/.github/workflows/functional-keymanager.yaml
index 595a09669..08ee8546e 100644
--- a/.github/workflows/functional-keymanager.yaml
+++ b/.github/workflows/functional-keymanager.yaml
@@ -42,7 +42,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-loadbalancer.yaml b/.github/workflows/functional-loadbalancer.yaml
index 11d67aef3..ec7b076ee 100644
--- a/.github/workflows/functional-loadbalancer.yaml
+++ b/.github/workflows/functional-loadbalancer.yaml
@@ -43,7 +43,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-messaging.yaml b/.github/workflows/functional-messaging.yaml
index 87c5b93de..d3e3169f6 100644
--- a/.github/workflows/functional-messaging.yaml
+++ b/.github/workflows/functional-messaging.yaml
@@ -43,7 +43,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-networking.yaml b/.github/workflows/functional-networking.yaml
index 67ab11960..8722b6789 100644
--- a/.github/workflows/functional-networking.yaml
+++ b/.github/workflows/functional-networking.yaml
@@ -57,7 +57,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-objectstorage.yaml b/.github/workflows/functional-objectstorage.yaml
index c71d17f37..ff88efbc1 100644
--- a/.github/workflows/functional-objectstorage.yaml
+++ b/.github/workflows/functional-objectstorage.yaml
@@ -46,7 +46,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-orchestration.yaml b/.github/workflows/functional-orchestration.yaml
index 71213eea5..9928aba17 100644
--- a/.github/workflows/functional-orchestration.yaml
+++ b/.github/workflows/functional-orchestration.yaml
@@ -42,7 +42,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-placement.yaml b/.github/workflows/functional-placement.yaml
index 15982c531..2f182285f 100644
--- a/.github/workflows/functional-placement.yaml
+++ b/.github/workflows/functional-placement.yaml
@@ -40,7 +40,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-sharedfilesystems.yaml b/.github/workflows/functional-sharedfilesystems.yaml
index d2702759a..56480d7d5 100644
--- a/.github/workflows/functional-sharedfilesystems.yaml
+++ b/.github/workflows/functional-sharedfilesystems.yaml
@@ -56,7 +56,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/functional-workflow.yaml b/.github/workflows/functional-workflow.yaml
index f124fc641..3ac1cdeb1 100644
--- a/.github/workflows/functional-workflow.yaml
+++ b/.github/workflows/functional-workflow.yaml
@@ -49,7 +49,7 @@ jobs:
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
-          go-version: '^1.22'
+          go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
         run: ./script/acceptancetest
         env:
diff --git a/.github/workflows/greetings.yaml b/.github/workflows/greetings.yaml
index 7bd51ee5e..72295f896 100644
--- a/.github/workflows/greetings.yaml
+++ b/.github/workflows/greetings.yaml
@@ -16,9 +16,9 @@ jobs:
           Thank you for reporting your first issue! Be sure that we will be looking at it, but keep in mind
           this sometimes takes a while.
           Please let the maintainers know if your issue has not got enough attention after a few days.
-          If any doubt, please consult our issue [tutorial](https://github.com/gophercloud/gophercloud/blob/master/docs/contributor-tutorial/step-02-issues.md).
+          If any doubt, please consult our issue [tutorial](https://github.com/gophercloud/gophercloud/blob/main/docs/contributor-tutorial/step-02-issues.md).
         pr-message: |
           Thank you for submitting your first PR! Be sure that we will be looking at it but keep in mind
           this sometimes takes a while.
           Please let the maintainers know if your PR has not got enough attention after a few days.
-          If any doubt, please consult our PR [tutorial](https://github.com/gophercloud/gophercloud/blob/master/docs/contributor-tutorial/step-05-pull-requests.md).
+          If any doubt, please consult our PR [tutorial](https://github.com/gophercloud/gophercloud/blob/main/docs/contributor-tutorial/step-05-pull-requests.md).
diff --git a/.github/workflows/label-issue.yaml b/.github/workflows/label-issue.yaml
index a1fbff16e..45ab4cbe3 100644
--- a/.github/workflows/label-issue.yaml
+++ b/.github/workflows/label-issue.yaml
@@ -11,6 +11,7 @@ jobs:
     runs-on: ubuntu-latest
     permissions:
       issues: write
+      pull-requests: write
     steps:
     - run: gh issue edit "$NUMBER" --remove-label "needinfo"
       env:
diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml
index 11fddb5f6..1489ec0e3 100644
--- a/.github/workflows/unit.yaml
+++ b/.github/workflows/unit.yaml
@@ -13,7 +13,7 @@ jobs:
       fail-fast: false
       matrix:
         go-version:
-          - "1.22.3"
+          - "1.23.0"
           - "1"
 
     steps:
diff --git a/README.md b/README.md
index ca47f5b0b..69a53bf59 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 # Gophercloud: an OpenStack SDK for Go
-[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=master)](https://coveralls.io/github/gophercloud/gophercloud?branch=master)
+[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=main)](https://coveralls.io/github/gophercloud/gophercloud?branch=main)
 
 Gophercloud is a Go SDK for OpenStack.
 
diff --git a/docs/contributor-tutorial/step-05-pull-requests.md b/docs/contributor-tutorial/step-05-pull-requests.md
index cb340a92c..c65eff878 100644
--- a/docs/contributor-tutorial/step-05-pull-requests.md
+++ b/docs/contributor-tutorial/step-05-pull-requests.md
@@ -125,14 +125,14 @@ it's self contained.
 Use the following `git` workflow to implement this method:
 
 ```shell
-$ git checkout master
+$ git checkout main
 $ git pull
 $ git checkout -b identityv3-regions-create
 $ (write your code)
 $ git add .
 $ git commit -m "Implementing Regions Create"
 
-$ git checkout master
+$ git checkout main
 $ git checkout -b identityv3-regions-update
 $ (write your code)
 $ git add .
@@ -159,7 +159,7 @@ defined relationship.
 Use the following `git` workflow to implement this method:
 
 ```shell
-$ git checkout master
+$ git checkout main
 $ git pull
 $ git checkout -b identityv3-regions-create
 $ (write your code)
diff --git a/go.mod b/go.mod
index dc5d92e32..16b9039f4 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,12 @@
 module github.com/gophercloud/gophercloud/v2
 
-go 1.22
+go 1.23.0
+
+toolchain go1.23.6
 
 require (
-	golang.org/x/crypto v0.30.0
+	golang.org/x/crypto v0.36.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
-require golang.org/x/sys v0.28.0 // indirect
+require golang.org/x/sys v0.31.0 // indirect
diff --git a/go.sum b/go.sum
index 1812f7828..d886e772d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,9 @@
-golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
-golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
-golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
-golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
+golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
+golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/internal/acceptance/openstack/baremetal/v1/baremetal.go b/internal/acceptance/openstack/baremetal/v1/baremetal.go
index a47f942da..4c121d18c 100644
--- a/internal/acceptance/openstack/baremetal/v1/baremetal.go
+++ b/internal/acceptance/openstack/baremetal/v1/baremetal.go
@@ -9,6 +9,7 @@ import (
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/allocations"
 	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes"
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
 	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/ports"
 )
 
@@ -76,6 +77,29 @@ func DeleteAllocation(t *testing.T, client *gophercloud.ServiceClient, allocatio
 	t.Logf("Deleted allocation: %s", allocation.UUID)
 }
 
+// CreatePortGroup creates an allocation
+func CreatePortGroup(t *testing.T, client *gophercloud.ServiceClient, node *nodes.Node) (*portgroups.PortGroup, error) {
+	name := tools.RandomString("ACPTTEST", 16)
+	t.Logf("Attempting to create bare metal allocation: %s", name)
+
+	allocation, err := portgroups.Create(context.TODO(), client, portgroups.CreateOpts{
+		Name:     name,
+		NodeUUID: node.UUID,
+	}).Extract()
+
+	return allocation, err
+}
+
+// DeletePortGroup deletes a bare metal portgroup via its UUID.
+func DeletePortGroup(t *testing.T, client *gophercloud.ServiceClient, portgroup *portgroups.PortGroup) {
+	err := portgroups.Delete(context.TODO(), client, portgroup.UUID).ExtractErr()
+	if err != nil {
+		t.Fatalf("Unable to delete portgroup %s: %s", portgroup.UUID, err)
+	}
+
+	t.Logf("Deleted portgroup: %s", portgroup.UUID)
+}
+
 // CreateFakeNode creates a node with fake-hardware.
 func CreateFakeNode(t *testing.T, client *gophercloud.ServiceClient) (*nodes.Node, error) {
 	name := tools.RandomString("ACPTTEST", 16)
diff --git a/internal/acceptance/openstack/compute/v2/compute.go b/internal/acceptance/openstack/compute/v2/compute.go
index 4a5ec655a..3d5da42a6 100644
--- a/internal/acceptance/openstack/compute/v2/compute.go
+++ b/internal/acceptance/openstack/compute/v2/compute.go
@@ -685,19 +685,17 @@ func CreateServerWithPublicKey(t *testing.T, client *gophercloud.ServiceClient,
 	name := tools.RandomString("ACPTTEST", 16)
 	t.Logf("Attempting to create server: %s", name)
 
-	serverCreateOpts := servers.CreateOpts{
+	createOpts := servers.CreateOpts{
 		Name:      name,
 		FlavorRef: choices.FlavorID,
 		ImageRef:  choices.ImageID,
 		Networks: []servers.Network{
 			{UUID: networkID},
 		},
+		KeyName: keyPairName,
 	}
 
-	server, err := servers.Create(context.TODO(), client, keypairs.CreateOptsExt{
-		CreateOptsBuilder: serverCreateOpts,
-		KeyName:           keyPairName,
-	}, nil).Extract()
+	server, err := servers.Create(context.TODO(), client, createOpts, nil).Extract()
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
index e8596506e..907e9d565 100644
--- a/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
+++ b/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
@@ -10,6 +10,7 @@ import (
 	"github.com/gophercloud/gophercloud/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavorprofiles"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies"
@@ -724,7 +725,7 @@ func CreateFlavor(t *testing.T, client *gophercloud.ServiceClient, flavorProfile
 		Name:            flavorName,
 		Description:     description,
 		FlavorProfileId: flavorProfile.ID,
-		Enabled:         true,
+		Enabled:         ptr.To(false),
 	}
 
 	flavor, err := flavors.Create(context.TODO(), client, createOpts).Extract()
@@ -737,7 +738,7 @@ func CreateFlavor(t *testing.T, client *gophercloud.ServiceClient, flavorProfile
 	th.AssertEquals(t, flavorName, flavor.Name)
 	th.AssertEquals(t, description, flavor.Description)
 	th.AssertEquals(t, flavorProfile.ID, flavor.FlavorProfileId)
-	th.AssertEquals(t, true, flavor.Enabled)
+	th.AssertEquals(t, false, flavor.Enabled)
 
 	return flavor, nil
 }
diff --git a/internal/acceptance/openstack/networking/v2/extensions/extensions.go b/internal/acceptance/openstack/networking/v2/extensions/extensions.go
index 4de1401f4..f50878abe 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/extensions.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/extensions.go
@@ -43,8 +43,8 @@ func CreateExternalNetwork(t *testing.T, client *gophercloud.ServiceClient) (*ne
 
 	t.Logf("Created external network: %s", networkName)
 
-	th.AssertEquals(t, network.Name, networkName)
-	th.AssertEquals(t, network.Description, networkDescription)
+	th.AssertEquals(t, networkName, network.Name)
+	th.AssertEquals(t, networkDescription, network.Description)
 
 	return network, nil
 }
@@ -74,9 +74,9 @@ func CreatePortWithSecurityGroup(t *testing.T, client *gophercloud.ServiceClient
 
 	t.Logf("Successfully created port: %s", portName)
 
-	th.AssertEquals(t, port.Name, portName)
-	th.AssertEquals(t, port.Description, portDescription)
-	th.AssertEquals(t, port.NetworkID, networkID)
+	th.AssertEquals(t, portName, port.Name)
+	th.AssertEquals(t, portDescription, port.Description)
+	th.AssertEquals(t, networkID, port.NetworkID)
 
 	return port, nil
 }
@@ -101,8 +101,8 @@ func CreateSecurityGroup(t *testing.T, client *gophercloud.ServiceClient) (*grou
 
 	t.Logf("Created security group: %s", secGroup.ID)
 
-	th.AssertEquals(t, secGroup.Name, secGroupName)
-	th.AssertEquals(t, secGroup.Description, secGroupDescription)
+	th.AssertEquals(t, secGroupName, secGroup.Name)
+	th.AssertEquals(t, secGroupDescription, secGroup.Description)
 
 	return secGroup, nil
 }
@@ -140,6 +140,44 @@ func CreateSecurityGroupRule(t *testing.T, client *gophercloud.ServiceClient, se
 	return rule, nil
 }
 
+// CreateSecurityGroupRulesBulk will create security group rules with a random name
+// and random port between 80 and 99.
+// An error will be returned if one was failed to be created.
+func CreateSecurityGroupRulesBulk(t *testing.T, client *gophercloud.ServiceClient, secGroupID string) ([]rules.SecGroupRule, error) {
+	t.Logf("Attempting to bulk create security group rules in group: %s", secGroupID)
+
+	sgRulesCreateOpts := make([]rules.CreateOpts, 3)
+	for i := range 3 {
+		description := "Rule description"
+		fromPort := tools.RandomInt(1080, 1089)
+		toPort := tools.RandomInt(1090, 1099)
+
+		sgRulesCreateOpts[i] = rules.CreateOpts{
+			Description:  description,
+			Direction:    "ingress",
+			EtherType:    "IPv4",
+			SecGroupID:   secGroupID,
+			PortRangeMin: fromPort,
+			PortRangeMax: toPort,
+			Protocol:     rules.ProtocolTCP,
+		}
+	}
+
+	rules, err := rules.CreateBulk(context.TODO(), client, sgRulesCreateOpts).Extract()
+	if err != nil {
+		return rules, err
+	}
+
+	for i, rule := range rules {
+		t.Logf("Created security group rule: %s", rule.ID)
+
+		th.AssertEquals(t, sgRulesCreateOpts[i].SecGroupID, rule.SecGroupID)
+		th.AssertEquals(t, sgRulesCreateOpts[i].Description, rule.Description)
+	}
+
+	return rules, nil
+}
+
 // DeleteSecurityGroup will delete a security group of a specified ID.
 // A fatal error will occur if the deletion failed. This works best as a
 // deferred function
diff --git a/internal/ptr/ptr.go b/internal/ptr/ptr.go
new file mode 100644
index 000000000..6c3ee9bd5
--- /dev/null
+++ b/internal/ptr/ptr.go
@@ -0,0 +1,5 @@
+package ptr
+
+func To[T any](v T) *T {
+	return &v
+}
diff --git a/openstack/baremetal/v1/nodes/requests.go b/openstack/baremetal/v1/nodes/requests.go
index 8b2eb4b21..cddd269a3 100644
--- a/openstack/baremetal/v1/nodes/requests.go
+++ b/openstack/baremetal/v1/nodes/requests.go
@@ -1002,3 +1002,61 @@ func DetachVirtualMedia(ctx context.Context, client *gophercloud.ServiceClient,
 	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
 	return
 }
+
+// VirtualInterfaceOpts defines options for attaching a VIF to a node
+type VirtualInterfaceOpts struct {
+	// The UUID or name of the VIF
+	ID string `json:"id" required:"true"`
+	// The UUID of a port to attach the VIF to. Cannot be specified with PortgroupUUID
+	PortUUID string `json:"port_uuid,omitempty"`
+	// The UUID of a portgroup to attach the VIF to. Cannot be specified with PortUUID
+	PortgroupUUID string `json:"portgroup_uuid,omitempty"`
+}
+
+// VirtualInterfaceOptsBuilder allows extensions to add additional parameters to the
+// AttachVirtualInterface request.
+type VirtualInterfaceOptsBuilder interface {
+	ToVirtualInterfaceMap() (map[string]any, error)
+}
+
+// ToVirtualInterfaceMap assembles a request body based on the contents of a VirtualInterfaceOpts.
+func (opts VirtualInterfaceOpts) ToVirtualInterfaceMap() (map[string]any, error) {
+	if opts.PortUUID != "" && opts.PortgroupUUID != "" {
+		return nil, fmt.Errorf("cannot specify both port_uuid and portgroup_uuid")
+	}
+
+	return gophercloud.BuildRequestBody(opts, "")
+}
+
+// ListVirtualInterfaces returns a list of VIFs that are attached to the node.
+func ListVirtualInterfaces(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ListVirtualInterfacesResult) {
+	resp, err := client.Get(ctx, virtualInterfaceURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
+// AttachVirtualInterface attaches a VIF to a node.
+func AttachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, opts VirtualInterfaceOptsBuilder) (r VirtualInterfaceAttachResult) {
+	reqBody, err := opts.ToVirtualInterfaceMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	resp, err := client.Post(ctx, virtualInterfaceURL(client, id), reqBody, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
+// DetachVirtualInterface detaches a VIF from a node.
+func DetachVirtualInterface(ctx context.Context, client *gophercloud.ServiceClient, id string, vifID string) (r VirtualInterfaceDetachResult) {
+	resp, err := client.Delete(ctx, virtualInterfaceDeleteURL(client, id, vifID), &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
diff --git a/openstack/baremetal/v1/nodes/results.go b/openstack/baremetal/v1/nodes/results.go
index 507e0d6a4..3e1976d3e 100644
--- a/openstack/baremetal/v1/nodes/results.go
+++ b/openstack/baremetal/v1/nodes/results.go
@@ -710,3 +710,40 @@ type VirtualMediaAttachResult struct {
 type VirtualMediaDetachResult struct {
 	gophercloud.ErrResult
 }
+
+// VirtualInterfaceAttachResult is the response from an AttachVirtualInterface operation.
+type VirtualInterfaceAttachResult struct {
+	gophercloud.ErrResult
+}
+
+// VirtualInterfaceDetachResult is the response from a DetachVirtualInterface operation.
+type VirtualInterfaceDetachResult struct {
+	gophercloud.ErrResult
+}
+
+// VIF represents a virtual interface attached to a node.
+type VIF struct {
+	// The UUID or name of the VIF
+	ID string `json:"id"`
+}
+
+// ListVirtualInterfacesResult is the response from a ListVirtualInterfaces operation.
+type ListVirtualInterfacesResult struct {
+	gophercloud.Result
+	gophercloud.HeaderResult
+}
+
+// Extract interprets any ListVirtualInterfacesResult as a list of VIFs.
+func (r ListVirtualInterfacesResult) Extract() ([]VIF, error) {
+	var s struct {
+		VIFs []VIF `json:"vifs"`
+	}
+
+	err := r.Result.ExtractInto(&s)
+	return s.VIFs, err
+}
+
+// ExtractHeader interprets any ListVirtualInterfacesResult as a HeaderResult.
+func (r ListVirtualInterfacesResult) ExtractHeader() (gophercloud.HeaderResult, error) {
+	return r.HeaderResult, nil
+}
diff --git a/openstack/baremetal/v1/nodes/urls.go b/openstack/baremetal/v1/nodes/urls.go
index 2948bb659..b86e4820e 100644
--- a/openstack/baremetal/v1/nodes/urls.go
+++ b/openstack/baremetal/v1/nodes/urls.go
@@ -89,3 +89,11 @@ func firmwareListURL(client *gophercloud.ServiceClient, id string) string {
 func virtualMediaURL(client *gophercloud.ServiceClient, id string) string {
 	return client.ServiceURL("nodes", id, "vmedia")
 }
+
+func virtualInterfaceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("nodes", id, "vifs")
+}
+
+func virtualInterfaceDeleteURL(client *gophercloud.ServiceClient, id string, vifID string) string {
+	return client.ServiceURL("nodes", id, "vifs", vifID)
+}
diff --git a/openstack/baremetal/v1/portgroups/requests.go b/openstack/baremetal/v1/portgroups/requests.go
new file mode 100644
index 000000000..9a0675532
--- /dev/null
+++ b/openstack/baremetal/v1/portgroups/requests.go
@@ -0,0 +1,149 @@
+package portgroups
+
+import (
+	"context"
+
+	"github.com/gophercloud/gophercloud/v2"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+)
+
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToPortGroupCreateMap() (map[string]any, error)
+}
+
+// CreateOpts specifies port group creation parameters
+type CreateOpts struct {
+	// NodeUUID is the UUID of the Node this resource belongs to
+	NodeUUID string `json:"node_uuid" required:"true"`
+
+	// Address is the physical hardware address of this Portgroup,
+	// typically the hardware MAC address
+	Address string `json:"address,omitempty"`
+
+	// Name is a human-readable identifier for the Portgroup resource
+	Name string `json:"name,omitempty"`
+
+	// Mode is the mode of the port group. For possible values, refer to
+	// https://www.kernel.org/doc/Documentation/networking/bonding.txt
+	// If not specified, it will be set to the value of the
+	// [DEFAULT]default_portgroup_mode configuration option.
+	// When set, cannot be removed from the port group.
+	Mode string `json:"mode,omitempty"`
+
+	// StandalonePortsSupported indicates whether ports that are members
+	// of this portgroup can be used as stand-alone ports
+	StandalonePortsSupported bool `json:"standalone_ports_supported,omitempty"`
+
+	// Properties contains key/value properties related to the port
+	// group's configuration
+	Properties map[string]interface{} `json:"properties,omitempty"`
+
+	// Extra is a set of one or more arbitrary metadata key and value pairs
+	Extra map[string]string `json:"extra,omitempty"`
+
+	// UUID is the UUID for the resource
+	UUID string `json:"uuid,omitempty"`
+}
+
+// ToPortGroupCreateMap assembles a request body based on the contents of a CreateOpts.
+func (opts CreateOpts) ToPortGroupCreateMap() (map[string]any, error) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		return nil, err
+	}
+
+	return body, nil
+}
+
+// Create requests a node to be created
+func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
+	reqBody, err := opts.ToPortGroupCreateMap()
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	resp, err := client.Post(ctx, createURL(client), reqBody, &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 {
+	ToPortGroupListQuery() (string, error)
+}
+
+// ListOpts allows the filtering and sorting of paginated collections through
+// the API. Filtering is achieved by passing specific query parameters to the API.
+type ListOpts struct {
+	// Node filters the list to return only Portgroups associated with this
+	// specific node (name or UUID)
+	Node string `q:"node,omitempty"`
+
+	// Address filters the list to return only Portgroups with the specified
+	// physical hardware address (typically MAC)
+	Address string `q:"address,omitempty"`
+
+	// Fields specifies which fields to return in the response
+	// For example: "uuid,name" will return only those fields
+	Fields []string `q:"fields,omitempty"`
+
+	// Limit requests a page size of items. Returns a number of items up to a limit value.
+	// Use with marker to implement pagination. Cannot exceed max_limit set in configuration.
+	Limit int `q:"limit,omitempty"`
+
+	// Marker is the ID of the last-seen item. Use with limit to implement pagination.
+	// Use the ID from the response as marker in subsequent limited requests.
+	Marker string `q:"marker,omitempty"`
+
+	// SortDir sorts the response by the requested direction.
+	// Valid values are "asc" or "desc". Default is "asc".
+	SortDir string `q:"sort_dir,omitempty"`
+
+	// SortKey sorts the response by this attribute value.
+	// Default is "id". Multiple sort key/direction pairs can be specified.
+	SortKey string `q:"sort_key,omitempty"`
+
+	// Detail indicates whether to show detailed information about the resource.
+	// Cannot be true if Fields parameter is specified.
+	Detail bool `q:"detail,omitempty"`
+}
+
+// ToPortGroupListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToPortGroupListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(opts)
+	return q.String(), err
+}
+
+// List makes a request against the API to list portgroups accessible to you.
+func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	url := listURL(client)
+	if opts != nil {
+		query, err := opts.ToPortGroupListQuery()
+		if err != nil {
+			return pagination.Pager{Err: err}
+		}
+		url += query
+	}
+	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
+		return PortGroupsPage{pagination.LinkedPageBase{PageResult: r}}
+	})
+}
+
+// Get requests the details of an portgroup by ID.
+func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) {
+	resp, err := client.Get(ctx, getURL(client, id), &r.Body, &gophercloud.RequestOpts{
+		OkCodes: []int{200},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
+// Delete requests the deletion of an portgroup
+func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) {
+	resp, err := client.Delete(ctx, deleteURL(client, id), nil)
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
diff --git a/openstack/baremetal/v1/portgroups/results.go b/openstack/baremetal/v1/portgroups/results.go
new file mode 100644
index 000000000..1c0c14191
--- /dev/null
+++ b/openstack/baremetal/v1/portgroups/results.go
@@ -0,0 +1,132 @@
+package portgroups
+
+import (
+	"time"
+
+	"github.com/gophercloud/gophercloud/v2"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+)
+
+// ResourceLink represents a link with href and rel attributes
+type ResourceLink struct {
+	Href string `json:"href"`
+	Rel  string `json:"rel"`
+}
+
+// PortGroup represents a port group in the baremetal service
+// https://docs.openstack.org/api-ref/baremetal/#portgroups-portgroups
+type PortGroup struct {
+	// Human-readable identifier for the Portgroup resource. May be undefined.
+	Name string `json:"name"`
+
+	// The UUID for the resource.
+	UUID string `json:"uuid"`
+
+	// Physical hardware address of this Portgroup, typically the hardware MAC address.
+	Address string `json:"address,omitempty"`
+
+	// UUID of the Node this resource belongs to.
+	NodeUUID string `json:"node_uuid"`
+
+	// Indicates whether ports that are members of this portgroup can be used as
+	// stand-alone ports.
+	StandalonePortsSupported bool `json:"standalone_ports_supported"`
+
+	// Internal metadata set and stored by the Portgroup. This field is read-only.
+	InternalInfo map[string]any `json:"internal_info"`
+
+	// A set of one or more arbitrary metadata key and value pairs.
+	Extra map[string]any `json:"extra"`
+
+	// Mode of the port group. For possible values, refer to
+	// https://www.kernel.org/doc/Documentation/networking/bonding.txt
+	Mode string `json:"mode"`
+
+	// Key/value properties related to the port group's configuration.
+	Properties map[string]any `json:"properties"`
+
+	// 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"`
+
+	// A list of relative links. Includes the self and bookmark links.
+	Links []ResourceLink `json:"links"`
+
+	// Links to the collection of ports belonging to this portgroup.
+	Ports []ResourceLink `json:"ports"`
+}
+
+type portgroupsResult struct {
+	gophercloud.Result
+}
+
+func (r portgroupsResult) Extract() (*PortGroup, error) {
+	var s PortGroup
+	err := r.ExtractInto(&s)
+	return &s, err
+}
+
+func (r portgroupsResult) ExtractInto(v any) error {
+	return r.Result.ExtractIntoStructPtr(v, "")
+}
+
+func ExtractPortGroupsInto(r pagination.Page, v any) error {
+	return r.(PortGroupsPage).Result.ExtractIntoSlicePtr(v, "portgroups")
+}
+
+// PortGroupsPage abstracts the raw results of making a List() request against
+// the API.
+type PortGroupsPage struct {
+	pagination.LinkedPageBase
+}
+
+// IsEmpty returns true if a page contains no PortGroup results.
+func (r PortGroupsPage) IsEmpty() (bool, error) {
+	if r.StatusCode == 204 {
+		return true, nil
+	}
+
+	s, err := ExtractPortGroups(r)
+	return len(s) == 0, err
+}
+
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
+func (r PortGroupsPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"portgroups_links"`
+	}
+	err := r.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
+// ExtractPortGroups interprets the results of a single page from a List() call,
+// producing a slice of PortGroup entities.
+func ExtractPortGroups(r pagination.Page) ([]PortGroup, error) {
+	var s []PortGroup
+	err := ExtractPortGroupsInto(r, &s)
+	return s, err
+}
+
+// GetResult is the response from a Get operation. Call its Extract
+// method to interpret it as a PortGroup.
+type GetResult struct {
+	portgroupsResult
+}
+
+// CreateResult is the response from a Create operation.
+type CreateResult struct {
+	portgroupsResult
+}
+
+// DeleteResult is the response from a Delete operation. Call its ExtractErr
+// method to determine if the call succeeded or failed.
+type DeleteResult struct {
+	gophercloud.ErrResult
+}
diff --git a/openstack/baremetal/v1/portgroups/urls.go b/openstack/baremetal/v1/portgroups/urls.go
new file mode 100644
index 000000000..3320a8e17
--- /dev/null
+++ b/openstack/baremetal/v1/portgroups/urls.go
@@ -0,0 +1,23 @@
+package portgroups
+
+import "github.com/gophercloud/gophercloud/v2"
+
+func createURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("portgroups")
+}
+
+func listURL(client *gophercloud.ServiceClient) string {
+	return createURL(client)
+}
+
+func resourceURL(client *gophercloud.ServiceClient, id string) string {
+	return client.ServiceURL("portgroups", id)
+}
+
+func deleteURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
+
+func getURL(client *gophercloud.ServiceClient, id string) string {
+	return resourceURL(client, id)
+}
diff --git a/openstack/blockstorage/v2/quotasets/results.go b/openstack/blockstorage/v2/quotasets/results.go
index e75aea5b5..179427f4b 100644
--- a/openstack/blockstorage/v2/quotasets/results.go
+++ b/openstack/blockstorage/v2/quotasets/results.go
@@ -111,6 +111,47 @@ type QuotaUsageSet struct {
 	// Note: allocated attribute is available only when nested quota is
 	// enabled.
 	Groups QuotaUsage `json:"groups"`
+
+	// Extra is a collection of key/values that has the size (GB) usage information
+	// per volume_type. Note: allocated attribute is available only when nested
+	// quota is enabled.
+	Extra map[string]QuotaUsage `json:"-"`
+}
+
+// UnmarshalJSON is used on QuotaUsageSet to unmarshal extra keys that are
+// used to represent QuotaUsage per volume_type.
+func (r *QuotaUsageSet) UnmarshalJSON(b []byte) error {
+	type tmp QuotaUsageSet
+	var s struct {
+		tmp
+		Extra map[string]QuotaUsage `json:"extra"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = QuotaUsageSet(s.tmp)
+
+	var result any
+	err = json.Unmarshal(b, &result)
+	if err != nil {
+		return err
+	}
+
+	// process remaining items as separate QuotaUsage objects.
+	if resultMap, ok := result.(map[string]any); ok {
+		tmpb, err := json.Marshal(gophercloud.RemainingKeys(QuotaUsageSet{}, resultMap))
+		if err != nil {
+			return err
+		}
+
+		err = json.Unmarshal(tmpb, &r.Extra)
+		if err != nil {
+			return err
+		}
+	}
+
+	return err
 }
 
 // QuotaUsage is a set of details about a single operational limit that allows
diff --git a/openstack/blockstorage/v3/quotasets/results.go b/openstack/blockstorage/v3/quotasets/results.go
index e75aea5b5..179427f4b 100644
--- a/openstack/blockstorage/v3/quotasets/results.go
+++ b/openstack/blockstorage/v3/quotasets/results.go
@@ -111,6 +111,47 @@ type QuotaUsageSet struct {
 	// Note: allocated attribute is available only when nested quota is
 	// enabled.
 	Groups QuotaUsage `json:"groups"`
+
+	// Extra is a collection of key/values that has the size (GB) usage information
+	// per volume_type. Note: allocated attribute is available only when nested
+	// quota is enabled.
+	Extra map[string]QuotaUsage `json:"-"`
+}
+
+// UnmarshalJSON is used on QuotaUsageSet to unmarshal extra keys that are
+// used to represent QuotaUsage per volume_type.
+func (r *QuotaUsageSet) UnmarshalJSON(b []byte) error {
+	type tmp QuotaUsageSet
+	var s struct {
+		tmp
+		Extra map[string]QuotaUsage `json:"extra"`
+	}
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+	*r = QuotaUsageSet(s.tmp)
+
+	var result any
+	err = json.Unmarshal(b, &result)
+	if err != nil {
+		return err
+	}
+
+	// process remaining items as separate QuotaUsage objects.
+	if resultMap, ok := result.(map[string]any); ok {
+		tmpb, err := json.Marshal(gophercloud.RemainingKeys(QuotaUsageSet{}, resultMap))
+		if err != nil {
+			return err
+		}
+
+		err = json.Unmarshal(tmpb, &r.Extra)
+		if err != nil {
+			return err
+		}
+	}
+
+	return err
 }
 
 // QuotaUsage is a set of details about a single operational limit that allows
diff --git a/openstack/compute/v2/aggregates/requests.go b/openstack/compute/v2/aggregates/requests.go
index 7111c203b..be20cbffe 100644
--- a/openstack/compute/v2/aggregates/requests.go
+++ b/openstack/compute/v2/aggregates/requests.go
@@ -66,13 +66,13 @@ func Get(ctx context.Context, client *gophercloud.ServiceClient, aggregateID int
 
 type UpdateOpts struct {
 	// The name of the host aggregate.
-	Name string `json:"name,omitempty"`
+	Name *string `json:"name,omitempty"`
 
 	// The availability zone of the host aggregate.
 	// You should use a custom availability zone rather than
 	// the default returned by the os-availability-zone API.
 	// The availability zone must not include ‘:’ in its name.
-	AvailabilityZone string `json:"availability_zone,omitempty"`
+	AvailabilityZone *string `json:"availability_zone,omitempty"`
 }
 
 func (opts UpdateOpts) ToAggregatesUpdateMap() (map[string]any, error) {
diff --git a/openstack/compute/v2/flavors/requests.go b/openstack/compute/v2/flavors/requests.go
index 119f5e78e..fe27fe97d 100644
--- a/openstack/compute/v2/flavors/requests.go
+++ b/openstack/compute/v2/flavors/requests.go
@@ -165,7 +165,7 @@ 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"`
+	Description *string `json:"description,omitempty"`
 }
 
 // ToFlavorUpdateMap constructs a request body from UpdateOpts.
diff --git a/openstack/compute/v2/hypervisors/requests.go b/openstack/compute/v2/hypervisors/requests.go
index 37ecbee17..b17dd3578 100644
--- a/openstack/compute/v2/hypervisors/requests.go
+++ b/openstack/compute/v2/hypervisors/requests.go
@@ -53,7 +53,7 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa
 	}
 
 	return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page {
-		return HypervisorPage{pagination.SinglePageBase(r)}
+		return HypervisorPage{pagination.LinkedPageBase{PageResult: r}}
 	})
 }
 
diff --git a/openstack/compute/v2/hypervisors/results.go b/openstack/compute/v2/hypervisors/results.go
index ee6046c6c..0a35dfdf0 100644
--- a/openstack/compute/v2/hypervisors/results.go
+++ b/openstack/compute/v2/hypervisors/results.go
@@ -240,7 +240,7 @@ func (r *Hypervisor) UnmarshalJSON(b []byte) error {
 // HypervisorPage represents a single page of all Hypervisors from a List
 // request.
 type HypervisorPage struct {
-	pagination.SinglePageBase
+	pagination.LinkedPageBase
 }
 
 // IsEmpty determines whether or not a HypervisorPage is empty.
@@ -253,6 +253,19 @@ func (page HypervisorPage) IsEmpty() (bool, error) {
 	return len(va) == 0, err
 }
 
+// NextPageURL uses the response's embedded link reference to navigate to the
+// next page of results.
+func (page HypervisorPage) NextPageURL() (string, error) {
+	var s struct {
+		Links []gophercloud.Link `json:"hypervisors_links"`
+	}
+	err := page.ExtractInto(&s)
+	if err != nil {
+		return "", err
+	}
+	return gophercloud.ExtractNextURL(s.Links)
+}
+
 // ExtractHypervisors interprets a page of results as a slice of Hypervisors.
 func ExtractHypervisors(p pagination.Page) ([]Hypervisor, error) {
 	var h struct {
diff --git a/openstack/compute/v2/keypairs/doc.go b/openstack/compute/v2/keypairs/doc.go
index 0e52dfc9a..b54d7945d 100644
--- a/openstack/compute/v2/keypairs/doc.go
+++ b/openstack/compute/v2/keypairs/doc.go
@@ -87,15 +87,11 @@ Example to Delete a Key Pair owned by a certain user using microversion 2.10 or
 
 Example to Create a Server With a Key Pair
 
-	serverCreateOpts := servers.CreateOpts{
+	createOpts := servers.CreateOpts{
 		Name:      "server_name",
 		ImageRef:  "image-uuid",
 		FlavorRef: "flavor-uuid",
-	}
-
-	createOpts := keypairs.CreateOptsExt{
-		CreateOptsBuilder: serverCreateOpts,
-		KeyName:           "keypair-name",
+		KeyName:   "keypair-name",
 	}
 
 	server, err := servers.Create(context.TODO(), computeClient, createOpts).Extract()
diff --git a/openstack/compute/v2/keypairs/requests.go b/openstack/compute/v2/keypairs/requests.go
index bc4cc270f..b2d17c73b 100644
--- a/openstack/compute/v2/keypairs/requests.go
+++ b/openstack/compute/v2/keypairs/requests.go
@@ -4,35 +4,9 @@ import (
 	"context"
 
 	"github.com/gophercloud/gophercloud/v2"
-	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
 
-// CreateOptsExt adds a KeyPair option to the base CreateOpts.
-type CreateOptsExt struct {
-	servers.CreateOptsBuilder
-
-	// KeyName is the name of the key pair.
-	KeyName string `json:"key_name,omitempty"`
-}
-
-// ToServerCreateMap adds the key_name to the base server creation options.
-func (opts CreateOptsExt) ToServerCreateMap() (map[string]any, error) {
-	base, err := opts.CreateOptsBuilder.ToServerCreateMap()
-	if err != nil {
-		return nil, err
-	}
-
-	if opts.KeyName == "" {
-		return base, nil
-	}
-
-	serverMap := base["server"].(map[string]any)
-	serverMap["key_name"] = opts.KeyName
-
-	return base, nil
-}
-
 // ListOptsBuilder allows extensions to add additional parameters to the
 // List request.
 type ListOptsBuilder interface {
diff --git a/openstack/compute/v2/secgroups/requests.go b/openstack/compute/v2/secgroups/requests.go
index d69a99b4f..c43575aeb 100644
--- a/openstack/compute/v2/secgroups/requests.go
+++ b/openstack/compute/v2/secgroups/requests.go
@@ -61,7 +61,7 @@ func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateO
 // UpdateOpts is the struct responsible for updating an existing security group.
 type UpdateOpts struct {
 	// the name of your security group.
-	Name string `json:"name,omitempty"`
+	Name *string `json:"name,omitempty"`
 	// the description of your security group.
 	Description *string `json:"description,omitempty"`
 }
diff --git a/openstack/compute/v2/servers/requests.go b/openstack/compute/v2/servers/requests.go
index dd3b132d1..b74b98768 100644
--- a/openstack/compute/v2/servers/requests.go
+++ b/openstack/compute/v2/servers/requests.go
@@ -508,6 +508,12 @@ type CreateOpts struct {
 
 	// DiskConfig [optional] controls how the created server's disk is partitioned.
 	DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"`
+
+	// KeyName is the name of the key pair.
+	KeyName string `json:"key_name,omitempty"`
+
+	// HypervisorHostname is the name of the hypervisor to which the server is scheduled.
+	HypervisorHostname string `json:"hypervisor_hostname,omitempty"`
 }
 
 // ToServerCreateMap assembles a request body based on the contents of a
@@ -641,13 +647,13 @@ type UpdateOpts struct {
 	// Name changes the displayed name of the server.
 	// The server host name will *not* change.
 	// Server names are not constrained to be unique, even within the same tenant.
-	Name string `json:"name,omitempty"`
+	Name *string `json:"name,omitempty"`
 
 	// AccessIPv4 provides a new IPv4 address for the instance.
-	AccessIPv4 string `json:"accessIPv4,omitempty"`
+	AccessIPv4 *string `json:"accessIPv4,omitempty"`
 
 	// AccessIPv6 provides a new IPv6 address for the instance.
-	AccessIPv6 string `json:"accessIPv6,omitempty"`
+	AccessIPv6 *string `json:"accessIPv6,omitempty"`
 }
 
 // ToServerUpdateMap formats an UpdateOpts structure into a request body.
diff --git a/openstack/compute/v2/servers/results.go b/openstack/compute/v2/servers/results.go
index dc5156417..385001c8d 100644
--- a/openstack/compute/v2/servers/results.go
+++ b/openstack/compute/v2/servers/results.go
@@ -279,6 +279,10 @@ type Server struct {
 
 	// AvailabilityZone is the availabilty zone the server is in.
 	AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"`
+
+	// Locked indicates the lock status of the server
+	// This requires microversion 2.9 or later
+	Locked *bool `json:"locked"`
 }
 
 type AttachedVolume struct {
diff --git a/openstack/db/v1/quotas/doc.go b/openstack/db/v1/quotas/doc.go
new file mode 100644
index 000000000..a4b62479d
--- /dev/null
+++ b/openstack/db/v1/quotas/doc.go
@@ -0,0 +1,7 @@
+// Package quotas provides information and interaction with the database API
+// resource in the OpenStack Database service.
+//
+// Quotas define operational limits, such as the maximum number of database
+// instances or volume storage allowed, to manage resource usage within a
+// single tenant environment in the OpenStack Database service.
+package quotas
diff --git a/openstack/db/v1/quotas/requests.go b/openstack/db/v1/quotas/requests.go
new file mode 100644
index 000000000..185e4e50b
--- /dev/null
+++ b/openstack/db/v1/quotas/requests.go
@@ -0,0 +1,14 @@
+package quotas
+
+import (
+	"context"
+
+	"github.com/gophercloud/gophercloud/v2"
+)
+
+// Get retrieves the details of quotas for a specified tenant.
+func Get(ctx context.Context, client *gophercloud.ServiceClient, projectID string) (r GetResult) {
+	resp, err := client.Get(ctx, baseURL(client, projectID), &r.Body, nil)
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
diff --git a/openstack/db/v1/quotas/results.go b/openstack/db/v1/quotas/results.go
new file mode 100644
index 000000000..ae7312df8
--- /dev/null
+++ b/openstack/db/v1/quotas/results.go
@@ -0,0 +1,29 @@
+package quotas
+
+import (
+	"github.com/gophercloud/gophercloud/v2"
+)
+
+// QuotaDetail represents a Quota API resource.
+type QuotaDetail struct {
+	Resource string
+	Limit    int
+	InUse    int `json:"in_use"`
+	Reserved int
+}
+
+// GetResult is the result of a Get operation. Call its Extract method to
+// interpret the result as a []QuotaDetail.
+type GetResult struct {
+	gophercloud.Result
+}
+
+// Extract interprets a GetResult as a []QuotaDetail.
+// An error is returned if the original call or the extraction failed.
+func (r GetResult) Extract() ([]QuotaDetail, error) {
+	var s struct {
+		Quotas []QuotaDetail `json:"quotas"`
+	}
+	err := r.ExtractInto(&s)
+	return s.Quotas, err
+}
diff --git a/openstack/db/v1/quotas/urls.go b/openstack/db/v1/quotas/urls.go
new file mode 100644
index 000000000..e0377500a
--- /dev/null
+++ b/openstack/db/v1/quotas/urls.go
@@ -0,0 +1,7 @@
+package quotas
+
+import "github.com/gophercloud/gophercloud/v2"
+
+func baseURL(c *gophercloud.ServiceClient, projectID string) string {
+	return c.ServiceURL("mgmt", "quotas", projectID)
+}
diff --git a/openstack/dns/v2/zones/requests.go b/openstack/dns/v2/zones/requests.go
index fba4371ce..b8ac4cf8a 100644
--- a/openstack/dns/v2/zones/requests.go
+++ b/openstack/dns/v2/zones/requests.go
@@ -178,3 +178,45 @@ func Delete(ctx context.Context, client *gophercloud.ServiceClient, zoneID strin
 	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
 	return
 }
+
+// request body for sharing a zone.
+type ShareOptsBuilder interface {
+	ToShareMap() (map[string]interface{}, error)
+}
+
+// ShareZoneOpts specifies the target project for sharing a zone.
+type ShareZoneOpts struct {
+	// TargetProjectID is the ID of the project to share the zone with.
+	TargetProjectID string `json:"target_project_id" required:"true"`
+}
+
+// ToShareMap constructs a request body from a ShareZoneOpts.
+func (opts ShareZoneOpts) ToShareMap() (map[string]interface{}, error) {
+	return map[string]interface{}{
+		"target_project_id": opts.TargetProjectID,
+	}, nil
+}
+
+// Share shares a zone with another project.
+func Share(ctx context.Context, client *gophercloud.ServiceClient, zoneID string, opts ShareOptsBuilder) (r gophercloud.ErrResult) {
+	body, err := gophercloud.BuildRequestBody(opts, "")
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	resp, err := client.Post(ctx, zoneShareURL(client, zoneID), body, nil, &gophercloud.RequestOpts{
+		OkCodes: []int{201},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
+// Unshare removes a share for a zone.
+func Unshare(ctx context.Context, client *gophercloud.ServiceClient, zoneID, shareID string) (r gophercloud.ErrResult) {
+	resp, err := client.Delete(ctx, zoneUnshareURL(client, zoneID, shareID), &gophercloud.RequestOpts{
+		OkCodes: []int{204},
+	})
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
diff --git a/openstack/dns/v2/zones/results.go b/openstack/dns/v2/zones/results.go
index ae2f27dbc..7ccf5ace9 100644
--- a/openstack/dns/v2/zones/results.go
+++ b/openstack/dns/v2/zones/results.go
@@ -50,6 +50,11 @@ type ZonePage struct {
 	pagination.LinkedPageBase
 }
 
+// ErrResult represents a generic error result.
+type ErrResult struct {
+	gophercloud.ErrResult
+}
+
 // IsEmpty returns true if the page contains no results.
 func (r ZonePage) IsEmpty() (bool, error) {
 	if r.StatusCode == 204 {
diff --git a/openstack/dns/v2/zones/urls.go b/openstack/dns/v2/zones/urls.go
index d157b30ef..ba156882f 100644
--- a/openstack/dns/v2/zones/urls.go
+++ b/openstack/dns/v2/zones/urls.go
@@ -2,10 +2,22 @@ package zones
 
 import "github.com/gophercloud/gophercloud/v2"
 
+// baseURL returns the base URL for zones.
 func baseURL(c *gophercloud.ServiceClient) string {
 	return c.ServiceURL("zones")
 }
 
+// zoneURL returns the URL for a specific zone.
 func zoneURL(c *gophercloud.ServiceClient, zoneID string) string {
 	return c.ServiceURL("zones", zoneID)
 }
+
+// zoneShareURL returns the URL for sharing a zone.
+func zoneShareURL(c *gophercloud.ServiceClient, zoneID string) string {
+	return c.ServiceURL("zones", zoneID, "shares")
+}
+
+// zoneUnshareURL returns the URL for unsharing a zone.
+func zoneUnshareURL(c *gophercloud.ServiceClient, zoneID, shareID string) string {
+	return c.ServiceURL("zones", zoneID, "shares", shareID)
+}
diff --git a/openstack/identity/v3/endpoints/requests.go b/openstack/identity/v3/endpoints/requests.go
index 35eb96607..4386bb9bc 100644
--- a/openstack/identity/v3/endpoints/requests.go
+++ b/openstack/identity/v3/endpoints/requests.go
@@ -90,6 +90,13 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa
 	})
 }
 
+// Get retrieves details on a single endpoint, by ID.
+func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) {
+	resp, err := client.Get(ctx, endpointURL(client, id), &r.Body, nil)
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
 // UpdateOptsBuilder allows extensions to add parameters to the Update request.
 type UpdateOptsBuilder interface {
 	ToEndpointUpdateMap() (map[string]any, error)
diff --git a/openstack/identity/v3/endpoints/results.go b/openstack/identity/v3/endpoints/results.go
index 9efec30af..19d279ebc 100644
--- a/openstack/identity/v3/endpoints/results.go
+++ b/openstack/identity/v3/endpoints/results.go
@@ -19,6 +19,12 @@ func (r commonResult) Extract() (*Endpoint, error) {
 	return s.Endpoint, err
 }
 
+// GetResult is the response from a Get operation. Call its Extract method
+// to interpret it as an Endpoint.
+type GetResult struct {
+	commonResult
+}
+
 // CreateResult is the response from a Create operation. Call its Extract
 // method to interpret it as an Endpoint.
 type CreateResult struct {
diff --git a/openstack/identity/v3/projects/doc.go b/openstack/identity/v3/projects/doc.go
index 6aea466a5..610032739 100644
--- a/openstack/identity/v3/projects/doc.go
+++ b/openstack/identity/v3/projects/doc.go
@@ -73,7 +73,7 @@ Example to List all tags of a Project
 		panic(err)
 	}
 
-Example to  modify all tags of a Project
+Example to modify all tags of a Project
 
 	projectID := "966b3c7d36a24facaf20b7e458bf2192"
 	tags := ["foo", "bar"]
diff --git a/openstack/loadbalancer/v2/apiversions/results.go b/openstack/loadbalancer/v2/apiversions/results.go
index 4d3d1ae22..11780a2a4 100644
--- a/openstack/loadbalancer/v2/apiversions/results.go
+++ b/openstack/loadbalancer/v2/apiversions/results.go
@@ -5,7 +5,7 @@ import "github.com/gophercloud/gophercloud/v2/pagination"
 // APIVersion represents an API version for load balancer. It contains
 // the status of the API, and its unique ID.
 type APIVersion struct {
-	Status string `son:"status"`
+	Status string `json:"status"`
 	ID     string `json:"id"`
 }
 
diff --git a/openstack/loadbalancer/v2/flavorprofiles/requests.go b/openstack/loadbalancer/v2/flavorprofiles/requests.go
index e51b9d99e..3c764592c 100644
--- a/openstack/loadbalancer/v2/flavorprofiles/requests.go
+++ b/openstack/loadbalancer/v2/flavorprofiles/requests.go
@@ -100,13 +100,13 @@ type UpdateOptsBuilder interface {
 // operation.
 type UpdateOpts struct {
 	// Human-readable name for the Loadbalancer. Does not have to be unique.
-	Name string `json:"name,omitempty"`
+	Name *string `json:"name,omitempty"`
 
 	// Providing the name of the provider supported by the Octavia installation.
-	ProviderName string `json:"provider_name,omitempty"`
+	ProviderName *string `json:"provider_name,omitempty"`
 
 	// Providing the json string containing the flavor metadata.
-	FlavorData string `json:"flavor_data,omitempty"`
+	FlavorData *string `json:"flavor_data,omitempty"`
 }
 
 // ToFlavorProfileUpdateMap builds a request body from UpdateOpts.
diff --git a/openstack/loadbalancer/v2/flavors/requests.go b/openstack/loadbalancer/v2/flavors/requests.go
index 710a6edf5..499bb3bd0 100644
--- a/openstack/loadbalancer/v2/flavors/requests.go
+++ b/openstack/loadbalancer/v2/flavors/requests.go
@@ -68,7 +68,7 @@ type CreateOpts struct {
 	FlavorProfileId string `json:"flavor_profile_id" required:"true"`
 
 	// If the resource is available for use. The default is True.
-	Enabled bool `json:"enabled,omitempty"`
+	Enabled *bool `json:"enabled,omitempty"`
 }
 
 // ToFlavorCreateMap builds a request body from CreateOpts.
@@ -106,13 +106,13 @@ type UpdateOptsBuilder interface {
 // operation.
 type UpdateOpts struct {
 	// Human-readable name for the Loadbalancer. Does not have to be unique.
-	Name string `json:"name,omitempty"`
+	Name *string `json:"name,omitempty"`
 
 	// Human-readable description for the Flavor.
-	Description string `json:"description,omitempty"`
+	Description *string `json:"description,omitempty"`
 
 	// If the resource is available for use.
-	Enabled bool `json:"enabled,omitempty"`
+	Enabled *bool `json:"enabled,omitempty"`
 }
 
 // ToFlavorUpdateMap builds a request body from UpdateOpts.
diff --git a/openstack/networking/v2/apiversions/results.go b/openstack/networking/v2/apiversions/results.go
index 89f30152b..90fac7347 100644
--- a/openstack/networking/v2/apiversions/results.go
+++ b/openstack/networking/v2/apiversions/results.go
@@ -7,7 +7,7 @@ import (
 // APIVersion represents an API version for Neutron. It contains the status of
 // the API, and its unique ID.
 type APIVersion struct {
-	Status string `son:"status"`
+	Status string `json:"status"`
 	ID     string `json:"id"`
 }
 
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
index f2ea9f2d1..866aeb872 100644
--- a/openstack/networking/v2/extensions/security/rules/requests.go
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -150,6 +150,23 @@ func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBu
 	return
 }
 
+// CreateBulk is an operation which adds new security group rules and associates them
+// with an existing security group (whose ID is specified in CreateOpts).
+// As of Dalmatian (2024.2) neutron only allows bulk creation of rules when
+// they all belong to the same tenant and security group.
+// https://github.com/openstack/neutron/blob/6183792/neutron/db/securitygroups_db.py#L814-L828
+func CreateBulk[createOpts CreateOptsBuilder](ctx context.Context, c *gophercloud.ServiceClient, opts []createOpts) (r CreateBulkResult) {
+	body, err := gophercloud.BuildRequestBody(opts, "security_group_rules")
+	if err != nil {
+		r.Err = err
+		return
+	}
+
+	resp, err := c.Post(ctx, rootURL(c), body, &r.Body, nil)
+	_, r.Header, r.Err = gophercloud.ParseResponse(resp, err)
+	return
+}
+
 // Get retrieves a particular security group rule based on its unique ID.
 func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) {
 	resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil)
diff --git a/openstack/networking/v2/extensions/security/rules/results.go b/openstack/networking/v2/extensions/security/rules/results.go
index cfdb27fa1..0901ced57 100644
--- a/openstack/networking/v2/extensions/security/rules/results.go
+++ b/openstack/networking/v2/extensions/security/rules/results.go
@@ -103,6 +103,10 @@ type commonResult struct {
 	gophercloud.Result
 }
 
+type bulkResult struct {
+	gophercloud.Result
+}
+
 // Extract is a function that accepts a result and extracts a security rule.
 func (r commonResult) Extract() (*SecGroupRule, error) {
 	var s struct {
@@ -112,12 +116,27 @@ func (r commonResult) Extract() (*SecGroupRule, error) {
 	return s.SecGroupRule, err
 }
 
+// Extract is a function that accepts a result and extracts security rules.
+func (r bulkResult) Extract() ([]SecGroupRule, error) {
+	var s struct {
+		SecGroupRules []SecGroupRule `json:"security_group_rules"`
+	}
+	err := r.ExtractInto(&s)
+	return s.SecGroupRules, err
+}
+
 // CreateResult represents the result of a create operation. Call its Extract
 // method to interpret it as a SecGroupRule.
 type CreateResult struct {
 	commonResult
 }
 
+// CreateBulkResult represents the result of a bulk create operation. Call its
+// Extract method to interpret it as a slice of SecGroupRules.
+type CreateBulkResult struct {
+	bulkResult
+}
+
 // GetResult represents the result of a get operation. Call its Extract
 // method to interpret it as a SecGroupRule.
 type GetResult struct {
diff --git a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go
index 7d5c81e13..f0845d6a6 100644
--- a/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go
+++ b/openstack/networking/v2/extensions/vpnaas/ikepolicies/requests.go
@@ -15,22 +15,62 @@ type IKEVersion string
 type Phase1NegotiationMode string
 
 const (
-	AuthAlgorithmSHA1         AuthAlgorithm         = "sha1"
-	AuthAlgorithmSHA256       AuthAlgorithm         = "sha256"
-	AuthAlgorithmSHA384       AuthAlgorithm         = "sha384"
-	AuthAlgorithmSHA512       AuthAlgorithm         = "sha512"
-	EncryptionAlgorithm3DES   EncryptionAlgorithm   = "3des"
-	EncryptionAlgorithmAES128 EncryptionAlgorithm   = "aes-128"
-	EncryptionAlgorithmAES256 EncryptionAlgorithm   = "aes-256"
-	EncryptionAlgorithmAES192 EncryptionAlgorithm   = "aes-192"
-	UnitSeconds               Unit                  = "seconds"
-	UnitKilobytes             Unit                  = "kilobytes"
-	PFSGroup2                 PFS                   = "group2"
-	PFSGroup5                 PFS                   = "group5"
-	PFSGroup14                PFS                   = "group14"
-	IKEVersionv1              IKEVersion            = "v1"
-	IKEVersionv2              IKEVersion            = "v2"
-	Phase1NegotiationModeMain Phase1NegotiationMode = "main"
+	AuthAlgorithmSHA1              AuthAlgorithm         = "sha1"
+	AuthAlgorithmSHA256            AuthAlgorithm         = "sha256"
+	AuthAlgorithmSHA384            AuthAlgorithm         = "sha384"
+	AuthAlgorithmSHA512            AuthAlgorithm         = "sha512"
+	AuthAlgorithmAESXCBC           AuthAlgorithm         = "aes-xcbc"
+	AuthAlgorithmAESCMAC           AuthAlgorithm         = "aes-cmac"
+	EncryptionAlgorithm3DES        EncryptionAlgorithm   = "3des"
+	EncryptionAlgorithmAES128      EncryptionAlgorithm   = "aes-128"
+	EncryptionAlgorithmAES192      EncryptionAlgorithm   = "aes-192"
+	EncryptionAlgorithmAES256      EncryptionAlgorithm   = "aes-256"
+	EncryptionAlgorithmAES128CTR   EncryptionAlgorithm   = "aes-128-ctr"
+	EncryptionAlgorithmAES192CTR   EncryptionAlgorithm   = "aes-192-ctr"
+	EncryptionAlgorithmAES256CTR   EncryptionAlgorithm   = "aes-256-ctr"
+	EncryptionAlgorithmAES128CCM8  EncryptionAlgorithm   = "aes-128-ccm-8"
+	EncryptionAlgorithmAES128CCM12 EncryptionAlgorithm   = "aes-128-ccm-12"
+	EncryptionAlgorithmAES128CCM16 EncryptionAlgorithm   = "aes-128-ccm-16"
+	EncryptionAlgorithmAES192CCM8  EncryptionAlgorithm   = "aes-192-ccm-8"
+	EncryptionAlgorithmAES192CCM12 EncryptionAlgorithm   = "aes-192-ccm-12"
+	EncryptionAlgorithmAES192CCM16 EncryptionAlgorithm   = "aes-192-ccm-16"
+	EncryptionAlgorithmAES256CCM8  EncryptionAlgorithm   = "aes-256-ccm-8"
+	EncryptionAlgorithmAES256CCM12 EncryptionAlgorithm   = "aes-256-ccm-12"
+	EncryptionAlgorithmAES256CCM16 EncryptionAlgorithm   = "aes-256-ccm-16"
+	EncryptionAlgorithmAES128GCM8  EncryptionAlgorithm   = "aes-128-gcm-8"
+	EncryptionAlgorithmAES128GCM12 EncryptionAlgorithm   = "aes-128-gcm-12"
+	EncryptionAlgorithmAES128GCM16 EncryptionAlgorithm   = "aes-128-gcm-16"
+	EncryptionAlgorithmAES192GCM8  EncryptionAlgorithm   = "aes-192-gcm-8"
+	EncryptionAlgorithmAES192GCM12 EncryptionAlgorithm   = "aes-192-gcm-12"
+	EncryptionAlgorithmAES192GCM16 EncryptionAlgorithm   = "aes-192-gcm-16"
+	EncryptionAlgorithmAES256GCM8  EncryptionAlgorithm   = "aes-256-gcm-8"
+	EncryptionAlgorithmAES256GCM12 EncryptionAlgorithm   = "aes-256-gcm-12"
+	EncryptionAlgorithmAES256GCM16 EncryptionAlgorithm   = "aes-256-gcm-16"
+	UnitSeconds                    Unit                  = "seconds"
+	UnitKilobytes                  Unit                  = "kilobytes"
+	PFSGroup2                      PFS                   = "group2"
+	PFSGroup5                      PFS                   = "group5"
+	PFSGroup14                     PFS                   = "group14"
+	PFSGroup15                     PFS                   = "group15"
+	PFSGroup16                     PFS                   = "group16"
+	PFSGroup17                     PFS                   = "group17"
+	PFSGroup18                     PFS                   = "group18"
+	PFSGroup19                     PFS                   = "group19"
+	PFSGroup20                     PFS                   = "group20"
+	PFSGroup21                     PFS                   = "group21"
+	PFSGroup22                     PFS                   = "group22"
+	PFSGroup23                     PFS                   = "group23"
+	PFSGroup24                     PFS                   = "group24"
+	PFSGroup25                     PFS                   = "group25"
+	PFSGroup26                     PFS                   = "group26"
+	PFSGroup27                     PFS                   = "group27"
+	PFSGroup28                     PFS                   = "group28"
+	PFSGroup29                     PFS                   = "group29"
+	PFSGroup30                     PFS                   = "group30"
+	PFSGroup31                     PFS                   = "group31"
+	IKEVersionv1                   IKEVersion            = "v1"
+	IKEVersionv2                   IKEVersion            = "v2"
+	Phase1NegotiationModeMain      Phase1NegotiationMode = "main"
 )
 
 // CreateOptsBuilder allows extensions to add additional parameters to the
diff --git a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go
index 4397c0587..9f34895c1 100644
--- a/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go
+++ b/openstack/networking/v2/extensions/vpnaas/ipsecpolicies/requests.go
@@ -15,24 +15,64 @@ type PFS string
 type Unit string
 
 const (
-	TransformProtocolESP       TransformProtocol   = "esp"
-	TransformProtocolAH        TransformProtocol   = "ah"
-	TransformProtocolAHESP     TransformProtocol   = "ah-esp"
-	AuthAlgorithmSHA1          AuthAlgorithm       = "sha1"
-	AuthAlgorithmSHA256        AuthAlgorithm       = "sha256"
-	AuthAlgorithmSHA384        AuthAlgorithm       = "sha384"
-	AuthAlgorithmSHA512        AuthAlgorithm       = "sha512"
-	EncryptionAlgorithm3DES    EncryptionAlgorithm = "3des"
-	EncryptionAlgorithmAES128  EncryptionAlgorithm = "aes-128"
-	EncryptionAlgorithmAES256  EncryptionAlgorithm = "aes-256"
-	EncryptionAlgorithmAES192  EncryptionAlgorithm = "aes-192"
-	EncapsulationModeTunnel    EncapsulationMode   = "tunnel"
-	EncapsulationModeTransport EncapsulationMode   = "transport"
-	UnitSeconds                Unit                = "seconds"
-	UnitKilobytes              Unit                = "kilobytes"
-	PFSGroup2                  PFS                 = "group2"
-	PFSGroup5                  PFS                 = "group5"
-	PFSGroup14                 PFS                 = "group14"
+	TransformProtocolESP           TransformProtocol   = "esp"
+	TransformProtocolAH            TransformProtocol   = "ah"
+	TransformProtocolAHESP         TransformProtocol   = "ah-esp"
+	AuthAlgorithmSHA1              AuthAlgorithm       = "sha1"
+	AuthAlgorithmSHA256            AuthAlgorithm       = "sha256"
+	AuthAlgorithmSHA384            AuthAlgorithm       = "sha384"
+	AuthAlgorithmSHA512            AuthAlgorithm       = "sha512"
+	AuthAlgorithmAESXCBC           AuthAlgorithm       = "aes-xcbc"
+	AuthAlgorithmAESCMAC           AuthAlgorithm       = "aes-cmac"
+	EncryptionAlgorithm3DES        EncryptionAlgorithm = "3des"
+	EncryptionAlgorithmAES128      EncryptionAlgorithm = "aes-128"
+	EncryptionAlgorithmAES192      EncryptionAlgorithm = "aes-192"
+	EncryptionAlgorithmAES256      EncryptionAlgorithm = "aes-256"
+	EncryptionAlgorithmAES128CTR   EncryptionAlgorithm = "aes-128-ctr"
+	EncryptionAlgorithmAES192CTR   EncryptionAlgorithm = "aes-192-ctr"
+	EncryptionAlgorithmAES256CTR   EncryptionAlgorithm = "aes-256-ctr"
+	EncryptionAlgorithmAES128CCM8  EncryptionAlgorithm = "aes-128-ccm-8"
+	EncryptionAlgorithmAES128CCM12 EncryptionAlgorithm = "aes-128-ccm-12"
+	EncryptionAlgorithmAES128CCM16 EncryptionAlgorithm = "aes-128-ccm-16"
+	EncryptionAlgorithmAES192CCM8  EncryptionAlgorithm = "aes-192-ccm-8"
+	EncryptionAlgorithmAES192CCM12 EncryptionAlgorithm = "aes-192-ccm-12"
+	EncryptionAlgorithmAES192CCM16 EncryptionAlgorithm = "aes-192-ccm-16"
+	EncryptionAlgorithmAES256CCM8  EncryptionAlgorithm = "aes-256-ccm-8"
+	EncryptionAlgorithmAES256CCM12 EncryptionAlgorithm = "aes-256-ccm-12"
+	EncryptionAlgorithmAES256CCM16 EncryptionAlgorithm = "aes-256-ccm-16"
+	EncryptionAlgorithmAES128GCM8  EncryptionAlgorithm = "aes-128-gcm-8"
+	EncryptionAlgorithmAES128GCM12 EncryptionAlgorithm = "aes-128-gcm-12"
+	EncryptionAlgorithmAES128GCM16 EncryptionAlgorithm = "aes-128-gcm-16"
+	EncryptionAlgorithmAES192GCM8  EncryptionAlgorithm = "aes-192-gcm-8"
+	EncryptionAlgorithmAES192GCM12 EncryptionAlgorithm = "aes-192-gcm-12"
+	EncryptionAlgorithmAES192GCM16 EncryptionAlgorithm = "aes-192-gcm-16"
+	EncryptionAlgorithmAES256GCM8  EncryptionAlgorithm = "aes-256-gcm-8"
+	EncryptionAlgorithmAES256GCM12 EncryptionAlgorithm = "aes-256-gcm-12"
+	EncryptionAlgorithmAES256GCM16 EncryptionAlgorithm = "aes-256-gcm-16"
+	EncapsulationModeTunnel        EncapsulationMode   = "tunnel"
+	EncapsulationModeTransport     EncapsulationMode   = "transport"
+	UnitSeconds                    Unit                = "seconds"
+	UnitKilobytes                  Unit                = "kilobytes"
+	PFSGroup2                      PFS                 = "group2"
+	PFSGroup5                      PFS                 = "group5"
+	PFSGroup14                     PFS                 = "group14"
+	PFSGroup15                     PFS                 = "group15"
+	PFSGroup16                     PFS                 = "group16"
+	PFSGroup17                     PFS                 = "group17"
+	PFSGroup18                     PFS                 = "group18"
+	PFSGroup19                     PFS                 = "group19"
+	PFSGroup20                     PFS                 = "group20"
+	PFSGroup21                     PFS                 = "group21"
+	PFSGroup22                     PFS                 = "group22"
+	PFSGroup23                     PFS                 = "group23"
+	PFSGroup24                     PFS                 = "group24"
+	PFSGroup25                     PFS                 = "group25"
+	PFSGroup26                     PFS                 = "group26"
+	PFSGroup27                     PFS                 = "group27"
+	PFSGroup28                     PFS                 = "group28"
+	PFSGroup29                     PFS                 = "group29"
+	PFSGroup30                     PFS                 = "group30"
+	PFSGroup31                     PFS                 = "group31"
 )
 
 // CreateOptsBuilder allows extensions to add additional parameters to the
diff --git a/openstack/objectstorage/v1/containers/results.go b/openstack/objectstorage/v1/containers/results.go
index 8e71c1506..fddfa26bf 100644
--- a/openstack/objectstorage/v1/containers/results.go
+++ b/openstack/objectstorage/v1/containers/results.go
@@ -111,6 +111,8 @@ type GetHeader struct {
 	TempURLKey2      string    `json:"X-Container-Meta-Temp-URL-Key-2"`
 	Timestamp        float64   `json:"X-Timestamp,string"`
 	VersionsEnabled  bool      `json:"-"`
+	SyncKey          string    `json:"X-Sync-Key"`
+	SyncTo           string    `json:"X-Sync-To"`
 }
 
 func (r *GetHeader) UnmarshalJSON(b []byte) error {
diff --git a/params.go b/params.go
index 09b322a6a..4a2ed6c94 100644
--- a/params.go
+++ b/params.go
@@ -11,9 +11,11 @@ import (
 )
 
 /*
-BuildRequestBody builds a map[string]interface from the given `struct`. If
-parent is not an empty string, the final map[string]interface returned will
-encapsulate the built one. For example:
+BuildRequestBody builds a map[string]interface from the given `struct`, or
+collection of `structs`. If parent is not an empty string, the final
+map[string]interface returned will encapsulate the built one. Parent is
+required when passing a list of `structs`.
+For example:
 
 	disk := 1
 	createOpts := flavors.CreateOpts{
@@ -27,7 +29,29 @@ encapsulate the built one. For example:
 
 	body, err := gophercloud.BuildRequestBody(createOpts, "flavor")
 
-The above example can be run as-is, however it is recommended to look at how
+
+	opts := []rules.CreateOpts{
+		{
+			Direction:     "ingress",
+			PortRangeMin:  80,
+			EtherType:     rules.EtherType4,
+			PortRangeMax:  80,
+			Protocol:      "tcp",
+			SecGroupID:    "a7734e61-b545-452d-a3cd-0189cbd9747a",
+		},
+		{
+			Direction:    "ingress",
+			PortRangeMin: 443,
+			EtherType:    rules.EtherType4,
+			PortRangeMax: 443,
+			Protocol:     "tcp",
+			SecGroupID:   "a7734e61-b545-452d-a3cd-0189cbd9747a",
+		},
+	}
+
+	body, err := gophercloud.BuildRequestBody(opts, "security_group_rules")
+
+The above examples can be run as-is, however it is recommended to look at how
 BuildRequestBody is used within Gophercloud to more fully understand how it
 fits within the request process as a whole rather than use it directly as shown
 above.
@@ -44,7 +68,8 @@ func BuildRequestBody(opts any, parent string) (map[string]any, error) {
 	}
 
 	optsMap := make(map[string]any)
-	if optsValue.Kind() == reflect.Struct {
+	switch optsValue.Kind() {
+	case reflect.Struct:
 		//fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind())
 		for i := 0; i < optsValue.NumField(); i++ {
 			v := optsValue.Field(i)
@@ -184,9 +209,22 @@ func BuildRequestBody(opts any, parent string) (map[string]any, error) {
 		}
 		//fmt.Printf("optsMap after parent added: %+v\n", optsMap)
 		return optsMap, nil
+	case reflect.Slice, reflect.Array:
+		optsMaps := make([]map[string]any, optsValue.Len())
+		for i := 0; i < optsValue.Len(); i++ {
+			b, err := BuildRequestBody(optsValue.Index(i).Interface(), "")
+			if err != nil {
+				return nil, err
+			}
+			optsMaps[i] = b
+		}
+		if parent == "" {
+			return nil, fmt.Errorf("Parent is required when passing an array or a slice.")
+		}
+		return map[string]any{parent: optsMaps}, nil
 	}
-	// Return an error if the underlying type of 'opts' isn't a struct.
-	return nil, fmt.Errorf("Options type is not a struct.")
+	// Return an error if we can't work with the underlying type of 'opts'
+	return nil, fmt.Errorf("Options type is not a struct, a slice, or an array.")
 }
 
 // EnabledState is a convenience type, mostly used in Create and Update
diff --git a/results.go b/results.go
index 9e6f630ab..b12c15a02 100644
--- a/results.go
+++ b/results.go
@@ -184,10 +184,19 @@ func (r Result) ExtractIntoStructPtr(to any, label string) error {
 		return r.Err
 	}
 
+	if to == nil {
+		return fmt.Errorf("Expected pointer, got %T", to)
+	}
+
 	t := reflect.TypeOf(to)
 	if k := t.Kind(); k != reflect.Ptr {
 		return fmt.Errorf("Expected pointer, got %v", k)
 	}
+
+	if reflect.ValueOf(to).IsNil() {
+		return fmt.Errorf("Expected pointer, got %T", to)
+	}
+
 	switch t.Elem().Kind() {
 	case reflect.Struct:
 		return r.extractIntoPtr(to, label)
@@ -210,10 +219,19 @@ func (r Result) ExtractIntoSlicePtr(to any, label string) error {
 		return r.Err
 	}
 
+	if to == nil {
+		return fmt.Errorf("Expected pointer, got %T", to)
+	}
+
 	t := reflect.TypeOf(to)
 	if k := t.Kind(); k != reflect.Ptr {
 		return fmt.Errorf("Expected pointer, got %v", k)
 	}
+
+	if reflect.ValueOf(to).IsNil() {
+		return fmt.Errorf("Expected pointer, got %T", to)
+	}
+
 	switch t.Elem().Kind() {
 	case reflect.Slice:
 		return r.extractIntoPtr(to, label)



Test patch:
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
new file mode 100644
index 000000000..b3ed1948e
--- /dev/null
+++ b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
@@ -0,0 +1,50 @@
+package httpbasic
+
+import (
+	"context"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	v1 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/baremetal/v1"
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+)
+
+func TestPortGroupsCreateDestroy(t *testing.T) {
+	clients.RequireLong(t)
+	clients.RequireIronicHTTPBasic(t)
+
+	client, err := clients.NewBareMetalV1HTTPBasic()
+	th.AssertNoErr(t, err)
+
+	// NOTE(sharpz7) - increased due to create fake node requiring it.
+	client.Microversion = "1.50"
+
+	node, err := v1.CreateFakeNode(t, client)
+	th.AssertNoErr(t, err)
+	defer v1.DeleteNode(t, client, node)
+
+	portgroup, err := v1.CreatePortGroup(t, client, node)
+	th.AssertNoErr(t, err)
+	defer v1.DeletePortGroup(t, client, portgroup)
+
+	// Verify the portgroup exists by listing
+	err = portgroups.List(client, portgroups.ListOpts{
+		Node: node.UUID,
+	}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
+		pg, err := portgroups.ExtractPortGroups(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, p := range pg {
+			if p.UUID == portgroup.UUID {
+				return true, nil
+			}
+		}
+
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go
new file mode 100644
index 000000000..c4b95bba6
--- /dev/null
+++ b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go
@@ -0,0 +1,49 @@
+package noauth
+
+import (
+	"context"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	v1 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/baremetal/v1"
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+)
+
+func TestPortGroupsCreateDestroy(t *testing.T) {
+	clients.RequireLong(t)
+
+	client, err := clients.NewBareMetalV1Client()
+	th.AssertNoErr(t, err)
+
+	// NOTE(sharpz7) - increased due to create fake node requiring it.
+	client.Microversion = "1.50"
+
+	node, err := v1.CreateFakeNode(t, client)
+	th.AssertNoErr(t, err)
+	defer v1.DeleteNode(t, client, node)
+
+	portgroup, err := v1.CreatePortGroup(t, client, node)
+	th.AssertNoErr(t, err)
+	defer v1.DeletePortGroup(t, client, portgroup)
+
+	// Verify the portgroup exists by listing
+	err = portgroups.List(client, portgroups.ListOpts{
+		Node: node.UUID,
+	}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
+		pg, err := portgroups.ExtractPortGroups(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, p := range pg {
+			if p.UUID == portgroup.UUID {
+				return true, nil
+			}
+		}
+
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/internal/acceptance/openstack/baremetal/v1/nodes_test.go b/internal/acceptance/openstack/baremetal/v1/nodes_test.go
index 669ea69cd..1151135e6 100644
--- a/internal/acceptance/openstack/baremetal/v1/nodes_test.go
+++ b/internal/acceptance/openstack/baremetal/v1/nodes_test.go
@@ -272,3 +272,51 @@ func TestNodesServicingHold(t *testing.T) {
 	}, nodes.Active)
 	th.AssertNoErr(t, err)
 }
+
+func TestNodesVirtualInterfaces(t *testing.T) {
+	clients.SkipReleasesBelow(t, "stable/2023.2") // Adjust based on when this feature was added
+	clients.RequireLong(t)
+
+	client, err := clients.NewBareMetalV1Client()
+	th.AssertNoErr(t, err)
+	// VIFs were added in API version 1.28, but at least 1.38 is needed for tests to pass
+	client.Microversion = "1.38"
+
+	node, err := CreateNode(t, client)
+	th.AssertNoErr(t, err)
+	defer DeleteNode(t, client, node)
+
+	// First, list VIFs (should be empty initially)
+	vifs, err := nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 0, len(vifs))
+
+	// For a real test, we would need a valid VIF ID from the networking service
+	// Since this is difficult in a test environment, we can test the API call
+	// with a fake ID and expect it to fail with a specific error
+	fakeVifID := "1974dcfa-836f-41b2-b541-686c100900e5"
+
+	// Try to attach a VIF (this will likely fail with a 404 Not Found since the VIF doesn't exist)
+	err = nodes.AttachVirtualInterface(context.TODO(), client, node.UUID, nodes.VirtualInterfaceOpts{
+		ID: fakeVifID,
+	}).ExtractErr()
+
+	// We expect this to fail, but we're testing the API call itself
+	// In a real environment with valid VIFs, you would check for success instead
+	if err == nil {
+		t.Logf("Warning: Expected error when attaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID)
+	}
+
+	// Try to detach a VIF (this will likely fail with a 404 Not Found)
+	err = nodes.DetachVirtualInterface(context.TODO(), client, node.UUID, fakeVifID).ExtractErr()
+
+	// Again, we expect this to fail in most test environments
+	if err == nil {
+		t.Logf("Warning: Expected error when detaching non-existent VIF, but got success. This might indicate the test environment has a VIF with ID %s", fakeVifID)
+	}
+
+	// List VIFs again to confirm state hasn't changed
+	vifs, err = nodes.ListVirtualInterfaces(context.TODO(), client, node.UUID).Extract()
+	th.AssertNoErr(t, err)
+	th.AssertEquals(t, 0, len(vifs))
+}
diff --git a/internal/acceptance/openstack/baremetal/v1/portgroups_test.go b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go
new file mode 100644
index 000000000..6f04223a0
--- /dev/null
+++ b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go
@@ -0,0 +1,48 @@
+package v1
+
+import (
+	"context"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+)
+
+func TestPortGroupsCreateDestroy(t *testing.T) {
+	clients.RequireLong(t)
+
+	client, err := clients.NewBareMetalV1Client()
+	th.AssertNoErr(t, err)
+
+	// NOTE(sharpz7) - increased due to create fake node requiring it.
+	client.Microversion = "1.50"
+
+	node, err := CreateFakeNode(t, client)
+	th.AssertNoErr(t, err)
+	defer DeleteNode(t, client, node)
+
+	portgroup, err := CreatePortGroup(t, client, node)
+	th.AssertNoErr(t, err)
+	defer DeletePortGroup(t, client, portgroup)
+
+	// Verify the portgroup exists by listing
+	err = portgroups.List(client, portgroups.ListOpts{
+		Node: node.UUID,
+	}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
+		pg, err := portgroups.ExtractPortGroups(page)
+		if err != nil {
+			return false, err
+		}
+
+		for _, p := range pg {
+			if p.UUID == portgroup.UUID {
+				return true, nil
+			}
+		}
+
+		return false, nil
+	})
+	th.AssertNoErr(t, err)
+}
diff --git a/internal/acceptance/openstack/blockstorage/v3/quotaset_test.go b/internal/acceptance/openstack/blockstorage/v3/quotaset_test.go
index 327ea0d79..125d9f9d6 100644
--- a/internal/acceptance/openstack/blockstorage/v3/quotaset_test.go
+++ b/internal/acceptance/openstack/blockstorage/v3/quotaset_test.go
@@ -46,6 +46,7 @@ func TestQuotasetGetUsage(t *testing.T) {
 	th.AssertNoErr(t, err)
 
 	tools.PrintResource(t, quotaSetUsage)
+	tools.PrintResource(t, quotaSetUsage.Extra)
 }
 
 var UpdateQuotaOpts = quotasets.UpdateOpts{
diff --git a/internal/acceptance/openstack/compute/v2/aggregates_test.go b/internal/acceptance/openstack/compute/v2/aggregates_test.go
index 88ac3a336..4b20b3ff2 100644
--- a/internal/acceptance/openstack/compute/v2/aggregates_test.go
+++ b/internal/acceptance/openstack/compute/v2/aggregates_test.go
@@ -11,6 +11,7 @@ import (
 	"github.com/gophercloud/gophercloud/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/hypervisors"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -47,8 +48,8 @@ func TestAggregatesCRUD(t *testing.T) {
 	tools.PrintResource(t, aggregate)
 
 	updateOpts := aggregates.UpdateOpts{
-		Name:             "new_aggregate_name",
-		AvailabilityZone: "new_azone",
+		Name:             ptr.To("new_aggregate_name"),
+		AvailabilityZone: ptr.To("new_azone"),
 	}
 
 	updatedAggregate, err := aggregates.Update(context.TODO(), client, aggregate.ID, updateOpts).Extract()
diff --git a/internal/acceptance/openstack/compute/v2/flavors_test.go b/internal/acceptance/openstack/compute/v2/flavors_test.go
index 412149adf..7a6ca6b2d 100644
--- a/internal/acceptance/openstack/compute/v2/flavors_test.go
+++ b/internal/acceptance/openstack/compute/v2/flavors_test.go
@@ -135,7 +135,7 @@ func TestFlavorsCreateUpdateDelete(t *testing.T) {
 
 	newFlavorDescription := "This is the new description"
 	updateOpts := flavors.UpdateOpts{
-		Description: newFlavorDescription,
+		Description: &newFlavorDescription,
 	}
 
 	flavor, err = flavors.Update(context.TODO(), client, flavor.ID, updateOpts).Extract()
diff --git a/internal/acceptance/openstack/compute/v2/secgroup_test.go b/internal/acceptance/openstack/compute/v2/secgroup_test.go
index dd6b6dd85..0a883d378 100644
--- a/internal/acceptance/openstack/compute/v2/secgroup_test.go
+++ b/internal/acceptance/openstack/compute/v2/secgroup_test.go
@@ -48,7 +48,7 @@ func TestSecGroupsCRUD(t *testing.T) {
 	newName := tools.RandomString("secgroup_", 4)
 	description := ""
 	updateOpts := secgroups.UpdateOpts{
-		Name:        newName,
+		Name:        &newName,
 		Description: &description,
 	}
 	updatedSecurityGroup, err := secgroups.Update(context.TODO(), client, securityGroup.ID, updateOpts).Extract()
diff --git a/internal/acceptance/openstack/compute/v2/servers_test.go b/internal/acceptance/openstack/compute/v2/servers_test.go
index 5934805ba..67ef63028 100644
--- a/internal/acceptance/openstack/compute/v2/servers_test.go
+++ b/internal/acceptance/openstack/compute/v2/servers_test.go
@@ -133,7 +133,7 @@ func TestServersUpdate(t *testing.T) {
 	t.Logf("Attempting to rename the server to %s.", alternateName)
 
 	updateOpts := servers.UpdateOpts{
-		Name: alternateName,
+		Name: &alternateName,
 	}
 
 	updated, err := servers.Update(context.TODO(), client, server.ID, updateOpts).Extract()
diff --git a/internal/acceptance/openstack/identity/v3/endpoint_test.go b/internal/acceptance/openstack/identity/v3/endpoint_test.go
index ae2254102..8dd9e5bad 100644
--- a/internal/acceptance/openstack/identity/v3/endpoint_test.go
+++ b/internal/acceptance/openstack/identity/v3/endpoint_test.go
@@ -39,6 +39,29 @@ func TestEndpointsList(t *testing.T) {
 	th.AssertEquals(t, found, true)
 }
 
+func TestEndpointsGet(t *testing.T) {
+	clients.RequireAdmin(t)
+
+	client, err := clients.NewIdentityV3Client()
+	th.AssertNoErr(t, err)
+
+	allPages, err := endpoints.List(client, nil).AllPages(context.TODO())
+	th.AssertNoErr(t, err)
+
+	allEndpoints, err := endpoints.ExtractEndpoints(allPages)
+	th.AssertNoErr(t, err)
+
+	endpoint := allEndpoints[0]
+	e, err := endpoints.Get(context.TODO(), client, endpoint.ID).Extract()
+	if err != nil {
+		t.Fatalf("Unable to get endpoint: %v", err)
+	}
+
+	tools.PrintResource(t, e)
+
+	th.AssertEquals(t, e.Name, e.Name)
+}
+
 func TestEndpointsNavigateCatalog(t *testing.T) {
 	clients.RequireAdmin(t)
 
diff --git a/internal/acceptance/openstack/loadbalancer/v2/flavorprofiles_test.go b/internal/acceptance/openstack/loadbalancer/v2/flavorprofiles_test.go
index 1a6ebadf2..7151a3458 100644
--- a/internal/acceptance/openstack/loadbalancer/v2/flavorprofiles_test.go
+++ b/internal/acceptance/openstack/loadbalancer/v2/flavorprofiles_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavorprofiles"
 
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -41,13 +42,13 @@ func TestFlavorProfilesCRUD(t *testing.T) {
 	th.AssertEquals(t, "amphora", flavorProfile.ProviderName)
 
 	flavorProfileUpdateOpts := flavorprofiles.UpdateOpts{
-		Name: tools.RandomString("TESTACCTUP-", 8),
+		Name: ptr.To(tools.RandomString("TESTACCTUP-", 8)),
 	}
 
 	flavorProfileUpdated, err := flavorprofiles.Update(context.TODO(), lbClient, flavorProfile.ID, flavorProfileUpdateOpts).Extract()
 	th.AssertNoErr(t, err)
 
-	th.AssertEquals(t, flavorProfileUpdateOpts.Name, flavorProfileUpdated.Name)
+	th.AssertEquals(t, *flavorProfileUpdateOpts.Name, flavorProfileUpdated.Name)
 
 	t.Logf("Successfully updated flavorprofile %s", flavorProfileUpdated.Name)
 }
diff --git a/internal/acceptance/openstack/loadbalancer/v2/flavors_test.go b/internal/acceptance/openstack/loadbalancer/v2/flavors_test.go
index b714af732..40bd1cc11 100644
--- a/internal/acceptance/openstack/loadbalancer/v2/flavors_test.go
+++ b/internal/acceptance/openstack/loadbalancer/v2/flavors_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
 )
@@ -54,13 +55,13 @@ func TestFlavorsCRUD(t *testing.T) {
 	th.AssertEquals(t, flavor.FlavorProfileId, flavorProfile.ID)
 
 	flavorUpdateOpts := flavors.UpdateOpts{
-		Name: tools.RandomString("TESTACCTUP-", 8),
+		Name: ptr.To(tools.RandomString("TESTACCTUP-", 8)),
 	}
 
 	flavorUpdated, err := flavors.Update(context.TODO(), lbClient, flavor.ID, flavorUpdateOpts).Extract()
 	th.AssertNoErr(t, err)
 
-	th.AssertEquals(t, flavorUpdateOpts.Name, flavorUpdated.Name)
+	th.AssertEquals(t, *flavorUpdateOpts.Name, flavorUpdated.Name)
 
 	t.Logf("Successfully updated flavor %s", flavorUpdated.Name)
 }
diff --git a/internal/acceptance/openstack/networking/v2/extensions/security_test.go b/internal/acceptance/openstack/networking/v2/extensions/security_test.go
index 670735ec5..8d855bbb0 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/security_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/security_test.go
@@ -26,6 +26,12 @@ func TestSecurityGroupsCreateUpdateDelete(t *testing.T) {
 	th.AssertNoErr(t, err)
 	defer DeleteSecurityGroupRule(t, client, rule.ID)
 
+	rules, err := CreateSecurityGroupRulesBulk(t, client, group.ID)
+	th.AssertNoErr(t, err)
+	for _, r := range rules {
+		defer DeleteSecurityGroupRule(t, client, r.ID)
+	}
+
 	tools.PrintResource(t, group)
 
 	var name = "Update group"
diff --git a/openstack/baremetal/v1/nodes/testing/fixtures_test.go b/openstack/baremetal/v1/nodes/testing/fixtures_test.go
index 88abebceb..282d5dbc7 100644
--- a/openstack/baremetal/v1/nodes/testing/fixtures_test.go
+++ b/openstack/baremetal/v1/nodes/testing/fixtures_test.go
@@ -1849,3 +1849,78 @@ func HandleDetachVirtualMediaSuccessfully(t *testing.T, withType bool) {
 		w.WriteHeader(http.StatusNoContent)
 	})
 }
+
+// HandleListVirtualInterfacesSuccessfully sets up the test server to respond to a ListVirtualInterfaces request
+func HandleListVirtualInterfacesSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/vifs",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "GET")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Accept", "application/json")
+
+			w.Header().Add("Content-Type", "application/json")
+			w.WriteHeader(http.StatusOK)
+			fmt.Fprintf(w, `
+{
+  "vifs": [
+    {
+      "id": "1974dcfa-836f-41b2-b541-686c100900e5"
+    }
+  ]
+}`)
+		})
+}
+
+// HandleAttachVirtualInterfaceSuccessfully sets up the test server to respond to an AttachVirtualInterface request
+func HandleAttachVirtualInterfaceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/vifs",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "POST")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Content-Type", "application/json")
+			th.TestHeader(t, r, "Accept", "application/json")
+			th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5"}`)
+
+			w.WriteHeader(http.StatusNoContent)
+		})
+}
+
+// HandleAttachVirtualInterfaceWithPortSuccessfully sets up the test server to respond to an AttachVirtualInterface request with port
+func HandleAttachVirtualInterfaceWithPortSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/vifs",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "POST")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Content-Type", "application/json")
+			th.TestHeader(t, r, "Accept", "application/json")
+			th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","port_uuid":"b2f96298-5172-45e9-b174-8d1ba936ab47"}`)
+
+			w.WriteHeader(http.StatusNoContent)
+		})
+}
+
+// HandleAttachVirtualInterfaceWithPortgroupSuccessfully sets up the test server to respond to an AttachVirtualInterface request with portgroup
+func HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/vifs",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "POST")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Content-Type", "application/json")
+			th.TestHeader(t, r, "Accept", "application/json")
+			th.TestJSONRequest(t, r, `{"id":"1974dcfa-836f-41b2-b541-686c100900e5","portgroup_uuid":"c24944b5-a52e-4c5c-9c0a-52a0235a08a2"}`)
+
+			w.WriteHeader(http.StatusNoContent)
+		})
+}
+
+// HandleDetachVirtualInterfaceSuccessfully sets up the test server to respond to a DetachVirtualInterface request
+func HandleDetachVirtualInterfaceSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/nodes/1234asdf/vifs/1974dcfa-836f-41b2-b541-686c100900e5",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "DELETE")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Accept", "application/json")
+
+			w.WriteHeader(http.StatusNoContent)
+		})
+}
diff --git a/openstack/baremetal/v1/nodes/testing/requests_test.go b/openstack/baremetal/v1/nodes/testing/requests_test.go
index 5529e795a..487c35017 100644
--- a/openstack/baremetal/v1/nodes/testing/requests_test.go
+++ b/openstack/baremetal/v1/nodes/testing/requests_test.go
@@ -829,3 +829,83 @@ func TestVirtualMediaDetachWithTypes(t *testing.T) {
 	err := nodes.DetachVirtualMedia(context.TODO(), c, "1234asdf", opts).ExtractErr()
 	th.AssertNoErr(t, err)
 }
+
+func TestListVirtualInterfaces(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleListVirtualInterfacesSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := nodes.ListVirtualInterfaces(context.TODO(), c, "1234asdf").Extract()
+	th.AssertNoErr(t, err)
+
+	expected := []nodes.VIF{
+		{
+			ID: "1974dcfa-836f-41b2-b541-686c100900e5",
+		},
+	}
+
+	th.CheckDeepEquals(t, expected, actual)
+}
+
+func TestAttachVirtualInterface(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAttachVirtualInterfaceSuccessfully(t)
+
+	c := client.ServiceClient()
+	opts := nodes.VirtualInterfaceOpts{
+		ID: "1974dcfa-836f-41b2-b541-686c100900e5",
+	}
+	err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAttachVirtualInterfaceWithPort(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAttachVirtualInterfaceWithPortSuccessfully(t)
+
+	c := client.ServiceClient()
+	opts := nodes.VirtualInterfaceOpts{
+		ID:       "1974dcfa-836f-41b2-b541-686c100900e5",
+		PortUUID: "b2f96298-5172-45e9-b174-8d1ba936ab47",
+	}
+	err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestAttachVirtualInterfaceWithPortgroup(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleAttachVirtualInterfaceWithPortgroupSuccessfully(t)
+
+	c := client.ServiceClient()
+	opts := nodes.VirtualInterfaceOpts{
+		ID:            "1974dcfa-836f-41b2-b541-686c100900e5",
+		PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2",
+	}
+	err := nodes.AttachVirtualInterface(context.TODO(), c, "1234asdf", opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestDetachVirtualInterface(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleDetachVirtualInterfaceSuccessfully(t)
+
+	c := client.ServiceClient()
+	err := nodes.DetachVirtualInterface(context.TODO(), c, "1234asdf", "1974dcfa-836f-41b2-b541-686c100900e5").ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestVirtualInterfaceOptsValidation(t *testing.T) {
+	opts := nodes.VirtualInterfaceOpts{
+		ID:            "1974dcfa-836f-41b2-b541-686c100900e5",
+		PortUUID:      "b2f96298-5172-45e9-b174-8d1ba936ab47",
+		PortgroupUUID: "c24944b5-a52e-4c5c-9c0a-52a0235a08a2",
+	}
+
+	_, err := opts.ToVirtualInterfaceMap()
+	th.AssertEquals(t, err.Error(), "cannot specify both port_uuid and portgroup_uuid")
+}
diff --git a/openstack/baremetal/v1/portgroups/testing/fixtures.go b/openstack/baremetal/v1/portgroups/testing/fixtures.go
new file mode 100644
index 000000000..9a8773fd8
--- /dev/null
+++ b/openstack/baremetal/v1/portgroups/testing/fixtures.go
@@ -0,0 +1,271 @@
+package testing
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+	"github.com/gophercloud/gophercloud/v2/testhelper/client"
+)
+
+// PortGroupsListBody is the JSON response for listing all portgroups.
+var PortGroupsListBody = `
+{
+  "portgroups": [
+    {
+      "address": "00:1a:2b:3c:4d:5e",
+      "created_at": "2019-02-20T09:43:58Z",
+      "extra": {
+        "description": "Primary network bond",
+        "location": "rack-3-unit-12"
+      },
+      "internal_info": {
+        "fault_count": 0,
+        "last_check": "2024-03-15T10:30:00Z"
+      },
+      "links": [
+        {
+          "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+          "rel": "bookmark"
+        }
+      ],
+      "mode": "active-backup",
+      "name": "bond0",
+      "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8",
+      "ports": [
+        {
+          "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports",
+          "rel": "self"
+        }
+      ],
+      "properties": {
+        "miimon": "100",
+        "updelay": "1000",
+        "downdelay": "1000",
+        "xmit_hash_policy": "layer2"
+      },
+      "standalone_ports_supported": true,
+      "updated_at": "2019-02-20T09:43:58Z",
+      "uuid": "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796"
+    },
+    {
+      "address": "11:22:33:44:55:66",
+      "created_at": "2019-02-20T09:43:58Z",
+      "extra": {
+        "description": "Secondary bond",
+        "location": "rack-1-unit-4"
+      },
+      "internal_info": {
+        "fault_count": 1,
+        "last_check": "2024-04-01T09:00:00Z"
+      },
+      "links": [
+        {
+          "href": "http://ironic.example.com/v1/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0",
+          "rel": "self"
+        },
+        {
+          "href": "http://ironic.example.com/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0",
+          "rel": "bookmark"
+        }
+      ],
+      "mode": "active-backup",
+      "name": "bond1",
+      "node_uuid": "aabbcc00-1122-3344-5566-778899aabbcc",
+      "ports": [
+        {
+          "href": "http://ironic.example.com/v1/portgroups/a1b2c3d4-e5f6-7890-1234-56789abcdef0/ports",
+          "rel": "self"
+        }
+      ],
+      "properties": {
+        "miimon": "200",
+        "updelay": "500",
+        "downdelay": "500",
+        "xmit_hash_policy": "layer3+4"
+      },
+      "standalone_ports_supported": true,
+      "updated_at": "2019-02-20T09:43:58Z",
+      "uuid": "a1b2c3d4-e5f6-7890-1234-56789abcdef0"
+    }
+  ]
+}
+`
+
+// SinglePortGroupBody returns JSON for a single portgroup.
+// Here we use PortGroup1 as the example.
+var SinglePortGroupBody = `
+{
+  "address": "00:1a:2b:3c:4d:5e",
+  "created_at": "2019-02-20T09:43:58Z",
+  "extra": {
+    "description": "Primary network bond",
+    "location": "rack-3-unit-12"
+  },
+  "internal_info": {
+    "fault_count": 0,
+    "last_check": "2024-03-15T10:30:00Z"
+  },
+  "links": [
+    {
+      "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+      "rel": "self"
+    },
+    {
+      "href": "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+      "rel": "bookmark"
+    }
+  ],
+  "mode": "active-backup",
+  "name": "bond0",
+  "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8",
+  "ports": [
+    {
+      "href": "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports",
+      "rel": "self"
+    }
+  ],
+  "properties": {
+    "miimon": "100",
+    "updelay": "1000",
+    "downdelay": "1000",
+    "xmit_hash_policy": "layer2"
+  },
+  "standalone_ports_supported": true,
+  "updated_at": "2019-02-20T09:43:58Z",
+  "uuid": "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796"
+}
+`
+
+var (
+	createdAt, _ = time.Parse(time.RFC3339, "2019-02-20T09:43:58Z")
+
+	// PortGroup1 is the first portgroup.
+	PortGroup1 = portgroups.PortGroup{
+		Name:                     "bond0",
+		UUID:                     "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+		Address:                  "00:1a:2b:3c:4d:5e",
+		NodeUUID:                 "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8",
+		StandalonePortsSupported: true,
+		InternalInfo: map[string]any{
+			"fault_count": float64(0),
+			"last_check":  "2024-03-15T10:30:00Z",
+		},
+		Extra: map[string]any{
+			"description": "Primary network bond",
+			"location":    "rack-3-unit-12",
+		},
+		Mode: "active-backup",
+		Properties: map[string]any{
+			"miimon":           "100",
+			"updelay":          "1000",
+			"downdelay":        "1000",
+			"xmit_hash_policy": "layer2",
+		},
+		CreatedAt: createdAt,
+		UpdatedAt: createdAt,
+		Links: []portgroups.ResourceLink{
+			{
+				Href: "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+				Rel:  "self",
+			},
+			{
+				Href: "http://ironic.example.com/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+				Rel:  "bookmark",
+			},
+		},
+		Ports: []portgroups.ResourceLink{
+			{
+				Href: "http://ironic.example.com/v1/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796/ports",
+				Rel:  "self",
+			},
+		},
+	}
+)
+
+// HandlePortGroupListSuccessfully sets up the test server to respond to a
+// portgroup List request.
+func HandlePortGroupListSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/portgroups", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		w.Header().Add("Content-Type", "application/json")
+		if err := r.ParseForm(); err != nil {
+			t.Errorf("Failed to parse request form: %v", err)
+		}
+
+		marker := r.Form.Get("marker")
+		switch marker {
+		case "":
+			// Return both portgroups.
+			fmt.Fprintf(w, PortGroupsListBody)
+		case "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796":
+			// No portgroups remain.
+			fmt.Fprintf(w, `{ "portgroups": [] }`)
+		default:
+			t.Fatalf("/portgroups invoked with unexpected marker=[%s]", marker)
+		}
+	})
+}
+
+// HandlePortGroupCreationSuccessfully sets up the test server to respond to a PortGroup creation request
+// with a given response.
+func HandlePortGroupCreationSuccessfully(t *testing.T, response string) {
+	th.Mux.HandleFunc("/portgroups", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+      "node_uuid": "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8",
+      "address": "00:1a:2b:3c:4d:5e",
+      "name": "bond0",
+      "mode": "active-backup",
+      "standalone_ports_supported": true,
+      "properties": {
+          "miimon": "100",
+          "updelay": "1000",
+          "downdelay": "1000",
+          "xmit_hash_policy": "layer2"
+      },
+      "extra": {
+          "description": "Primary network bond",
+          "location": "rack-3-unit-12"
+      }
+  }`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, response)
+	})
+}
+
+// HandlePortGroupDeletionSuccessfully sets up the test server to respond to a
+// portgroup Deletion (DELETE) request for PortGroup2.
+func HandlePortGroupDeletionSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "DELETE")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			w.WriteHeader(http.StatusNoContent)
+		})
+}
+
+// HandlePortGroupGetSuccessfully sets up the test server to respond to a
+// portgroup Get request for PortGroup1.
+func HandlePortGroupGetSuccessfully(t *testing.T) {
+	th.Mux.HandleFunc("/portgroups/d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796",
+		func(w http.ResponseWriter, r *http.Request) {
+			th.TestMethod(t, r, "GET")
+			th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+			th.TestHeader(t, r, "Accept", "application/json")
+
+			w.Header().Add("Content-Type", "application/json")
+			fmt.Fprintf(w, SinglePortGroupBody)
+		})
+}
diff --git a/openstack/baremetal/v1/portgroups/testing/requests_test.go b/openstack/baremetal/v1/portgroups/testing/requests_test.go
new file mode 100644
index 000000000..7a818060d
--- /dev/null
+++ b/openstack/baremetal/v1/portgroups/testing/requests_test.go
@@ -0,0 +1,94 @@
+package testing
+
+import (
+	"context"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/portgroups"
+	"github.com/gophercloud/gophercloud/v2/pagination"
+	"github.com/gophercloud/gophercloud/v2/testhelper/client"
+
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+)
+
+func TestListPortGroups(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortGroupListSuccessfully(t)
+
+	pages := 0
+	err := portgroups.List(client.ServiceClient(), portgroups.ListOpts{}).EachPage(context.TODO(), func(_ context.Context, page pagination.Page) (bool, error) {
+		pages++
+
+		actual, err := portgroups.ExtractPortGroups(page)
+		if err != nil {
+			return false, err
+		}
+
+		if len(actual) != 2 {
+			t.Fatalf("Expected 2 portgroups, got %d", len(actual))
+		}
+		th.AssertEquals(t, "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796", actual[0].UUID)
+		th.AssertEquals(t, "a1b2c3d4-e5f6-7890-1234-56789abcdef0", actual[1].UUID)
+		th.AssertEquals(t, "bond0", actual[0].Name)
+		th.AssertEquals(t, "bond1", actual[1].Name)
+
+		return true, nil
+	})
+
+	th.AssertNoErr(t, err)
+
+	if pages != 1 {
+		t.Errorf("Expected 1 page, saw %d", pages)
+	}
+}
+
+func TestCreatePortGroup(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortGroupCreationSuccessfully(t, SinglePortGroupBody)
+
+	actual, err := portgroups.Create(context.TODO(), client.ServiceClient(), portgroups.CreateOpts{
+		Name:                     "bond0",
+		NodeUUID:                 "f9c9a846-c53f-4b17-9f0c-dd9f459d35c8",
+		Address:                  "00:1a:2b:3c:4d:5e",
+		Mode:                     "active-backup",
+		StandalonePortsSupported: true,
+		Properties: map[string]interface{}{
+			"miimon":           "100",
+			"updelay":          "1000",
+			"downdelay":        "1000",
+			"xmit_hash_policy": "layer2",
+		},
+		Extra: map[string]string{
+			"description": "Primary network bond",
+			"location":    "rack-3-unit-12",
+		},
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, PortGroup1, *actual)
+}
+
+func TestDeletePortGroup(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortGroupDeletionSuccessfully(t)
+
+	res := portgroups.Delete(context.TODO(), client.ServiceClient(), "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796")
+	th.AssertNoErr(t, res.Err)
+}
+
+func TestGetPortGroup(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandlePortGroupGetSuccessfully(t)
+
+	c := client.ServiceClient()
+	actual, err := portgroups.Get(context.TODO(), c, "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected Get error: %v", err)
+	}
+
+	th.CheckDeepEquals(t, PortGroup1, *actual)
+}
diff --git a/openstack/blockstorage/v2/quotasets/testing/fixtures_test.go b/openstack/blockstorage/v2/quotasets/testing/fixtures_test.go
index 854ff08b6..1e26bec78 100644
--- a/openstack/blockstorage/v2/quotasets/testing/fixtures_test.go
+++ b/openstack/blockstorage/v2/quotasets/testing/fixtures_test.go
@@ -75,8 +75,23 @@ var getUsageExpectedJSONBody = `
             "in_use": 40,
             "limit": 41,
             "reserved": 42
+        },
+        "gigabytes_hdd" : {
+            "in_use": 50,
+            "limit": 51,
+            "reserved": 52
+        },
+        "volumes_hdd" : {
+            "in_use": 53,
+            "limit": 54,
+            "reserved": 55
+        },
+        "snapshots_hdd": {
+            "in_use": 56,
+            "limit": 57,
+            "reserved": 58
         }
-	}
+    }
 }`
 
 var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
@@ -88,6 +103,11 @@ var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
 	Backups:            quotasets.QuotaUsage{InUse: 27, Limit: 28, Reserved: 29},
 	BackupGigabytes:    quotasets.QuotaUsage{InUse: 30, Limit: 31, Reserved: 32},
 	Groups:             quotasets.QuotaUsage{InUse: 40, Limit: 41, Reserved: 42},
+	Extra: map[string]quotasets.QuotaUsage{
+		"gigabytes_hdd": {InUse: 50, Limit: 51, Reserved: 52},
+		"volumes_hdd":   {InUse: 53, Limit: 54, Reserved: 55},
+		"snapshots_hdd": {InUse: 56, Limit: 57, Reserved: 58},
+	},
 }
 
 var fullUpdateExpectedJSONBody = `
@@ -147,7 +167,7 @@ var partialUpdateOpts = quotasets.UpdateOpts{
 	Extra:              make(map[string]any),
 }
 
-var partiualUpdateExpectedQuotaSet = quotasets.QuotaSet{
+var partialUpdateExpectedQuotaSet = quotasets.QuotaSet{
 	Volumes: 200,
 	Extra:   make(map[string]any),
 }
diff --git a/openstack/blockstorage/v2/quotasets/testing/requests_test.go b/openstack/blockstorage/v2/quotasets/testing/requests_test.go
index 563b2d1d3..025d824f2 100644
--- a/openstack/blockstorage/v2/quotasets/testing/requests_test.go
+++ b/openstack/blockstorage/v2/quotasets/testing/requests_test.go
@@ -51,7 +51,7 @@ func TestPartialUpdate(t *testing.T) {
 	HandleSuccessfulRequest(t, "PUT", "/os-quota-sets/"+FirstTenantID, partialUpdateExpectedJSONBody, uriQueryParms)
 	actual, err := quotasets.Update(context.TODO(), client.ServiceClient(), FirstTenantID, partialUpdateOpts).Extract()
 	th.AssertNoErr(t, err)
-	th.CheckDeepEquals(t, &partiualUpdateExpectedQuotaSet, actual)
+	th.CheckDeepEquals(t, &partialUpdateExpectedQuotaSet, actual)
 }
 
 type ErrorUpdateOpts quotasets.UpdateOpts
diff --git a/openstack/blockstorage/v3/quotasets/testing/fixtures_test.go b/openstack/blockstorage/v3/quotasets/testing/fixtures_test.go
index 103afef6e..d78b7d012 100644
--- a/openstack/blockstorage/v3/quotasets/testing/fixtures_test.go
+++ b/openstack/blockstorage/v3/quotasets/testing/fixtures_test.go
@@ -75,8 +75,23 @@ var getUsageExpectedJSONBody = `
             "in_use": 40,
             "limit": 41,
             "reserved": 42
+        },
+        "gigabytes_hdd" : {
+            "in_use": 50,
+            "limit": 51,
+            "reserved": 52
+        },
+        "volumes_hdd" : {
+            "in_use": 53,
+            "limit": 54,
+            "reserved": 55
+        },
+        "snapshots_hdd": {
+            "in_use": 56,
+            "limit": 57,
+            "reserved": 58
         }
-	}
+    }
 }`
 
 var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
@@ -88,6 +103,11 @@ var getUsageExpectedQuotaSet = quotasets.QuotaUsageSet{
 	Backups:            quotasets.QuotaUsage{InUse: 27, Limit: 28, Reserved: 29},
 	BackupGigabytes:    quotasets.QuotaUsage{InUse: 30, Limit: 31, Reserved: 32},
 	Groups:             quotasets.QuotaUsage{InUse: 40, Limit: 41, Reserved: 42},
+	Extra: map[string]quotasets.QuotaUsage{
+		"gigabytes_hdd": {InUse: 50, Limit: 51, Reserved: 52},
+		"volumes_hdd":   {InUse: 53, Limit: 54, Reserved: 55},
+		"snapshots_hdd": {InUse: 56, Limit: 57, Reserved: 58},
+	},
 }
 
 var fullUpdateExpectedJSONBody = `
@@ -147,7 +167,7 @@ var partialUpdateOpts = quotasets.UpdateOpts{
 	Extra:              make(map[string]any),
 }
 
-var partiualUpdateExpectedQuotaSet = quotasets.QuotaSet{
+var partialUpdateExpectedQuotaSet = quotasets.QuotaSet{
 	Volumes: 200,
 	Extra:   make(map[string]any),
 }
diff --git a/openstack/blockstorage/v3/quotasets/testing/requests_test.go b/openstack/blockstorage/v3/quotasets/testing/requests_test.go
index ca17ef10b..ba2019fa5 100644
--- a/openstack/blockstorage/v3/quotasets/testing/requests_test.go
+++ b/openstack/blockstorage/v3/quotasets/testing/requests_test.go
@@ -51,7 +51,7 @@ func TestPartialUpdate(t *testing.T) {
 	HandleSuccessfulRequest(t, "PUT", "/os-quota-sets/"+FirstTenantID, partialUpdateExpectedJSONBody, uriQueryParms)
 	actual, err := quotasets.Update(context.TODO(), client.ServiceClient(), FirstTenantID, partialUpdateOpts).Extract()
 	th.AssertNoErr(t, err)
-	th.CheckDeepEquals(t, &partiualUpdateExpectedQuotaSet, actual)
+	th.CheckDeepEquals(t, &partialUpdateExpectedQuotaSet, actual)
 }
 
 type ErrorUpdateOpts quotasets.UpdateOpts
diff --git a/openstack/compute/v2/aggregates/testing/requests_test.go b/openstack/compute/v2/aggregates/testing/requests_test.go
index 2e229bf71..601150a58 100644
--- a/openstack/compute/v2/aggregates/testing/requests_test.go
+++ b/openstack/compute/v2/aggregates/testing/requests_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/aggregates"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -86,10 +87,9 @@ func TestUpdateAggregate(t *testing.T) {
 	HandleUpdateSuccessfully(t)
 
 	expected := UpdatedAggregate
-
 	opts := aggregates.UpdateOpts{
-		Name:             "test-aggregates2",
-		AvailabilityZone: "nova2",
+		Name:             ptr.To("test-aggregates2"),
+		AvailabilityZone: ptr.To("nova2"),
 	}
 
 	actual, err := aggregates.Update(context.TODO(), client.ServiceClient(), expected.ID, opts).Extract()
diff --git a/openstack/compute/v2/flavors/testing/requests_test.go b/openstack/compute/v2/flavors/testing/requests_test.go
index efd6e4bce..315fcd340 100644
--- a/openstack/compute/v2/flavors/testing/requests_test.go
+++ b/openstack/compute/v2/flavors/testing/requests_test.go
@@ -7,6 +7,7 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -242,7 +243,7 @@ func TestUpdateFlavor(t *testing.T) {
 	})
 
 	opts := &flavors.UpdateOpts{
-		Description: "foo",
+		Description: ptr.To("foo"),
 	}
 	actual, err := flavors.Update(context.TODO(), fake.ServiceClient(), "12345678", opts).Extract()
 	if err != nil {
diff --git a/openstack/compute/v2/hypervisors/testing/fixtures_test.go b/openstack/compute/v2/hypervisors/testing/fixtures_test.go
index fa0b93a3f..4bdb2c061 100644
--- a/openstack/compute/v2/hypervisors/testing/fixtures_test.go
+++ b/openstack/compute/v2/hypervisors/testing/fixtures_test.go
@@ -6,6 +6,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/hypervisors"
+	"github.com/gophercloud/gophercloud/v2/testhelper"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
 	"github.com/gophercloud/gophercloud/v2/testhelper/client"
 )
@@ -85,8 +86,8 @@ const HypervisorListBodyPre253 = `
     ]
 }`
 
-// HypervisorListBody represents a raw hypervisor list result with Pike+ release.
-const HypervisorListBody = `
+// HypervisorListBodyPage1 represents page 1 of a raw hypervisor list result with Pike+ release.
+const HypervisorListBodyPage1 = `
 {
     "hypervisors": [
         {
@@ -127,7 +128,20 @@ const HypervisorListBody = `
             },
             "vcpus": 1,
             "vcpus_used": 0
-        },
+        }
+    ],
+    "hypervisors_links": [
+        {
+            "href": "%s/os-hypervisors/detail?marker=c48f6247-abe4-4a24-824e-ea39e108874f",
+            "rel": "next"
+        }
+    ]
+}`
+
+// HypervisorListBodyPage2 represents page 2 of a raw hypervisor list result with Pike+ release.
+const HypervisorListBodyPage2 = `
+{
+    "hypervisors": [
         {
             "cpu_info": "{\"arch\": \"x86_64\", \"model\": \"Nehalem\", \"vendor\": \"Intel\", \"features\": [\"pge\", \"clflush\"], \"topology\": {\"cores\": 1, \"threads\": 1, \"sockets\": 4}}",
             "current_workload": 0,
@@ -157,6 +171,9 @@ const HypervisorListBody = `
     ]
 }`
 
+// HypervisorListBodyEmpty represents an empty raw hypervisor list result, marking the end of pagination.
+const HypervisorListBodyEmpty = `{ "hypervisors": [] }`
+
 // HypervisorListWithParametersBody represents a raw hypervisor list result with Pike+ release.
 const HypervisorListWithParametersBody = `
 {
@@ -624,8 +641,16 @@ func HandleHypervisorListSuccessfully(t *testing.T) {
 		th.TestMethod(t, r, "GET")
 		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
 
-		w.Header().Add("Content-Type", "application/json")
-		fmt.Fprint(w, HypervisorListBody)
+		switch r.URL.Query().Get("marker") {
+		case "":
+			w.Header().Add("Content-Type", "application/json")
+			fmt.Fprintf(w, HypervisorListBodyPage1, testhelper.Server.URL)
+		case "c48f6247-abe4-4a24-824e-ea39e108874f":
+			w.Header().Add("Content-Type", "application/json")
+			fmt.Fprint(w, HypervisorListBodyPage2)
+		default:
+			http.Error(w, "unexpected marker value", http.StatusInternalServerError)
+		}
 	})
 }
 
diff --git a/openstack/compute/v2/hypervisors/testing/requests_test.go b/openstack/compute/v2/hypervisors/testing/requests_test.go
index 0303f7f1d..5acf7d0d1 100644
--- a/openstack/compute/v2/hypervisors/testing/requests_test.go
+++ b/openstack/compute/v2/hypervisors/testing/requests_test.go
@@ -69,19 +69,18 @@ func TestListHypervisors(t *testing.T) {
 			return false, err
 		}
 
-		if len(actual) != 2 {
-			t.Fatalf("Expected 2 hypervisors, got %d", len(actual))
+		if len(actual) != 1 {
+			t.Fatalf("Expected 1 hypervisors on page %d, got %d", pages, len(actual))
 		}
 		th.CheckDeepEquals(t, HypervisorFake, actual[0])
-		th.CheckDeepEquals(t, HypervisorFake, actual[1])
 
 		return true, nil
 	})
 
 	th.AssertNoErr(t, err)
 
-	if pages != 1 {
-		t.Errorf("Expected 1 page, saw %d", pages)
+	if pages != 2 {
+		t.Errorf("Expected 2 pages, saw %d", pages)
 	}
 }
 
diff --git a/openstack/compute/v2/secgroups/testing/requests_test.go b/openstack/compute/v2/secgroups/testing/requests_test.go
index 222295d5e..53c473ce0 100644
--- a/openstack/compute/v2/secgroups/testing/requests_test.go
+++ b/openstack/compute/v2/secgroups/testing/requests_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/secgroups"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -116,10 +117,9 @@ func TestUpdate(t *testing.T) {
 
 	mockUpdateGroupResponse(t, groupID)
 
-	description := "new_desc"
 	opts := secgroups.UpdateOpts{
-		Name:        "new_name",
-		Description: &description,
+		Name:        ptr.To("new_name"),
+		Description: ptr.To("new_desc"),
 	}
 
 	group, err := secgroups.Update(context.TODO(), client.ServiceClient(), groupID, opts).Extract()
diff --git a/openstack/compute/v2/servers/testing/fixtures_test.go b/openstack/compute/v2/servers/testing/fixtures_test.go
index 1c25b4007..a9c3980d8 100644
--- a/openstack/compute/v2/servers/testing/fixtures_test.go
+++ b/openstack/compute/v2/servers/testing/fixtures_test.go
@@ -158,7 +158,8 @@ const ServerListBody = `
 			"progress": 0,
 			"OS-EXT-STS:power_state": 1,
 			"config_drive": "",
-			"metadata": {}
+			"metadata": {},
+			"locked": true
 		},
 		{
 		"status": "ACTIVE",
@@ -297,7 +298,8 @@ const SingleServerBody = `
 		"progress": 0,
 		"OS-EXT-STS:power_state": 1,
 		"config_drive": "",
-		"metadata": {}
+		"metadata": {},
+		"locked": true
 	}
 }
 `
@@ -631,6 +633,7 @@ var (
 		TerminatedAt:       time.Time{},
 		DiskConfig:         servers.Manual,
 		AvailabilityZone:   "nova",
+		Locked:             func() *bool { b := true; return &b }(),
 	}
 
 	ConsoleOutput = "abc"
diff --git a/openstack/compute/v2/servers/testing/requests_test.go b/openstack/compute/v2/servers/testing/requests_test.go
index b6ebefce5..831ddb2ec 100644
--- a/openstack/compute/v2/servers/testing/requests_test.go
+++ b/openstack/compute/v2/servers/testing/requests_test.go
@@ -7,6 +7,7 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -774,6 +775,7 @@ func TestGetFaultyServer(t *testing.T) {
 
 	FaultyServer := ServerDerp
 	FaultyServer.Fault = DerpFault
+	FaultyServer.Locked = nil
 	th.CheckDeepEquals(t, FaultyServer, *actual)
 }
 
@@ -807,7 +809,7 @@ func TestUpdateServer(t *testing.T) {
 	HandleServerUpdateSuccessfully(t)
 
 	client := client.ServiceClient()
-	actual, err := servers.Update(context.TODO(), client, "1234asdf", servers.UpdateOpts{Name: "new-name"}).Extract()
+	actual, err := servers.Update(context.TODO(), client, "1234asdf", servers.UpdateOpts{Name: ptr.To("new-name")}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
 	}
@@ -1145,6 +1147,7 @@ func TestCreateServerWithTags(t *testing.T) {
 	tags := []string{"foo", "bar"}
 	ServerDerpTags := ServerDerp
 	ServerDerpTags.Tags = &tags
+	ServerDerpTags.Locked = nil
 
 	createOpts := servers.CreateOpts{
 		Name:      "derp",
@@ -1158,3 +1161,25 @@ func TestCreateServerWithTags(t *testing.T) {
 	th.AssertNoErr(t, err)
 	th.CheckDeepEquals(t, ServerDerpTags, *actualServer)
 }
+
+func TestCreateServerWithHypervisorHostname(t *testing.T) {
+	opts := servers.CreateOpts{
+		Name:               "createdserver",
+		FlavorRef:          "performance1-1",
+		ImageRef:           "asdfasdfasdf",
+		HypervisorHostname: "test-hypervisor",
+	}
+	expected := `
+    {
+        "server": {
+            "name":"createdserver",
+            "flavorRef":"performance1-1",
+            "imageRef":"asdfasdfasdf",
+            "hypervisor_hostname":"test-hypervisor"
+        }
+    }
+    `
+	actual, err := opts.ToServerCreateMap()
+	th.AssertNoErr(t, err)
+	th.CheckJSONEquals(t, expected, actual)
+}
diff --git a/openstack/db/v1/quotas/testing/doc.go b/openstack/db/v1/quotas/testing/doc.go
new file mode 100644
index 000000000..e8785db23
--- /dev/null
+++ b/openstack/db/v1/quotas/testing/doc.go
@@ -0,0 +1,2 @@
+// db_quotas_v1
+package testing
diff --git a/openstack/db/v1/quotas/testing/fixtures_test.go b/openstack/db/v1/quotas/testing/fixtures_test.go
new file mode 100644
index 000000000..6e19a51b4
--- /dev/null
+++ b/openstack/db/v1/quotas/testing/fixtures_test.go
@@ -0,0 +1,42 @@
+package testing
+
+import (
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/testhelper/fixture"
+)
+
+var (
+	projectID = "{projectID}"
+	resURL    = "/mgmt/" + "quotas/" + projectID
+)
+
+// getQuotasResp is a sample response to a Get call.
+var getQuotasResp = `
+{
+    "quotas": [
+        {
+            "in_use": 5,
+            "limit": 15,
+            "reserved": 0,
+            "resource": "instances"
+        },
+        {
+            "in_use": 2,
+            "limit": 50,
+            "reserved": 0,
+            "resource": "backups"
+        },
+        {
+            "in_use": 1,
+            "limit": 40,
+            "reserved": 0,
+            "resource": "volumes"
+        }
+    ]
+}
+`
+
+func HandleGet(t *testing.T) {
+	fixture.SetupHandler(t, resURL, "GET", "", getQuotasResp, 200)
+}
diff --git a/openstack/db/v1/quotas/testing/requests_test.go b/openstack/db/v1/quotas/testing/requests_test.go
new file mode 100644
index 000000000..31009b5d9
--- /dev/null
+++ b/openstack/db/v1/quotas/testing/requests_test.go
@@ -0,0 +1,26 @@
+package testing
+
+import (
+	"context"
+	"testing"
+
+	"github.com/gophercloud/gophercloud/v2/openstack/db/v1/quotas"
+	th "github.com/gophercloud/gophercloud/v2/testhelper"
+	"github.com/gophercloud/gophercloud/v2/testhelper/client"
+)
+
+func TestGet(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleGet(t)
+
+	expectedQuotas := []quotas.QuotaDetail{
+		{Resource: "instances", Limit: 15, InUse: 5, Reserved: 0},
+		{Resource: "backups", Limit: 50, InUse: 2, Reserved: 0},
+		{Resource: "volumes", Limit: 40, InUse: 1, Reserved: 0},
+	}
+
+	actual, err := quotas.Get(context.TODO(), client.ServiceClient(), "e131f89a-c1d8-11ef-bfaa-370c246e2439").Extract()
+	th.AssertNoErr(t, err)
+	th.CheckDeepEquals(t, expectedQuotas, actual)
+}
diff --git a/openstack/dns/v2/zones/testing/requests_test.go b/openstack/dns/v2/zones/testing/requests_test.go
index 0b64e81ec..b75b99e96 100644
--- a/openstack/dns/v2/zones/testing/requests_test.go
+++ b/openstack/dns/v2/zones/testing/requests_test.go
@@ -2,6 +2,9 @@ package testing
 
 import (
 	"context"
+	"encoding/json"
+	"io"
+	"net/http"
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/openstack/dns/v2/zones"
@@ -105,3 +108,41 @@ func TestDelete(t *testing.T) {
 	th.AssertNoErr(t, err)
 	th.CheckDeepEquals(t, &DeletedZone, actual)
 }
+
+func TestShare(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/zones/zone-id/shares", func(w http.ResponseWriter, r *http.Request) {
+		th.AssertEquals(t, r.Method, "POST")
+
+		body, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		th.AssertNoErr(t, err)
+
+		var reqBody map[string]string
+		err = json.Unmarshal(body, &reqBody)
+		th.AssertNoErr(t, err)
+		expectedBody := map[string]string{"target_project_id": "project-id"}
+		th.CheckDeepEquals(t, expectedBody, reqBody)
+
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	opts := zones.ShareZoneOpts{TargetProjectID: "project-id"}
+	err := zones.Share(context.TODO(), client.ServiceClient(), "zone-id", opts).ExtractErr()
+	th.AssertNoErr(t, err)
+}
+
+func TestUnshare(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/zones/zone-id/shares/share-id", func(w http.ResponseWriter, r *http.Request) {
+		th.AssertEquals(t, r.Method, "DELETE")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	err := zones.Unshare(context.TODO(), client.ServiceClient(), "zone-id", "share-id").ExtractErr()
+	th.AssertNoErr(t, err)
+}
diff --git a/openstack/identity/v3/endpoints/testing/requests_test.go b/openstack/identity/v3/endpoints/testing/requests_test.go
index 348a1f1c6..ad138acd4 100644
--- a/openstack/identity/v3/endpoints/testing/requests_test.go
+++ b/openstack/identity/v3/endpoints/testing/requests_test.go
@@ -20,35 +20,31 @@ func TestCreateSuccessful(t *testing.T) {
 	th.Mux.HandleFunc("/endpoints", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "POST")
 		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
-		th.TestJSONRequest(t, r, `
-      {
-        "endpoint": {
-          "interface": "public",
-          "name": "the-endiest-of-points",
-          "region": "underground",
-          "url": "https://1.2.3.4:9000/",
-          "service_id": "asdfasdfasdfasdf"
-        }
-      }
-    `)
+		th.TestJSONRequest(t, r, `{
+			"endpoint": {
+				"interface": "public",
+				"name": "the-endiest-of-points",
+				"region": "underground",
+				"url": "https://1.2.3.4:9000/",
+				"service_id": "asdfasdfasdfasdf"
+			}
+		}`)
 
 		w.WriteHeader(http.StatusCreated)
-		fmt.Fprint(w, `
-      {
-        "endpoint": {
-          "id": "12",
-          "interface": "public",
-		  "enabled": true,
-		  "links": {
-            "self": "https://localhost:5000/v3/endpoints/12"
-          },
-          "name": "the-endiest-of-points",
-          "region": "underground",
-          "service_id": "asdfasdfasdfasdf",
-          "url": "https://1.2.3.4:9000/"
-        }
-      }
-    `)
+		fmt.Fprint(w, `{
+			"endpoint": {
+				"id": "12",
+				"interface": "public",
+				"enabled": true,
+				"links": {
+					"self": "https://localhost:5000/v3/endpoints/12"
+				},
+				"name": "the-endiest-of-points",
+				"region": "underground",
+				"service_id": "asdfasdfasdfasdf",
+				"url": "https://1.2.3.4:9000/"
+			}
+		}`)
 	})
 
 	actual, err := endpoints.Create(context.TODO(), client.ServiceClient(), endpoints.CreateOpts{
@@ -82,40 +78,38 @@ func TestListEndpoints(t *testing.T) {
 		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
 
 		w.Header().Add("Content-Type", "application/json")
-		fmt.Fprint(w, `
-			{
-				"endpoints": [
-					{
-						"id": "12",
-						"interface": "public",
-						"enabled": true,
-						"links": {
-							"self": "https://localhost:5000/v3/endpoints/12"
-						},
-						"name": "the-endiest-of-points",
-						"region": "underground",
-						"service_id": "asdfasdfasdfasdf",
-						"url": "https://1.2.3.4:9000/"
+		fmt.Fprint(w, `{
+			"endpoints": [
+				{
+					"id": "12",
+					"interface": "public",
+					"enabled": true,
+					"links": {
+						"self": "https://localhost:5000/v3/endpoints/12"
 					},
-					{
-						"id": "13",
-						"interface": "internal",
-						"enabled": false,
-						"links": {
-							"self": "https://localhost:5000/v3/endpoints/13"
-						},
-						"name": "shhhh",
-						"region": "underground",
-						"service_id": "asdfasdfasdfasdf",
-						"url": "https://1.2.3.4:9001/"
-					}
-				],
-				"links": {
-					"next": null,
-					"previous": null
+					"name": "the-endiest-of-points",
+					"region": "underground",
+					"service_id": "asdfasdfasdfasdf",
+					"url": "https://1.2.3.4:9000/"
+				},
+				{
+					"id": "13",
+					"interface": "internal",
+					"enabled": false,
+					"links": {
+						"self": "https://localhost:5000/v3/endpoints/13"
+					},
+					"name": "shhhh",
+					"region": "underground",
+					"service_id": "asdfasdfasdfasdf",
+					"url": "https://1.2.3.4:9001/"
 				}
+			],
+			"links": {
+				"next": null,
+				"previous": null
 			}
-		`)
+		}`)
 	})
 
 	count := 0
@@ -154,6 +148,47 @@ func TestListEndpoints(t *testing.T) {
 	th.AssertEquals(t, 1, count)
 }
 
+func TestGetEndpoint(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "GET")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		fmt.Fprint(w, `{
+			"endpoint": {
+				"id": "12",
+				"interface": "public",
+				"enabled": true,
+				"links": {
+					"self": "https://localhost:5000/v3/endpoints/12"
+				},
+				"name": "the-endiest-of-points",
+				"region": "underground",
+				"service_id": "asdfasdfasdfasdf",
+				"url": "https://1.2.3.4:9000/"
+			}
+		}`)
+	})
+
+	actual, err := endpoints.Get(context.TODO(), client.ServiceClient(), "12").Extract()
+	if err != nil {
+		t.Fatalf("Unexpected error from Get: %v", err)
+	}
+
+	expected := &endpoints.Endpoint{
+		ID:           "12",
+		Availability: gophercloud.AvailabilityPublic,
+		Enabled:      true,
+		Name:         "the-endiest-of-points",
+		Region:       "underground",
+		ServiceID:    "asdfasdfasdfasdf",
+		URL:          "https://1.2.3.4:9000/",
+	}
+	th.AssertDeepEquals(t, expected, actual)
+}
+
 func TestUpdateEndpoint(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
@@ -161,17 +196,14 @@ func TestUpdateEndpoint(t *testing.T) {
 	th.Mux.HandleFunc("/endpoints/12", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "PATCH")
 		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
-		th.TestJSONRequest(t, r, `
-		{
-	    "endpoint": {
-	      "name": "renamed",
+		th.TestJSONRequest(t, r, `{
+			"endpoint": {
+				"name": "renamed",
 				"region": "somewhere-else"
-	    }
-		}
-	`)
+			}
+		}`)
 
-		fmt.Fprint(w, `
-		{
+		fmt.Fprint(w, `{
 			"endpoint": {
 				"id": "12",
 				"interface": "public",
@@ -184,8 +216,7 @@ func TestUpdateEndpoint(t *testing.T) {
 				"service_id": "asdfasdfasdfasdf",
 				"url": "https://1.2.3.4:9000/"
 			}
-		}
-	`)
+		}`)
 	})
 
 	actual, err := endpoints.Update(context.TODO(), client.ServiceClient(), "12", endpoints.UpdateOpts{
diff --git a/openstack/loadbalancer/v2/flavorprofiles/testing/requests_test.go b/openstack/loadbalancer/v2/flavorprofiles/testing/requests_test.go
index aebb2c940..5cefc8c97 100644
--- a/openstack/loadbalancer/v2/flavorprofiles/testing/requests_test.go
+++ b/openstack/loadbalancer/v2/flavorprofiles/testing/requests_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavorprofiles"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 
@@ -106,9 +107,9 @@ func TestUpdateFlavorProfile(t *testing.T) {
 
 	client := fake.ServiceClient()
 	actual, err := flavorprofiles.Update(context.TODO(), client, "dcd65be5-f117-4260-ab3d-b32cc5bd1272", flavorprofiles.UpdateOpts{
-		Name:         "amphora-test-updated",
-		ProviderName: "amphora",
-		FlavorData:   "{\"loadbalancer_topology\": \"SINGLE\"}",
+		Name:         ptr.To("amphora-test-updated"),
+		ProviderName: ptr.To("amphora"),
+		FlavorData:   ptr.To(`{"loadbalancer_topology": "SINGLE"}`),
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
diff --git a/openstack/loadbalancer/v2/flavors/testing/fixtures.go b/openstack/loadbalancer/v2/flavors/testing/fixtures.go
index 1ec935c15..b33166dfa 100644
--- a/openstack/loadbalancer/v2/flavors/testing/fixtures.go
+++ b/openstack/loadbalancer/v2/flavors/testing/fixtures.go
@@ -44,6 +44,18 @@ const SingleFlavorBody = `
 }
 `
 
+const SingleFlavorDisabledBody = `
+{
+	"flavor": {
+		"id": "5548c807-e6e8-43d7-9ea4-b38d34dd74a0",
+		"name": "Basic",
+		"description": "A basic standalone Octavia load balancer.",
+		"enabled": false,
+		"flavor_profile_id": "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1"
+	}
+}
+`
+
 const PostUpdateFlavorBody = `
 {
 	"flavor": {
@@ -81,6 +93,14 @@ var (
 		FlavorProfileId: "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1",
 	}
 
+	FlavorDisabled = flavors.Flavor{
+		ID:              "5548c807-e6e8-43d7-9ea4-b38d34dd74a0",
+		Name:            "Basic",
+		Description:     "A basic standalone Octavia load balancer.",
+		Enabled:         false,
+		FlavorProfileId: "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1",
+	}
+
 	FlavorUpdated = flavors.Flavor{
 		ID:              "5548c807-e6e8-43d7-9ea4-b38d34dd74a0",
 		Name:            "Basic v2",
@@ -130,6 +150,25 @@ func HandleFlavorCreationSuccessfully(t *testing.T, response string) {
 	})
 }
 
+func HandleFlavorCreationSuccessfullyDisabled(t *testing.T, response string) {
+	th.Mux.HandleFunc("/v2.0/lbaas/flavors", func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+		th.TestJSONRequest(t, r, `{
+			"flavor": {
+				"name": "Basic",
+				"description": "A basic standalone Octavia load balancer.",
+				"enabled": false,
+				"flavor_profile_id": "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1"
+			}
+		}`)
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprint(w, response)
+	})
+}
+
 func HandleFlavorGetSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/v2.0/lbaas/flavors/5548c807-e6e8-43d7-9ea4-b38d34dd74a0", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "GET")
diff --git a/openstack/loadbalancer/v2/flavors/testing/requests_test.go b/openstack/loadbalancer/v2/flavors/testing/requests_test.go
index bc73b1271..1321716c9 100644
--- a/openstack/loadbalancer/v2/flavors/testing/requests_test.go
+++ b/openstack/loadbalancer/v2/flavors/testing/requests_test.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/gophercloud/gophercloud/v2/internal/ptr"
 	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 
@@ -109,7 +110,7 @@ func TestCreateFlavor(t *testing.T) {
 	actual, err := flavors.Create(context.TODO(), fake.ServiceClient(), flavors.CreateOpts{
 		Name:            "Basic",
 		Description:     "A basic standalone Octavia load balancer.",
-		Enabled:         true,
+		Enabled:         ptr.To(true),
 		FlavorProfileId: "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1",
 	}).Extract()
 	th.AssertNoErr(t, err)
@@ -117,6 +118,22 @@ func TestCreateFlavor(t *testing.T) {
 	th.CheckDeepEquals(t, FlavorDb, *actual)
 }
 
+func TestCreateFlavorDisabled(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+	HandleFlavorCreationSuccessfullyDisabled(t, SingleFlavorDisabledBody)
+
+	actual, err := flavors.Create(context.TODO(), fake.ServiceClient(), flavors.CreateOpts{
+		Name:            "Basic",
+		Description:     "A basic standalone Octavia load balancer.",
+		Enabled:         ptr.To(false),
+		FlavorProfileId: "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1",
+	}).Extract()
+	th.AssertNoErr(t, err)
+
+	th.CheckDeepEquals(t, FlavorDisabled, *actual)
+}
+
 func TestRequiredCreateOpts(t *testing.T) {
 	res := flavors.Create(context.TODO(), fake.ServiceClient(), flavors.CreateOpts{})
 	if res.Err == nil {
@@ -154,9 +171,9 @@ func TestUpdateFlavor(t *testing.T) {
 
 	client := fake.ServiceClient()
 	actual, err := flavors.Update(context.TODO(), client, "5548c807-e6e8-43d7-9ea4-b38d34dd74a0", flavors.UpdateOpts{
-		Name:        "Basic v2",
-		Description: "Rename flavor",
-		Enabled:     true,
+		Name:        ptr.To("Basic v2"),
+		Description: ptr.To("Rename flavor"),
+		Enabled:     ptr.To(true),
 	}).Extract()
 	if err != nil {
 		t.Fatalf("Unexpected Update error: %v", err)
diff --git a/openstack/networking/v2/extensions/security/rules/testing/requests_test.go b/openstack/networking/v2/extensions/security/rules/testing/requests_test.go
index 843d7eb20..ad6552a77 100644
--- a/openstack/networking/v2/extensions/security/rules/testing/requests_test.go
+++ b/openstack/networking/v2/extensions/security/rules/testing/requests_test.go
@@ -222,6 +222,112 @@ func TestCreateAnyProtocol(t *testing.T) {
 	th.AssertNoErr(t, err)
 }
 
+func TestCreateBulk(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	th.Mux.HandleFunc("/v2.0/security-group-rules", 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, `
+{
+    "security_group_rules": [
+        {
+            "description": "test description of rule",
+            "direction": "ingress",
+            "port_range_min": 80,
+            "ethertype": "IPv4",
+            "port_range_max": 80,
+            "protocol": "tcp",
+            "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+            "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+        },
+        {
+            "description": "test description of rule",
+            "direction": "ingress",
+            "port_range_min": 443,
+            "ethertype": "IPv4",
+            "port_range_max": 443,
+            "protocol": "tcp",
+            "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a"
+        }
+    ]
+}
+      `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(http.StatusCreated)
+
+		fmt.Fprint(w, `
+{
+    "security_group_rules": [
+        {
+            "description": "test description of rule",
+            "direction": "ingress",
+            "ethertype": "IPv4",
+            "port_range_max": 80,
+            "port_range_min": 80,
+            "protocol": "tcp",
+            "remote_group_id": "85cc3048-abc3-43cc-89b3-377341426ac5",
+            "remote_ip_prefix": null,
+            "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+            "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+        },
+        {
+            "description": "test description of rule",
+            "direction": "ingress",
+            "ethertype": "IPv4",
+            "port_range_max": 443,
+            "port_range_min": 443,
+            "protocol": "tcp",
+            "remote_group_id": null,
+            "remote_ip_prefix": null,
+            "security_group_id": "a7734e61-b545-452d-a3cd-0189cbd9747a",
+            "tenant_id": "e4f50856753b4dc6afee5fa6b9b6c550"
+        }
+    ]
+}
+    `)
+	})
+
+	opts := []rules.CreateOpts{
+		{
+			Description:   "test description of rule",
+			Direction:     "ingress",
+			PortRangeMin:  80,
+			EtherType:     rules.EtherType4,
+			PortRangeMax:  80,
+			Protocol:      "tcp",
+			RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5",
+			SecGroupID:    "a7734e61-b545-452d-a3cd-0189cbd9747a",
+		},
+		{
+			Description:  "test description of rule",
+			Direction:    "ingress",
+			PortRangeMin: 443,
+			EtherType:    rules.EtherType4,
+			PortRangeMax: 443,
+			Protocol:     "tcp",
+			SecGroupID:   "a7734e61-b545-452d-a3cd-0189cbd9747a",
+		},
+	}
+	{
+		_, err := rules.CreateBulk(context.TODO(), fake.ServiceClient(), opts).Extract()
+		th.AssertNoErr(t, err)
+	}
+
+	{
+		optsBuilder := make([]rules.CreateOptsBuilder, len(opts))
+		for i := range opts {
+			optsBuilder[i] = opts[i]
+		}
+		_, err := rules.CreateBulk(context.TODO(), fake.ServiceClient(), optsBuilder).Extract()
+		th.AssertNoErr(t, err)
+	}
+}
+
 func TestRequiredCreateOpts(t *testing.T) {
 	res := rules.Create(context.TODO(), fake.ServiceClient(), rules.CreateOpts{Direction: rules.DirIngress})
 	if res.Err == nil {
diff --git a/openstack/objectstorage/v1/containers/testing/fixtures.go b/openstack/objectstorage/v1/containers/testing/fixtures.go
index 55f4f723b..35c0f734c 100644
--- a/openstack/objectstorage/v1/containers/testing/fixtures.go
+++ b/openstack/objectstorage/v1/containers/testing/fixtures.go
@@ -258,6 +258,8 @@ func HandleGetContainerSuccessfully(t *testing.T, options ...option) {
 		w.Header().Set("X-Trans-Id", "tx554ed59667a64c61866f1-0057b4ba37")
 		w.Header().Set("X-Storage-Policy", "test_policy")
 		w.Header().Set("X-Versions-Enabled", "True")
+		w.Header().Set("X-Sync-Key", "272465181849")
+		w.Header().Set("X-Sync-To", "anotherContainer")
 		w.WriteHeader(http.StatusNoContent)
 	})
 }
diff --git a/openstack/objectstorage/v1/containers/testing/requests_test.go b/openstack/objectstorage/v1/containers/testing/requests_test.go
index 9a2074d51..fedf0f2fe 100644
--- a/openstack/objectstorage/v1/containers/testing/requests_test.go
+++ b/openstack/objectstorage/v1/containers/testing/requests_test.go
@@ -244,6 +244,8 @@ func TestGetContainer(t *testing.T) {
 		StoragePolicy:   "test_policy",
 		Timestamp:       1471298837.95721,
 		VersionsEnabled: true,
+		SyncKey:         "272465181849",
+		SyncTo:          "anotherContainer",
 	}
 	actual, err := res.Extract()
 	th.AssertNoErr(t, err)
diff --git a/testing/results_test.go b/testing/results_test.go
index 21ef44c80..51ba60499 100644
--- a/testing/results_test.go
+++ b/testing/results_test.go
@@ -113,6 +113,40 @@ func TestUnmarshalAnonymousStructs(t *testing.T) {
 	th.AssertEquals(t, "Canada unmarshalled", actual.Location)
 }
 
+func TestUnmarshalNilStruct(t *testing.T) {
+	var x *TestPerson
+	var y TestPerson
+
+	err1 := gophercloud.Result{}.ExtractIntoStructPtr(&x, "")
+	err2 := gophercloud.Result{}.ExtractIntoStructPtr(nil, "")
+	err3 := gophercloud.Result{}.ExtractIntoStructPtr(y, "")
+	err4 := gophercloud.Result{}.ExtractIntoStructPtr(&y, "")
+	err5 := gophercloud.Result{}.ExtractIntoStructPtr(x, "")
+
+	th.AssertErr(t, err1)
+	th.AssertErr(t, err2)
+	th.AssertErr(t, err3)
+	th.AssertNoErr(t, err4)
+	th.AssertErr(t, err5)
+}
+
+func TestUnmarshalNilSlice(t *testing.T) {
+	var x *[]TestPerson
+	var y []TestPerson
+
+	err1 := gophercloud.Result{}.ExtractIntoSlicePtr(&x, "")
+	err2 := gophercloud.Result{}.ExtractIntoSlicePtr(nil, "")
+	err3 := gophercloud.Result{}.ExtractIntoSlicePtr(y, "")
+	err4 := gophercloud.Result{}.ExtractIntoSlicePtr(&y, "")
+	err5 := gophercloud.Result{}.ExtractIntoSlicePtr(x, "")
+
+	th.AssertErr(t, err1)
+	th.AssertErr(t, err2)
+	th.AssertErr(t, err3)
+	th.AssertNoErr(t, err4)
+	th.AssertErr(t, err5)
+}
+
 // TestUnmarshalSliceofAnonymousStructs tests if UnmarshalJSON is called on each
 // of the anonymous structs contained in an overarching struct slice.
 func TestUnmarshalSliceOfAnonymousStructs(t *testing.T) {