Repository: gophercloud/gophercloud

Issue:
Pagination is broken from messaging service (Zaqar)
https://github.com/gophercloud/gophercloud/actions/workflows/functional-messaging.yaml job is red. Fails on openstack master since Jan 1st.

Hints from issue comments:
@mandre: Several tests fail because we're getting a 404 error:
```
     message_test.go:57: Failure in message_test.go, line 57: unexpected error "Expected HTTP response code [200 204 300] when accessing [GET http://10.1.0.118/v2/queues/ACPTTESTWromD/messages?include_delayed=true&marker=3], but got 404 instead: <!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\n<p>The requested URL was not found on this server.</p>\n<hr>\n<address>Apache/2.4.52 (Ubuntu) Server at 10.1.0.118 Port 80</address>\n</body></html>"
    messaging.go:44: Attempting to delete Queue: ACPTTESTWromD
    messaging.go:50: Deleted Queue: ACPTTESTWromD
--- FAIL: TestListMessages (0.24s)
```@mandre: I can't exactly figure what's going on. I wonder if this has to do with https://github.com/openstack/zaqar/commit/c9389d83a0d4f1471007e84430e6aa9c7a688c5a, which changed the devstack setup and merged at the same time our failure started happening.@stephenfin: This line concerns me:

```
     message_test.go:57: Failure in message_test.go, line 57: unexpected error "Expected HTTP response code [200 204 300] when accessing [GET http://10.1.0.118/v2/queues/ACPTTESTWromD/messages?include_delayed=true&marker=3], but got 404 instead: <!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\n<p>The requested URL was not found on this server.</p>\n<hr>\n<address>Apache/2.4.52 (Ubuntu) Server at 10.1.0.118 Port 80</address>\n</body></html>"
```

Specifically, the URL looks off:

```
GET http://10.1.0.118/v2/queues/ACPTTESTWromD/messages?include_delayed=true&marker=3
```

Based on the change you linked - specifically [this line](https://github.com/openstack/zaqar/commit/c9389d83a0d4f1471007e84430e6aa9c7a688c5a#diff-415b4cb5e7be0a0b150c94b44f0d8737ddb37e2978906f11b16301e1579a4a9cR283) - I'd have _expected_ this to read e.g.

```
GET http://10.1.0.118/messages/v2/queues/ACPTTESTWromD/messages?include_delayed=true&marker=3
```

I wonder if our parsing of the service catalog is off...@mandre: The error appears when retrieving the last message from a queue. I'm testing a change with https://github.com/gophercloud/gophercloud/pull/3341 without too much conviction.@stephenfin: Yeah, that looks like our issue. I deployed DevStack with the Zaqar plugin enabled and took a look at the service catalog. Zaqar is the only one that is giving a path that (I suspect?) is intended to be parsed as a relatively path or is flat out wrong:

```
$ curl -w "\n" http://10.0.110.161/messaging
{"versions": [{"id": "1.1", "status": "DEPRECATED", "updated": "2016-7-29T02:22:47Z", "media-types": [{"base": "application/json", "type": "application/vnd.openstack.messaging-v1_1+json"}], "links": [{"href": "/v1.1/", "rel": "self"}]}, {"id": "2", "status": "CURRENT", "updated": "2014-9-24T04:06:47Z", "media-types": [{"base": "application/json", "type": "application/vnd.openstack.messaging-v2+json"}], "links": [{"href": "/v2/", "rel": "self"}]}]}
```

By comparison, everything else uses fully-qualified absolute paths. For example, keystonm:

```shell
$ curl -w "\n" http://10.0.110.161/identity
{"versions": {"values": [{"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://10.0.110.161/identity/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+json"}]}]}}
```

For what it's worth, this also breaks `openstacksdk` so I have no idea how their CI is currently working 😅:

```python
>>> import openstack
>>> conn = openstack.connect('devstack-admin')
>>>
>>> conn.messaging.get('/').text
'<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">\n<html><head>\n<title>404 Not Found</title>\n</head><body>\n<h1>Not Found</h1>\n<p>The requested URL was not found on this server.</p>\n<hr>\n<address>Apache/2.4.58 (Ubuntu) Server at 10.0.110.161 Port 80</address>\n</body></html>\n'
>>>
>>> conn.identity.get('/').text
'{"version": {"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://10.0.110.161/identity/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+json"}]}}'
```

Full logs are below but IMO the fix is in Zaqar's DevStack plugin.

----

```shell
$ openstack catalog list 
+-----------------+---------------------+---------------------------------------------------------------------------+
| Name            | Type                | Endpoints                                                                 |
+-----------------+---------------------+---------------------------------------------------------------------------+
| nova            | compute             | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/compute/v2.1                                |
|                 |                     |                                                                           |
| nova_legacy     | compute_legacy      | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/compute/v2/3086ab003f784a49a8cacc2d4c281193 |
|                 |                     |                                                                           |
| zaqar-websocket | messaging-websocket | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161:9000                                        |
|                 |                     |                                                                           |
| neutron         | network             | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161:9696/networking                             |
|                 |                     |                                                                           |
| glance          | image               | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/image                                       |
|                 |                     |                                                                           |
| placement       | placement           | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/placement                                   |
|                 |                     |                                                                           |
| zaqar           | messaging           | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/messaging                                   |
|                 |                     |                                                                           |
| cinder          | block-storage       | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/volume/v3                                   |
|                 |                     |                                                                           |
| keystone        | identity            | RegionOne                                                                 |
|                 |                     |   public: http://10.0.110.161/identity                                    |
|                 |                     |                                                                           |
+-----------------+---------------------+---------------------------------------------------------------------------+

$ curl -w "\n" http://10.0.110.161/compute/v2
{"version": {"id": "v2.0", "status": "SUPPORTED", "version": "", "min_version": "", "updated": "2011-01-21T11:33:21Z", "links": [{"rel": "self", "href": "http://10.0.110.161/compute/v2/"}, {"rel": "describedby", "type": "text/html", "href": "http://docs.openstack.org/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.compute+json;version=2"}]}}

$ curl -w "\n" http://10.0.110.161:9696/networking
<html>
 <head>
  <title>302 Found</title>
 </head>
 <body>
  <h1>302 Found</h1>
  The resource was found at <a href="http://10.0.110.161:9696/networking/">http://10.0.110.161:9696/networking/</a>;
you should be redirected automatically.


 </body>
</html>

$ curl -w "\n" http://10.0.110.161:9696/networking/
{"versions": [{"id": "v2.0", "status": "CURRENT", "links": [{"rel": "self", "href": "http://10.0.110.161:9696/networking/v2.0/"}]}]}

$ curl -w "\n" http://10.0.110.161/image
{"versions": [{"id": "v2.16", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.15", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.14", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.9", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.17", "status": "CURRENT", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.7", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.6", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.5", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.4", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.3", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.2", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.1", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}, {"id": "v2.0", "status": "SUPPORTED", "links": [{"rel": "self", "href": "http://10.0.110.161/image/v2/"}]}]}

$ curl -w "\n" http://10.0.110.161/messaging
{"versions": [{"id": "1.1", "status": "DEPRECATED", "updated": "2016-7-29T02:22:47Z", "media-types": [{"base": "application/json", "type": "application/vnd.openstack.messaging-v1_1+json"}], "links": [{"href": "/v1.1/", "rel": "self"}]}, {"id": "2", "status": "CURRENT", "updated": "2014-9-24T04:06:47Z", "media-types": [{"base": "application/json", "type": "application/vnd.openstack.messaging-v2+json"}], "links": [{"href": "/v2/", "rel": "self"}]}]}

$ curl -w "\n" http://10.0.110.161/placement
{"versions": [{"id": "v1.0", "max_version": "1.39", "min_version": "1.0", "status": "CURRENT", "links": [{"rel": "self", "href": ""}]}]}

$ curl -w "\n" http://10.0.110.161/volume/v3
{"error": {"code": 401, "title": "Unauthorized", "message": "The request you have made requires authentication."}}

$ curl -w "\n" http://10.0.110.161/volume
{"versions": [{"id": "v3.0", "status": "CURRENT", "version": "3.71", "min_version": "3.0", "updated": "2023-08-31T00:00:00Z", "links": [{"rel": "describedby", "type": "text/html", "href": "https://docs.openstack.org/"}, {"rel": "self", "href": "http://10.0.110.161/volume/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.volume+json;version=3"}]}]}

$ curl -w "\n" http://10.0.110.161/identity
{"versions": {"values": [{"id": "v3.14", "status": "stable", "updated": "2020-04-07T00:00:00Z", "links": [{"rel": "self", "href": "http://10.0.110.161/identity/v3/"}], "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v3+json"}]}]}}
```@stephenfin: Bug report for Zaqar is here: https://bugs.launchpad.net/zaqar/+bug/2104271

Initial fix is here: https://review.opendev.org/c/openstack/zaqar/+/945589

More fixes will be needed across the API since this affects virtually all resources.@stephenfin: > Bug report for Zaqar is here: https://bugs.launchpad.net/zaqar/+bug/2104271
> 
> Initial fix is here: https://review.opendev.org/c/openstack/zaqar/+/945589
> 
> More fixes will be needed across the API since this affects virtually all resources.

It transpires that this is not a bug in Zaqar but rather in Gophercloud. Our pagination code for the affected resources is ignoring the path of the endpoint URL, when it should be concatenating these.

Solution patch:
diff --git a/.github/workflows/functional-baremetal.yaml b/.github/workflows/functional-baremetal.yaml
index 008f7a3d3..0fd1b2391 100644
--- a/.github/workflows/functional-baremetal.yaml
+++ b/.github/workflows/functional-baremetal.yaml
@@ -39,14 +39,6 @@ jobs:
     steps:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
-      - name: Workaround for grub-efi-amd64-signed
-        run: sudo rm /var/cache/debconf/config.dat
-        shell: bash
-        if: matrix.ubuntu_version == '20.04'
-      - name: Ensure update and upgrade
-        run: sudo apt update && sudo apt -y upgrade
-        shell: bash
-        if: matrix.ubuntu_version == '20.04'
       - name: Work around broken dnsmasq
         run: sudo apt-get purge -y dnsmasq-base
       - name: Deploy devstack
@@ -93,10 +85,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-baremetal
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/baremetal/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
           # TODO(dtantsur): default to true when no longer supporting versions before 2024.1
           USE_SYSTEM_SCOPE: ${{ matrix.use_system_scope }}
diff --git a/.github/workflows/functional-basic.yaml b/.github/workflows/functional-basic.yaml
index 0fc650a1f..e4e35e15a 100644
--- a/.github/workflows/functional-basic.yaml
+++ b/.github/workflows/functional-basic.yaml
@@ -45,10 +45,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-basic
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: './internal/acceptance/openstack'
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-blockstorage.yaml b/.github/workflows/functional-blockstorage.yaml
index 26e81321e..1e1cfefd9 100644
--- a/.github/workflows/functional-blockstorage.yaml
+++ b/.github/workflows/functional-blockstorage.yaml
@@ -44,10 +44,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-blockstorage
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/blockstorage/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-compute.yaml b/.github/workflows/functional-compute.yaml
index a01dbdfc9..caf7ce117 100644
--- a/.github/workflows/functional-compute.yaml
+++ b/.github/workflows/functional-compute.yaml
@@ -44,10 +44,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-compute
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/compute/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-containerinfra.yaml b/.github/workflows/functional-containerinfra.yaml
index 634685f60..15c4b2416 100644
--- a/.github/workflows/functional-containerinfra.yaml
+++ b/.github/workflows/functional-containerinfra.yaml
@@ -15,6 +15,12 @@ jobs:
             openstack_version: "master"
             ubuntu_version: "22.04"
             devstack_conf_overrides: |
+              # ensure we're using a working version of setuptools
+              if [ -n "\$TOP_DIR" ]; then
+                sed -i 's/setuptools\[core\]$/setuptools[core]==79.0.1/g' \$TOP_DIR/lib/infra \$TOP_DIR/inc/python
+                sed -i 's/pip_install "-U" "pbr"/pip_install "-U" "pbr" "setuptools[core]==79.0.1"/g' \$TOP_DIR/lib/infra
+              fi
+
               enable_plugin magnum https://github.com/openstack/magnum master
               MAGNUMCLIENT_BRANCH=master
             additional_services: "openstack-cli-server"
@@ -54,6 +60,7 @@ jobs:
             GLANCE_LIMIT_IMAGE_SIZE_TOTAL=5000
             SWIFT_MAX_FILE_SIZE=5368709122
             KEYSTONE_ADMIN_ENDPOINT=true
+
             ${{ matrix.devstack_conf_overrides }}
           enabled_services: "h-eng,h-api,h-api-cfn,h-api-cw,${{ matrix.additional_services }}"
       - name: Checkout go
@@ -61,10 +68,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-containerinfra
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/containerinfra/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-dns.yaml b/.github/workflows/functional-dns.yaml
index f829002ba..9795797ec 100644
--- a/.github/workflows/functional-dns.yaml
+++ b/.github/workflows/functional-dns.yaml
@@ -15,6 +15,12 @@ jobs:
           - name: "master"
             openstack_version: "master"
             ubuntu_version: "22.04"
+            devstack_conf_overrides: |
+              # ensure we're using a working version of setuptools
+              if [ -n "\$TOP_DIR" ]; then
+                sed -i 's/setuptools\[core\]$/setuptools[core]==79.0.1/g' \$TOP_DIR/lib/infra \$TOP_DIR/inc/python
+                sed -i 's/pip_install "-U" "pbr"/pip_install "-U" "pbr" "setuptools[core]==79.0.1"/g' \$TOP_DIR/lib/infra
+              fi
             additional_services: "openstack-cli-server"
           - name: "dalmatian"
             openstack_version: "stable/2024.2"
@@ -39,16 +45,19 @@ jobs:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
             enable_plugin designate https://github.com/openstack/designate ${{ matrix.openstack_version }}
+
+            ${{ matrix.devstack_conf_overrides }}
           enabled_services: "designate,designate-central,designate-api,designate-worker,designate-producer,designate-mdns,${{ matrix.additional_services }}"
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-dns
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/dns/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-fwaas_v2.yaml b/.github/workflows/functional-fwaas_v2.yaml
index 297da6ff2..fe4a47f1c 100644
--- a/.github/workflows/functional-fwaas_v2.yaml
+++ b/.github/workflows/functional-fwaas_v2.yaml
@@ -49,10 +49,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-networking
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/networking/v2/extensions/fwaas_v2/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-identity.yaml b/.github/workflows/functional-identity.yaml
index 6db4b9d11..49dbb6738 100644
--- a/.github/workflows/functional-identity.yaml
+++ b/.github/workflows/functional-identity.yaml
@@ -42,10 +42,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-identity
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/identity/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-image.yaml b/.github/workflows/functional-image.yaml
index 3b66c8598..32af7ac67 100644
--- a/.github/workflows/functional-image.yaml
+++ b/.github/workflows/functional-image.yaml
@@ -42,10 +42,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-image
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/image/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-keymanager.yaml b/.github/workflows/functional-keymanager.yaml
index 08ee8546e..ec58ffbeb 100644
--- a/.github/workflows/functional-keymanager.yaml
+++ b/.github/workflows/functional-keymanager.yaml
@@ -14,6 +14,12 @@ jobs:
           - name: "master"
             openstack_version: "master"
             ubuntu_version: "22.04"
+            devstack_conf_overrides: |
+              # ensure we're using a working version of setuptools
+              if [ -n "\$TOP_DIR" ]; then
+                sed -i 's/setuptools\[core\]$/setuptools[core]==79.0.1/g' \$TOP_DIR/lib/infra \$TOP_DIR/inc/python
+                sed -i 's/pip_install "-U" "pbr"/pip_install "-U" "pbr" "setuptools[core]==79.0.1"/g' \$TOP_DIR/lib/infra
+              fi
             additional_services: "openstack-cli-server"
           - name: "dalmatian"
             openstack_version: "stable/2024.2"
@@ -38,16 +44,19 @@ jobs:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
             enable_plugin barbican https://github.com/openstack/barbican ${{ matrix.openstack_version }}
+
+            ${{ matrix.devstack_conf_overrides }}
           enabled_services: "barbican-svc,barbican-retry,barbican-keystone-listener,${{ matrix.additional_services }}"
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-keymanager
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/keymanager/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-loadbalancer.yaml b/.github/workflows/functional-loadbalancer.yaml
index ec7b076ee..f2c61c5d2 100644
--- a/.github/workflows/functional-loadbalancer.yaml
+++ b/.github/workflows/functional-loadbalancer.yaml
@@ -14,6 +14,12 @@ jobs:
           - name: "master"
             openstack_version: "master"
             ubuntu_version: "22.04"
+            devstack_conf_overrides: |
+              # ensure we're using a working version of setuptools
+              if [ -n "\$TOP_DIR" ]; then
+                sed -i 's/setuptools\[core\]$/setuptools[core]==79.0.1/g' \$TOP_DIR/lib/infra \$TOP_DIR/inc/python
+                sed -i 's/pip_install "-U" "pbr"/pip_install "-U" "pbr" "setuptools[core]==79.0.1"/g' \$TOP_DIR/lib/infra
+              fi
             additional_services: "openstack-cli-server"
           - name: "dalmatian"
             openstack_version: "stable/2024.2"
@@ -39,16 +45,19 @@ jobs:
           conf_overrides: |
             enable_plugin octavia https://github.com/openstack/octavia ${{ matrix.openstack_version }}
             enable_plugin neutron https://github.com/openstack/neutron ${{ matrix.openstack_version }}
+
+            ${{ matrix.devstack_conf_overrides }}
           enabled_services: "octavia,o-api,o-cw,o-hk,o-hm,o-da,neutron-qos,${{ matrix.additional_services }}"
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-loadbalancer
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/loadbalancer/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-messaging.yaml b/.github/workflows/functional-messaging.yaml
index d3e3169f6..a002dc084 100644
--- a/.github/workflows/functional-messaging.yaml
+++ b/.github/workflows/functional-messaging.yaml
@@ -45,10 +45,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-messaging
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/messaging/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-networking.yaml b/.github/workflows/functional-networking.yaml
index 8722b6789..7800c7d8f 100644
--- a/.github/workflows/functional-networking.yaml
+++ b/.github/workflows/functional-networking.yaml
@@ -59,10 +59,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-networking
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/networking/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-objectstorage.yaml b/.github/workflows/functional-objectstorage.yaml
index ff88efbc1..6095701b4 100644
--- a/.github/workflows/functional-objectstorage.yaml
+++ b/.github/workflows/functional-objectstorage.yaml
@@ -48,10 +48,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-objectstorage
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/objectstorage/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-orchestration.yaml b/.github/workflows/functional-orchestration.yaml
index 9928aba17..05f2fc013 100644
--- a/.github/workflows/functional-orchestration.yaml
+++ b/.github/workflows/functional-orchestration.yaml
@@ -44,10 +44,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-orchestration
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/orchestration/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-placement.yaml b/.github/workflows/functional-placement.yaml
index 2f182285f..a0fdb33fa 100644
--- a/.github/workflows/functional-placement.yaml
+++ b/.github/workflows/functional-placement.yaml
@@ -42,10 +42,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-placement
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/placement/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-sharedfilesystems.yaml b/.github/workflows/functional-sharedfilesystems.yaml
index 56480d7d5..8f5deb116 100644
--- a/.github/workflows/functional-sharedfilesystems.yaml
+++ b/.github/workflows/functional-sharedfilesystems.yaml
@@ -14,6 +14,12 @@ jobs:
           - name: "master"
             openstack_version: "master"
             ubuntu_version: "22.04"
+            devstack_conf_overrides: |
+              # ensure we're using a working version of setuptools
+              if [ -n "\$TOP_DIR" ]; then
+                sed -i 's/setuptools\[core\]$/setuptools[core]==79.0.1/g' \$TOP_DIR/lib/infra \$TOP_DIR/inc/python
+                sed -i 's/pip_install "-U" "pbr"/pip_install "-U" "pbr" "setuptools[core]==79.0.1"/g' \$TOP_DIR/lib/infra
+              fi
             additional_services: "openstack-cli-server"
           - name: "dalmatian"
             openstack_version: "stable/2024.2"
@@ -52,16 +58,19 @@ jobs:
             MANILA_DEFAULT_SHARE_TYPE_EXTRA_SPECS='snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True mount_snapshot_support=True'
             MANILA_CONFIGURE_DEFAULT_TYPES=True
             MANILA_INSTALL_TEMPEST_PLUGIN_SYSTEMWIDE=false
+
+            ${{ matrix.devstack_conf_overrides }}
           enabled_services: "${{ matrix.additional_services }}"
       - name: Checkout go
         uses: actions/setup-go@v5
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-sharedfilesystems
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/sharedfilesystems/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/functional-workflow.yaml b/.github/workflows/functional-workflow.yaml
index 3ac1cdeb1..7cb15ec88 100644
--- a/.github/workflows/functional-workflow.yaml
+++ b/.github/workflows/functional-workflow.yaml
@@ -51,10 +51,11 @@ jobs:
         with:
           go-version: '^1.23'
       - name: Run Gophercloud acceptance tests
-        run: ./script/acceptancetest
+        run: |
+          source ${{ github.workspace }}/script/stackenv
+          make acceptance-workflow
         env:
           DEVSTACK_PATH: ${{ github.workspace }}/devstack
-          PACKAGE: "./internal/acceptance/openstack/workflow/..."
           OS_BRANCH: ${{ matrix.openstack_version }}
       - name: Generate logs on failure
         run: ./script/collectlogs
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 665b88718..ed7399819 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -1,27 +1,23 @@
-on: [push, pull_request]
 name: Linters
+on:
+  - push
+  - pull_request
 permissions:
   contents: read
-
 jobs:
   test:
     runs-on: ubuntu-latest
-
     steps:
+      - name: Checkout Gophercloud
+        uses: actions/checkout@v4
       - uses: actions/setup-go@v5
         with:
           go-version: '1'
-
-      - uses: actions/checkout@v4
-
-      - name: Run go fmt
+      - name: Run linters
         run: |
-          ./script/format
-
-      - name: Run go vet
-        run: |
-          ./script/vet
-
+          make lint
+      # TODO: Use 'go mod tidy -diff' instead once go 1.23 is out
+      # https://github.com/golang/go/issues/27005
       - name: Ensure go.mod is up-to-date
         run: |
           if [ $(go mod tidy && git diff | wc -l) -gt 0 ]; then git diff && exit 1; fi
diff --git a/.github/workflows/unit.yaml b/.github/workflows/unit.yaml
index 1489ec0e3..ab11e2656 100644
--- a/.github/workflows/unit.yaml
+++ b/.github/workflows/unit.yaml
@@ -1,8 +1,9 @@
-on: [push, pull_request]
 name: Unit Testing
+on:
+  - push
+  - pull_request
 permissions:
   contents: read
-
 jobs:
   test:
     permissions:
@@ -15,35 +16,29 @@ jobs:
         go-version:
           - "1.23.0"
           - "1"
-
     steps:
+      - name: Checkout Gophercloud
+        uses: actions/checkout@v4
       - name: Setup Go ${{ matrix.go-version }}
         uses: actions/setup-go@v5
         with:
           go-version: ${{ matrix.go-version }}
-
-      - uses: actions/checkout@v4
-
       - name: Setup environment
         run: |
           # Changing into a different directory to avoid polluting go.sum with "go get"
           cd "$(mktemp -d)"
           go mod init unit_tests
-
           go install github.com/wadey/gocovmerge@master
-
       - name: Run unit tests
         run: |
-          ./script/coverage
-          ./script/unittest
-
+          make unit
+          make coverage
       - name: Check coverage
         uses: coverallsapp/github-action@v2
         with:
           file: cover.out
           flag-name: Go-${{ matrix.go-version }}
           parallel: true
-
   finish:
     permissions:
       checks: write  # for coverallsapp/github-action to create a new check based on the results
diff --git a/Makefile b/Makefile
index 2a0618a6b..86356b78a 100644
--- a/Makefile
+++ b/Makefile
@@ -2,6 +2,7 @@ undefine GOFLAGS
 
 GOLANGCI_LINT_VERSION?=v1.62.2
 GO_TEST?=go run gotest.tools/gotestsum@latest --format testname --
+TIMEOUT := "60m"
 
 ifeq ($(shell command -v podman 2> /dev/null),)
 	RUNNER=docker
@@ -9,15 +10,18 @@ else
 	RUNNER=podman
 endif
 
-# if the golangci-lint steps fails with the following error message:
+# if the golangci-lint steps fails with one of the following error messages:
 #
 #   directory prefix . does not contain main module or its selected dependencies
 #
+#   failed to initialize build cache at /root/.cache/golangci-lint: mkdir /root/.cache/golangci-lint: permission denied
+#
 # you probably have to fix the SELinux security context for root directory plus your cache
 #
 #   chcon -Rt svirt_sandbox_file_t .
 #   chcon -Rt svirt_sandbox_file_t ~/.cache/golangci-lint
 lint:
+	mkdir -p ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION)
 	$(RUNNER) run -t --rm \
 		-v $(shell pwd):/app \
 		-v ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION):/root/.cache \
@@ -38,77 +42,81 @@ coverage:
 	$(GO_TEST) -covermode count -coverprofile cover.out -coverpkg=./... ./...
 .PHONY: coverage
 
-acceptance: acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-imageservice acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow
+acceptance: acceptance-basic acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-image acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow
 .PHONY: acceptance
 
+acceptance-basic:
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack
+.PHONY: acceptance-basic
+
 acceptance-baremetal:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/...
 .PHONY: acceptance-baremetal
 
 acceptance-blockstorage:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/...
 .PHONY: acceptance-blockstorage
 
 acceptance-compute:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/...
 .PHONY: acceptance-compute
 
 acceptance-container:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/...
 .PHONY: acceptance-container
 
 acceptance-containerinfra:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/...
 .PHONY: acceptance-containerinfra
 
 acceptance-db:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/...
 .PHONY: acceptance-db
 
 acceptance-dns:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/...
 .PHONY: acceptance-dns
 
 acceptance-identity:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/...
 .PHONY: acceptance-identity
 
 acceptance-image:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/imageservice/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/image/...
 .PHONY: acceptance-image
 
 acceptance-keymanager:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/...
 .PHONY: acceptance-keymanager
 
 acceptance-loadbalancer:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/...
 .PHONY: acceptance-loadbalancer
 
 acceptance-messaging:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/...
 .PHONY: acceptance-messaging
 
 acceptance-networking:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/...
 .PHONY: acceptance-networking
 
 acceptance-objectstorage:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/...
 .PHONY: acceptance-objectstorage
 
 acceptance-orchestration:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/...
 .PHONY: acceptance-orchestration
 
 acceptance-placement:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/...
 .PHONY: acceptance-placement
 
 acceptance-sharedfilesystems:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/...
 .PHONY: acceptance-sharefilesystems
 
 acceptance-workflow:
-	$(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/...
+	$(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/...
 .PHONY: acceptance-workflow
diff --git a/go.mod b/go.mod
index 078a93042..e1ad7471b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.23.0
 toolchain go1.23.6
 
 require (
-	golang.org/x/crypto v0.37.0
+	golang.org/x/crypto v0.38.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
-require golang.org/x/sys v0.32.0 // indirect
+require golang.org/x/sys v0.33.0 // indirect
diff --git a/go.sum b/go.sum
index a6575a7f8..ff29c0704 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,9 @@
-golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
-golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
-golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
-golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
-golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
+golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
+golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
 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/README.md b/internal/acceptance/README.md
index cbbbac7f3..febf394f1 100644
--- a/internal/acceptance/README.md
+++ b/internal/acceptance/README.md
@@ -91,7 +91,13 @@ to set them manually.
 From the root directory, run:
 
 ```
-./script/acceptancetest
+make acceptance
+```
+
+You can also run tests for a specific service:
+
+```
+make acceptance-compute
 ```
 
 Alternatively, add the following to your `.bashrc`:
diff --git a/internal/acceptance/openstack/containerinfra/v1/containerinfra.go b/internal/acceptance/openstack/containerinfra/v1/containerinfra.go
index 9b26c0ad2..84ee98e87 100644
--- a/internal/acceptance/openstack/containerinfra/v1/containerinfra.go
+++ b/internal/acceptance/openstack/containerinfra/v1/containerinfra.go
@@ -31,6 +31,9 @@ func CreateClusterTemplateCOE(t *testing.T, client *gophercloud.ServiceClient, c
 	t.Logf("Attempting to create %s cluster template: %s", coe, name)
 
 	boolFalse := false
+	labels := map[string]string{
+		"test": "test",
+	}
 	createOpts := clustertemplates.CreateOpts{
 		COE:                 coe,
 		DNSNameServer:       "8.8.8.8",
@@ -45,6 +48,8 @@ func CreateClusterTemplateCOE(t *testing.T, client *gophercloud.ServiceClient, c
 		Public:              &boolFalse,
 		RegistryEnabled:     &boolFalse,
 		ServerType:          "vm",
+		// workaround for https://bugs.launchpad.net/magnum/+bug/2109685
+		Labels: labels,
 	}
 
 	res := clustertemplates.Create(context.TODO(), client, createOpts)
@@ -68,6 +73,7 @@ func CreateClusterTemplateCOE(t *testing.T, client *gophercloud.ServiceClient, c
 	tools.PrintResource(t, clusterTemplate.CreatedAt)
 
 	th.AssertEquals(t, name, clusterTemplate.Name)
+	th.AssertDeepEquals(t, labels, clusterTemplate.Labels)
 	th.AssertEquals(t, choices.ExternalNetworkID, clusterTemplate.ExternalNetworkID)
 	th.AssertEquals(t, choices.MagnumImageID, clusterTemplate.ImageID)
 
diff --git a/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go b/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
index 907e9d565..6a36a49d2 100644
--- a/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
+++ b/internal/acceptance/openstack/loadbalancer/v2/loadbalancer.go
@@ -214,6 +214,7 @@ func CreateLoadBalancerFullyPopulated(t *testing.T, client *gophercloud.ServiceC
 					MaxRetries:     5,
 					MaxRetriesDown: 4,
 					Type:           monitors.TypeHTTP,
+					HTTPVersion:    "1.0",
 				},
 			},
 			L7Policies: []l7policies.CreateOpts{{
@@ -272,6 +273,13 @@ func CreateLoadBalancerFullyPopulated(t *testing.T, client *gophercloud.ServiceC
 	th.AssertEquals(t, lb.Pools[0].Members[0].ProtocolPort, memberPort)
 	th.AssertEquals(t, lb.Pools[0].Members[0].Weight, memberWeight)
 
+	th.AssertEquals(t, lb.Pools[0].Monitor.Delay, 10)
+	th.AssertEquals(t, lb.Pools[0].Monitor.Timeout, 5)
+	th.AssertEquals(t, lb.Pools[0].Monitor.MaxRetries, 5)
+	th.AssertEquals(t, lb.Pools[0].Monitor.MaxRetriesDown, 4)
+	th.AssertEquals(t, lb.Pools[0].Monitor.Type, string(monitors.TypeHTTP))
+	th.AssertEquals(t, lb.Pools[0].Monitor.HTTPVersion, "1.0")
+
 	if len(tags) > 0 {
 		th.AssertDeepEquals(t, lb.Tags, tags)
 	}
@@ -333,6 +341,7 @@ func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient, lb *loadbala
 		MaxRetries:     5,
 		MaxRetriesDown: 4,
 		Type:           monitors.TypePING,
+		HTTPVersion:    "1.1",
 	}
 
 	monitor, err := monitors.Create(context.TODO(), client, createOpts).Extract()
@@ -352,6 +361,7 @@ func CreateMonitor(t *testing.T, client *gophercloud.ServiceClient, lb *loadbala
 	th.AssertEquals(t, monitor.Timeout, 5)
 	th.AssertEquals(t, monitor.MaxRetries, 5)
 	th.AssertEquals(t, monitor.MaxRetriesDown, 4)
+	th.AssertEquals(t, monitor.HTTPVersion, "1.1")
 
 	return monitor, nil
 }
diff --git a/openstack/blockstorage/v2/backups/requests.go b/openstack/blockstorage/v2/backups/requests.go
index 573946ab2..a791191d9 100644
--- a/openstack/blockstorage/v2/backups/requests.go
+++ b/openstack/blockstorage/v2/backups/requests.go
@@ -238,6 +238,12 @@ func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, o
 	return
 }
 
+// RestoreOptsBuilder allows extensions to add additional parameters to the
+// Restore request.
+type RestoreOptsBuilder interface {
+	ToRestoreMap() (map[string]any, error)
+}
+
 // RestoreOpts contains options for restoring a Backup. This object is passed to
 // the backups.RestoreFromBackup function.
 type RestoreOpts struct {
@@ -257,7 +263,7 @@ func (opts RestoreOpts) ToRestoreMap() (map[string]any, error) {
 // RestoreFromBackup will restore a Backup to a volume based on the values in
 // RestoreOpts. To extract the Restore object from the response, call the
 // Extract method on the RestoreResult.
-func RestoreFromBackup(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RestoreOpts) (r RestoreResult) {
+func RestoreFromBackup(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RestoreOptsBuilder) (r RestoreResult) {
 	b, err := opts.ToRestoreMap()
 	if err != nil {
 		r.Err = err
@@ -278,6 +284,12 @@ func Export(ctx context.Context, client *gophercloud.ServiceClient, id string) (
 	return
 }
 
+// ImportOptsBuilder allows extensions to add additional parameters to the
+// Import request.
+type ImportOptsBuilder interface {
+	ToBackupImportMap() (map[string]any, error)
+}
+
 // ImportOpts contains options for importing a Backup. This object is passed to
 // the backups.ImportBackup function.
 type ImportOpts BackupRecord
@@ -291,7 +303,7 @@ func (opts ImportOpts) ToBackupImportMap() (map[string]any, error) {
 // Import will import a Backup data to a backup based on the values in
 // ImportOpts. To extract the Backup object from the response, call the
 // Extract method on the ImportResult.
-func Import(ctx context.Context, client *gophercloud.ServiceClient, opts ImportOpts) (r ImportResult) {
+func Import(ctx context.Context, client *gophercloud.ServiceClient, opts ImportOptsBuilder) (r ImportResult) {
 	b, err := opts.ToBackupImportMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/blockstorage/v2/transfers/requests.go b/openstack/blockstorage/v2/transfers/requests.go
index 9c762cfe1..4d146ff23 100644
--- a/openstack/blockstorage/v2/transfers/requests.go
+++ b/openstack/blockstorage/v2/transfers/requests.go
@@ -7,6 +7,12 @@ import (
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToCreateMap() (map[string]any, error)
+}
+
 // CreateOpts contains options for a Volume transfer.
 type CreateOpts struct {
 	// The ID of the volume to transfer.
@@ -23,7 +29,7 @@ func (opts CreateOpts) ToCreateMap() (map[string]any, error) {
 }
 
 // Create will create a volume tranfer request based on the values in CreateOpts.
-func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToCreateMap()
 	if err != nil {
 		r.Err = err
@@ -36,6 +42,12 @@ func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateO
 	return
 }
 
+// AcceptOptsBuilder allows extensions to add additional parameters to the
+// Accept request.
+type AcceptOptsBuilder interface {
+	ToAcceptMap() (map[string]any, error)
+}
+
 // AcceptOpts contains options for a Volume transfer accept reqeust.
 type AcceptOpts struct {
 	// The auth key of the volume transfer to accept.
@@ -49,7 +61,7 @@ func (opts AcceptOpts) ToAcceptMap() (map[string]any, error) {
 }
 
 // Accept will accept a volume tranfer request based on the values in AcceptOpts.
-func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOpts) (r CreateResult) {
+func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOptsBuilder) (r CreateResult) {
 	b, err := opts.ToAcceptMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/blockstorage/v2/volumes/requests.go b/openstack/blockstorage/v2/volumes/requests.go
index 9d1e797fc..62b36d214 100644
--- a/openstack/blockstorage/v2/volumes/requests.go
+++ b/openstack/blockstorage/v2/volumes/requests.go
@@ -617,6 +617,12 @@ func SetImageMetadata(ctx context.Context, client *gophercloud.ServiceClient, id
 	return
 }
 
+// BootableOptsBuilder allows extensions to add additional parameters to the
+// SetBootable request.
+type BootableOptsBuilder interface {
+	ToBootableMap() (map[string]any, error)
+}
+
 // BootableOpts contains options for setting bootable status to a volume.
 type BootableOpts struct {
 	// Enables or disables the bootable attribute. You can boot an instance from a bootable volume.
@@ -630,7 +636,7 @@ func (opts BootableOpts) ToBootableMap() (map[string]any, error) {
 }
 
 // SetBootable will set bootable status on a volume based on the values in BootableOpts
-func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOpts) (r SetBootableResult) {
+func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOptsBuilder) (r SetBootableResult) {
 	b, err := opts.ToBootableMap()
 	if err != nil {
 		r.Err = err
@@ -691,6 +697,12 @@ func ChangeType(ctx context.Context, client *gophercloud.ServiceClient, id strin
 	return
 }
 
+// ReImageOptsBuilder allows extensions to add additional parameters to the
+// ReImage request.
+type ReImageOptsBuilder interface {
+	ToReImageMap() (map[string]any, error)
+}
+
 // ReImageOpts contains options for Re-image a volume.
 type ReImageOpts struct {
 	// New image id
@@ -705,7 +717,7 @@ func (opts ReImageOpts) ToReImageMap() (map[string]any, error) {
 }
 
 // ReImage will re-image a volume based on the values in ReImageOpts
-func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOpts) (r ReImageResult) {
+func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOptsBuilder) (r ReImageResult) {
 	b, err := opts.ToReImageMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/blockstorage/v3/backups/requests.go b/openstack/blockstorage/v3/backups/requests.go
index a1f3cc8ee..d75da4c25 100644
--- a/openstack/blockstorage/v3/backups/requests.go
+++ b/openstack/blockstorage/v3/backups/requests.go
@@ -238,6 +238,12 @@ func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, o
 	return
 }
 
+// RestoreOptsBuilder allows extensions to add additional parameters to the
+// Restore request.
+type RestoreOptsBuilder interface {
+	ToRestoreMap() (map[string]any, error)
+}
+
 // RestoreOpts contains options for restoring a Backup. This object is passed to
 // the backups.RestoreFromBackup function.
 type RestoreOpts struct {
@@ -257,7 +263,7 @@ func (opts RestoreOpts) ToRestoreMap() (map[string]any, error) {
 // RestoreFromBackup will restore a Backup to a volume based on the values in
 // RestoreOpts. To extract the Restore object from the response, call the
 // Extract method on the RestoreResult.
-func RestoreFromBackup(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RestoreOpts) (r RestoreResult) {
+func RestoreFromBackup(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RestoreOptsBuilder) (r RestoreResult) {
 	b, err := opts.ToRestoreMap()
 	if err != nil {
 		r.Err = err
@@ -278,6 +284,12 @@ func Export(ctx context.Context, client *gophercloud.ServiceClient, id string) (
 	return
 }
 
+// ImportOptsBuilder allows extensions to add additional parameters to the
+// Import request.
+type ImportOptsBuilder interface {
+	ToBackupImportMap() (map[string]any, error)
+}
+
 // ImportOpts contains options for importing a Backup. This object is passed to
 // the backups.ImportBackup function.
 type ImportOpts BackupRecord
@@ -291,7 +303,7 @@ func (opts ImportOpts) ToBackupImportMap() (map[string]any, error) {
 // Import will import a Backup data to a backup based on the values in
 // ImportOpts. To extract the Backup object from the response, call the
 // Extract method on the ImportResult.
-func Import(ctx context.Context, client *gophercloud.ServiceClient, opts ImportOpts) (r ImportResult) {
+func Import(ctx context.Context, client *gophercloud.ServiceClient, opts ImportOptsBuilder) (r ImportResult) {
 	b, err := opts.ToBackupImportMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/blockstorage/v3/transfers/requests.go b/openstack/blockstorage/v3/transfers/requests.go
index 9c762cfe1..4d146ff23 100644
--- a/openstack/blockstorage/v3/transfers/requests.go
+++ b/openstack/blockstorage/v3/transfers/requests.go
@@ -7,6 +7,12 @@ import (
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToCreateMap() (map[string]any, error)
+}
+
 // CreateOpts contains options for a Volume transfer.
 type CreateOpts struct {
 	// The ID of the volume to transfer.
@@ -23,7 +29,7 @@ func (opts CreateOpts) ToCreateMap() (map[string]any, error) {
 }
 
 // Create will create a volume tranfer request based on the values in CreateOpts.
-func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToCreateMap()
 	if err != nil {
 		r.Err = err
@@ -36,6 +42,12 @@ func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateO
 	return
 }
 
+// AcceptOptsBuilder allows extensions to add additional parameters to the
+// Accept request.
+type AcceptOptsBuilder interface {
+	ToAcceptMap() (map[string]any, error)
+}
+
 // AcceptOpts contains options for a Volume transfer accept reqeust.
 type AcceptOpts struct {
 	// The auth key of the volume transfer to accept.
@@ -49,7 +61,7 @@ func (opts AcceptOpts) ToAcceptMap() (map[string]any, error) {
 }
 
 // Accept will accept a volume tranfer request based on the values in AcceptOpts.
-func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOpts) (r CreateResult) {
+func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOptsBuilder) (r CreateResult) {
 	b, err := opts.ToAcceptMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/blockstorage/v3/volumes/requests.go b/openstack/blockstorage/v3/volumes/requests.go
index 77210943b..4dc2c1be7 100644
--- a/openstack/blockstorage/v3/volumes/requests.go
+++ b/openstack/blockstorage/v3/volumes/requests.go
@@ -623,6 +623,12 @@ func SetImageMetadata(ctx context.Context, client *gophercloud.ServiceClient, id
 	return
 }
 
+// BootableOptsBuilder allows extensions to add additional parameters to the
+// SetBootable request.
+type BootableOptsBuilder interface {
+	ToBootableMap() (map[string]any, error)
+}
+
 // BootableOpts contains options for setting bootable status to a volume.
 type BootableOpts struct {
 	// Enables or disables the bootable attribute. You can boot an instance from a bootable volume.
@@ -636,7 +642,7 @@ func (opts BootableOpts) ToBootableMap() (map[string]any, error) {
 }
 
 // SetBootable will set bootable status on a volume based on the values in BootableOpts
-func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOpts) (r SetBootableResult) {
+func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOptsBuilder) (r SetBootableResult) {
 	b, err := opts.ToBootableMap()
 	if err != nil {
 		r.Err = err
@@ -697,6 +703,12 @@ func ChangeType(ctx context.Context, client *gophercloud.ServiceClient, id strin
 	return
 }
 
+// ReImageOptsBuilder allows extensions to add additional parameters to the
+// ReImage request.
+type ReImageOptsBuilder interface {
+	ToReImageMap() (map[string]any, error)
+}
+
 // ReImageOpts contains options for Re-image a volume.
 type ReImageOpts struct {
 	// New image id
@@ -711,7 +723,7 @@ func (opts ReImageOpts) ToReImageMap() (map[string]any, error) {
 }
 
 // ReImage will re-image a volume based on the values in ReImageOpts
-func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOpts) (r ReImageResult) {
+func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOptsBuilder) (r ReImageResult) {
 	b, err := opts.ToReImageMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/compute/v2/aggregates/requests.go b/openstack/compute/v2/aggregates/requests.go
index be20cbffe..bdf9ccb3d 100644
--- a/openstack/compute/v2/aggregates/requests.go
+++ b/openstack/compute/v2/aggregates/requests.go
@@ -15,6 +15,12 @@ func List(client *gophercloud.ServiceClient) pagination.Pager {
 	})
 }
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToAggregatesCreateMap() (map[string]any, error)
+}
+
 type CreateOpts struct {
 	// The name of the host aggregate.
 	Name string `json:"name" required:"true"`
@@ -31,7 +37,7 @@ func (opts CreateOpts) ToAggregatesCreateMap() (map[string]any, error) {
 }
 
 // Create makes a request against the API to create an aggregate.
-func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToAggregatesCreateMap()
 	if err != nil {
 		r.Err = err
@@ -64,6 +70,12 @@ func Get(ctx context.Context, client *gophercloud.ServiceClient, aggregateID int
 	return
 }
 
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToAggregatesUpdateMap() (map[string]any, error)
+}
+
 type UpdateOpts struct {
 	// The name of the host aggregate.
 	Name *string `json:"name,omitempty"`
@@ -80,7 +92,7 @@ func (opts UpdateOpts) ToAggregatesUpdateMap() (map[string]any, error) {
 }
 
 // Update makes a request against the API to update a specific aggregate.
-func Update(ctx context.Context, client *gophercloud.ServiceClient, aggregateID int, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, client *gophercloud.ServiceClient, aggregateID int, opts UpdateOptsBuilder) (r UpdateResult) {
 	v := strconv.Itoa(aggregateID)
 
 	b, err := opts.ToAggregatesUpdateMap()
diff --git a/openstack/compute/v2/services/requests.go b/openstack/compute/v2/services/requests.go
index 7f6ac1d6d..32be17cb7 100644
--- a/openstack/compute/v2/services/requests.go
+++ b/openstack/compute/v2/services/requests.go
@@ -51,6 +51,12 @@ const (
 	ServiceDisabled ServiceStatus = "disabled"
 )
 
+// UpdateOptsBuilder allows extensions to add additional parameters to the
+// Update request.
+type UpdateOptsBuilder interface {
+	ToServiceUpdateMap() (map[string]any, error)
+}
+
 // UpdateOpts specifies the base attributes that may be updated on a service.
 type UpdateOpts struct {
 	// Status represents the new service status. One of enabled or disabled.
@@ -70,7 +76,7 @@ func (opts UpdateOpts) ToServiceUpdateMap() (map[string]any, error) {
 }
 
 // Update requests that various attributes of the indicated service be changed.
-func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToServiceUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/config/clouds/clouds.go b/openstack/config/clouds/clouds.go
index e2310fbc1..146605809 100644
--- a/openstack/config/clouds/clouds.go
+++ b/openstack/config/clouds/clouds.go
@@ -40,7 +40,7 @@ import (
 //     clouds.yaml, use that location as the only search location for `clouds.yaml` and `secure.yaml`;
 //   - otherwise, the search locations for `clouds.yaml` and `secure.yaml` are:
 //     1. the current working directory (on Linux: `./`)
-//     2. the directory `openstack` under the standatd user config location for
+//     2. the directory `openstack` under the standard user config location for
 //     the operating system (on Linux: `${XDG_CONFIG_HOME:-$HOME/.config}/openstack/`)
 //     3. on Linux, `/etc/openstack/`
 //
diff --git a/openstack/identity/v3/ec2credentials/requests.go b/openstack/identity/v3/ec2credentials/requests.go
index aefb2b14b..df3c59aff 100644
--- a/openstack/identity/v3/ec2credentials/requests.go
+++ b/openstack/identity/v3/ec2credentials/requests.go
@@ -22,6 +22,12 @@ func Get(ctx context.Context, client *gophercloud.ServiceClient, userID string,
 	return
 }
 
+// CreateOptsBuilder allows extensions to add additional parameters to the
+// Create request.
+type CreateOptsBuilder interface {
+	ToCredentialCreateMap() (map[string]any, error)
+}
+
 // CreateOpts provides options used to create an EC2 credential.
 type CreateOpts struct {
 	// TenantID is the project ID scope of the EC2 credential.
@@ -34,7 +40,7 @@ func (opts CreateOpts) ToCredentialCreateMap() (map[string]any, error) {
 }
 
 // Create creates a new EC2 Credential.
-func Create(ctx context.Context, client *gophercloud.ServiceClient, userID string, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, client *gophercloud.ServiceClient, userID string, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToCredentialCreateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/identity/v3/oauth1/requests.go b/openstack/identity/v3/oauth1/requests.go
index 8c66b36e2..0b23269ff 100644
--- a/openstack/identity/v3/oauth1/requests.go
+++ b/openstack/identity/v3/oauth1/requests.go
@@ -214,6 +214,12 @@ func GetConsumer(ctx context.Context, client *gophercloud.ServiceClient, id stri
 	return
 }
 
+// UpdateConsumerOptsBuilder allows extensions to add additional parameters to the
+// UpdateConsumer request.
+type UpdateConsumerOptsBuilder interface {
+	ToOAuth1UpdateConsumerMap() (map[string]any, error)
+}
+
 // UpdateConsumerOpts provides options used to update a consumer.
 type UpdateConsumerOpts struct {
 	// Description is the consumer description.
@@ -227,7 +233,7 @@ func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]any, erro
 }
 
 // UpdateConsumer updates an existing Consumer.
-func UpdateConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) {
+func UpdateConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateConsumerOptsBuilder) (r UpdateConsumerResult) {
 	b, err := opts.ToOAuth1UpdateConsumerMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/identity/v3/projects/requests.go b/openstack/identity/v3/projects/requests.go
index 30338fc51..6fb8448a9 100644
--- a/openstack/identity/v3/projects/requests.go
+++ b/openstack/identity/v3/projects/requests.go
@@ -276,8 +276,7 @@ func (opts ModifyTagsOpts) ToModifyTagsCreateMap() (map[string]any, error) {
 }
 
 // ModifyTags deletes all tags of a project and adds new ones.
-func ModifyTags(ctx context.Context, client *gophercloud.ServiceClient, projectID string, opts ModifyTagsOpts) (r ModifyTagsResult) {
-
+func ModifyTags(ctx context.Context, client *gophercloud.ServiceClient, projectID string, opts ModifyTagsOptsBuilder) (r ModifyTagsResult) {
 	b, err := opts.ToModifyTagsCreateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/flavorprofiles/requests.go b/openstack/loadbalancer/v2/flavorprofiles/requests.go
index 3c764592c..a541c6c77 100644
--- a/openstack/loadbalancer/v2/flavorprofiles/requests.go
+++ b/openstack/loadbalancer/v2/flavorprofiles/requests.go
@@ -121,7 +121,7 @@ func (opts UpdateOpts) ToFlavorProfileUpdateMap() (map[string]any, error) {
 
 // Update is an operation which modifies the attributes of the specified
 // FlavorProfile.
-func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToFlavorProfileUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/flavors/requests.go b/openstack/loadbalancer/v2/flavors/requests.go
index 499bb3bd0..52c936269 100644
--- a/openstack/loadbalancer/v2/flavors/requests.go
+++ b/openstack/loadbalancer/v2/flavors/requests.go
@@ -127,7 +127,7 @@ func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]any, error) {
 
 // Update is an operation which modifies the attributes of the specified
 // Flavor.
-func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToFlavorUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/l7policies/requests.go b/openstack/loadbalancer/v2/l7policies/requests.go
index 62a4f179e..ab0b22c6b 100644
--- a/openstack/loadbalancer/v2/l7policies/requests.go
+++ b/openstack/loadbalancer/v2/l7policies/requests.go
@@ -263,6 +263,12 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U
 	return
 }
 
+// CreateRuleOptsBuilder allows extensions to add additional parameters to the
+// CreateRule request.
+type CreateRuleOptsBuilder interface {
+	ToRuleCreateMap() (map[string]any, error)
+}
+
 // CreateRuleOpts is the common options struct used in this package's CreateRule
 // operation.
 type CreateRuleOpts struct {
@@ -300,7 +306,7 @@ func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]any, error) {
 }
 
 // CreateRule will create and associate a Rule with a particular L7Policy.
-func CreateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, opts CreateRuleOpts) (r CreateRuleResult) {
+func CreateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, opts CreateRuleOptsBuilder) (r CreateRuleResult) {
 	b, err := opts.ToRuleCreateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/listeners/requests.go b/openstack/loadbalancer/v2/listeners/requests.go
index 3216fbddd..abd5d0897 100644
--- a/openstack/loadbalancer/v2/listeners/requests.go
+++ b/openstack/loadbalancer/v2/listeners/requests.go
@@ -380,7 +380,7 @@ func (opts UpdateOpts) ToListenerUpdateMap() (map[string]any, error) {
 
 // Update is an operation which modifies the attributes of the specified
 // Listener.
-func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToListenerUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/loadbalancers/requests.go b/openstack/loadbalancer/v2/loadbalancers/requests.go
index f815806f3..095170edd 100644
--- a/openstack/loadbalancer/v2/loadbalancers/requests.go
+++ b/openstack/loadbalancer/v2/loadbalancers/requests.go
@@ -208,7 +208,7 @@ func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]any, error) {
 
 // Update is an operation which modifies the attributes of the specified
 // LoadBalancer.
-func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToLoadBalancerUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/loadbalancer/v2/monitors/requests.go b/openstack/loadbalancer/v2/monitors/requests.go
index be5701c5f..15a503bad 100644
--- a/openstack/loadbalancer/v2/monitors/requests.go
+++ b/openstack/loadbalancer/v2/monitors/requests.go
@@ -2,6 +2,7 @@ package monitors
 
 import (
 	"context"
+	"strconv"
 
 	"github.com/gophercloud/gophercloud/v2"
 	"github.com/gophercloud/gophercloud/v2/pagination"
@@ -153,7 +154,25 @@ type CreateOpts struct {
 
 // ToMonitorCreateMap builds a request body from CreateOpts.
 func (opts CreateOpts) ToMonitorCreateMap() (map[string]any, error) {
-	return gophercloud.BuildRequestBody(opts, "healthmonitor")
+	b, err := gophercloud.BuildRequestBody(opts, "healthmonitor")
+	if err != nil {
+		return nil, err
+	}
+
+	if v, ok := b["healthmonitor"]; ok {
+		if m, ok := v.(map[string]any); ok {
+			if v, ok := m["http_version"]; ok {
+				if v, ok := v.(string); ok {
+					m["http_version"], err = strconv.ParseFloat(v, 64)
+					if err != nil {
+						return nil, err
+					}
+				}
+			}
+		}
+	}
+
+	return b, nil
 }
 
 /*
@@ -247,7 +266,25 @@ type UpdateOpts struct {
 
 // ToMonitorUpdateMap builds a request body from UpdateOpts.
 func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]any, error) {
-	return gophercloud.BuildRequestBody(opts, "healthmonitor")
+	b, err := gophercloud.BuildRequestBody(opts, "healthmonitor")
+	if err != nil {
+		return nil, err
+	}
+
+	if v, ok := b["healthmonitor"]; ok {
+		if m, ok := v.(map[string]any); ok {
+			if v, ok := m["http_version"]; ok {
+				if v, ok := v.(string); ok {
+					m["http_version"], err = strconv.ParseFloat(v, 64)
+					if err != nil {
+						return nil, err
+					}
+				}
+			}
+		}
+	}
+
+	return b, nil
 }
 
 // Update is an operation which modifies the attributes of the specified
diff --git a/openstack/loadbalancer/v2/monitors/results.go b/openstack/loadbalancer/v2/monitors/results.go
index 644ef1870..6e8563faa 100644
--- a/openstack/loadbalancer/v2/monitors/results.go
+++ b/openstack/loadbalancer/v2/monitors/results.go
@@ -1,6 +1,9 @@
 package monitors
 
 import (
+	"encoding/json"
+	"strconv"
+
 	"github.com/gophercloud/gophercloud/v2"
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
@@ -61,7 +64,7 @@ type Monitor struct {
 	HTTPMethod string `json:"http_method"`
 
 	// The HTTP version that the monitor uses for requests.
-	HTTPVersion string `json:"http_version"`
+	HTTPVersion string `json:"-"`
 
 	// The HTTP path of the request sent by the monitor to test the health of a
 	// member. Must be a string beginning with a forward slash (/).
@@ -96,6 +99,26 @@ type Monitor struct {
 	Tags []string `json:"tags"`
 }
 
+func (r *Monitor) UnmarshalJSON(b []byte) error {
+	type tmp Monitor
+	var s struct {
+		tmp
+		HTTPVersion float64 `json:"http_version"`
+	}
+
+	err := json.Unmarshal(b, &s)
+	if err != nil {
+		return err
+	}
+
+	*r = Monitor(s.tmp)
+	if s.HTTPVersion != 0 {
+		r.HTTPVersion = strconv.FormatFloat(s.HTTPVersion, 'f', 1, 64)
+	}
+
+	return nil
+}
+
 // MonitorPage is the page returned by a pager when traversing over a
 // collection of health monitors.
 type MonitorPage struct {
diff --git a/openstack/networking/v2/extensions/bgp/peers/requests.go b/openstack/networking/v2/extensions/bgp/peers/requests.go
index eef38c0b7..589c4f7ad 100644
--- a/openstack/networking/v2/extensions/bgp/peers/requests.go
+++ b/openstack/networking/v2/extensions/bgp/peers/requests.go
@@ -43,7 +43,7 @@ func (opts CreateOpts) ToPeerCreateMap() (map[string]any, error) {
 }
 
 // Create a BGP Peer
-func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToPeerCreateMap()
 	if err != nil {
 		r.Err = err
@@ -79,7 +79,7 @@ func (opts UpdateOpts) ToPeerUpdateMap() (map[string]any, error) {
 }
 
 // Update accept a BGP Peer ID and an UpdateOpts and update the BGP Peer
-func Update(ctx context.Context, c *gophercloud.ServiceClient, bgpPeerID string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, bgpPeerID string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToPeerUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/networking/v2/extensions/bgp/speakers/requests.go b/openstack/networking/v2/extensions/bgp/speakers/requests.go
index b6dd26d90..8597602a7 100644
--- a/openstack/networking/v2/extensions/bgp/speakers/requests.go
+++ b/openstack/networking/v2/extensions/bgp/speakers/requests.go
@@ -43,7 +43,7 @@ func (opts CreateOpts) ToSpeakerCreateMap() (map[string]any, error) {
 }
 
 // Create accepts a CreateOpts and create a BGP Speaker.
-func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToSpeakerCreateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/networking/v2/extensions/bgpvpns/requests.go b/openstack/networking/v2/extensions/bgpvpns/requests.go
index 587961ede..374b4daf5 100644
--- a/openstack/networking/v2/extensions/bgpvpns/requests.go
+++ b/openstack/networking/v2/extensions/bgpvpns/requests.go
@@ -81,7 +81,7 @@ func (opts CreateOpts) ToBGPVPNCreateMap() (map[string]any, error) {
 }
 
 // Create a BGP VPN
-func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOpts) (r CreateResult) {
+func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) {
 	b, err := opts.ToBGPVPNCreateMap()
 	if err != nil {
 		r.Err = err
@@ -121,7 +121,7 @@ func (opts UpdateOpts) ToBGPVPNUpdateMap() (map[string]any, error) {
 }
 
 // Update accept a BGP VPN ID and an UpdateOpts and update the BGP VPN
-func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) {
+func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) {
 	b, err := opts.ToBGPVPNUpdateMap()
 	if err != nil {
 		r.Err = err
diff --git a/openstack/networking/v2/extensions/layer3/routers/requests.go b/openstack/networking/v2/extensions/layer3/routers/requests.go
index f6ca65484..5d31ec113 100644
--- a/openstack/networking/v2/extensions/layer3/routers/requests.go
+++ b/openstack/networking/v2/extensions/layer3/routers/requests.go
@@ -8,6 +8,12 @@ import (
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToRouterListQuery() (string, error)
+}
+
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the floating IP attributes you want to see returned. SortKey allows you to
@@ -33,18 +39,27 @@ type ListOpts struct {
 	RevisionNumber *int   `q:"revision_number"`
 }
 
+// ToRouterListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToRouterListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List returns a Pager which allows you to iterate over a collection of
 // routers. It accepts a ListOpts struct, which allows you to filter and sort
 // the returned collection for greater efficiency.
 //
 // Default policy settings return only those routers that are owned by the
 // tenant who submits the request, unless an admin user submits the request.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	q, err := gophercloud.BuildQueryString(&opts)
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	q, err := opts.ToRouterListQuery()
 	if err != nil {
 		return pagination.Pager{Err: err}
 	}
-	u := rootURL(c) + q.String()
+	u := rootURL(c) + q
 	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
 		return RouterPage{pagination.LinkedPageBase{PageResult: r}}
 	})
diff --git a/openstack/networking/v2/extensions/security/rules/requests.go b/openstack/networking/v2/extensions/security/rules/requests.go
index f70456d18..405cf549c 100644
--- a/openstack/networking/v2/extensions/security/rules/requests.go
+++ b/openstack/networking/v2/extensions/security/rules/requests.go
@@ -7,6 +7,12 @@ import (
 	"github.com/gophercloud/gophercloud/v2/pagination"
 )
 
+// ListOptsBuilder allows extensions to add additional parameters to the List
+// request.
+type ListOptsBuilder interface {
+	ToSecGroupListQuery() (string, error)
+}
+
 // ListOpts allows the filtering and sorting of paginated collections through
 // the API. Filtering is achieved by passing in struct field values that map to
 // the security group rule attributes you want to see returned. SortKey allows
@@ -32,15 +38,24 @@ type ListOpts struct {
 	RevisionNumber *int   `q:"revision_number"`
 }
 
+// ToSecGroupListQuery formats a ListOpts into a query string.
+func (opts ListOpts) ToSecGroupListQuery() (string, error) {
+	q, err := gophercloud.BuildQueryString(&opts)
+	if err != nil {
+		return "", err
+	}
+	return q.String(), nil
+}
+
 // List returns a Pager which allows you to iterate over a collection of
 // security group rules. It accepts a ListOpts struct, which allows you to filter
 // and sort the returned collection for greater efficiency.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
-	q, err := gophercloud.BuildQueryString(&opts)
+func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager {
+	q, err := opts.ToSecGroupListQuery()
 	if err != nil {
 		return pagination.Pager{Err: err}
 	}
-	u := rootURL(c) + q.String()
+	u := rootURL(c) + q
 	return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page {
 		return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}}
 	})
diff --git a/openstack/sharedfilesystems/v2/sharetransfers/requests.go b/openstack/sharedfilesystems/v2/sharetransfers/requests.go
index 642d2a36d..88b1950eb 100644
--- a/openstack/sharedfilesystems/v2/sharetransfers/requests.go
+++ b/openstack/sharedfilesystems/v2/sharetransfers/requests.go
@@ -42,6 +42,12 @@ func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateO
 	return
 }
 
+// AcceptOptsBuilder allows extensions to add additional parameters to the
+// Accept request.
+type AcceptOptsBuilder interface {
+	ToAcceptMap() (map[string]any, error)
+}
+
 // AcceptOpts contains options for a Share transfer accept reqeust.
 type AcceptOpts struct {
 	// The auth key of the share transfer to accept.
@@ -58,7 +64,7 @@ func (opts AcceptOpts) ToAcceptMap() (map[string]any, error) {
 }
 
 // Accept will accept a share tranfer request based on the values in AcceptOpts.
-func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOpts) (r AcceptResult) {
+func Accept(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AcceptOptsBuilder) (r AcceptResult) {
 	b, err := opts.ToAcceptMap()
 	if err != nil {
 		r.Err = err
diff --git a/script/acceptancetest b/script/acceptancetest
deleted file mode 100755
index 7e223bae5..000000000
--- a/script/acceptancetest
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/bash
-#
-# Run acceptance tests.
-
-# We intentionally don't set '-u' (error on unbound variables) or '-e' (exit on
-# first failure) initially since DevStack is not designed to run with these
-# flags and things crash and burn *spectacularly* 🔥🔥🔥
-set -xo pipefail
-
-# shellcheck disable=SC1091
-source "$(dirname "$0")/stackenv"
-
-# ...but we can do it after the fact
-set -eu
-
-timeout="60m"
-
-LOG_DIR=${LOG_DIR:-}
-if [[ -z "${LOG_DIR}" ]]; then
-    echo "LOG_DIR not set, will set a temp directory"
-    LOG_DIR=/tmp/devstack-logs
-fi
-mkdir -p "${LOG_DIR}"
-
-# shellcheck disable=SC2068
-go test -v -timeout $timeout -tags "fixtures acceptance" "${PACKAGE:-./internal/acceptance/openstack/...}" $@ |& tee -a "${LOG_DIR}/acceptance_tests.log"
diff --git a/script/coverage b/script/coverage
deleted file mode 100755
index ec5b2776b..000000000
--- a/script/coverage
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/bin/bash
-#
-# Run unit tests with coverage enabled.
-
-set -euxo pipefail
-
-n=1
-for testpkg in $(go list ./testing ./.../testing); do
-  covpkg="${testpkg/"/testing"/}"
-  go test -covermode count -coverprofile "testing_"$n.coverprofile -coverpkg "$covpkg" "$testpkg" 2>/dev/null
-  n=$((n+1))
-done
-
-base_pkg=$(go list)
-# Look for additional test files
-for path in $(find . -path '*/testing' -prune -o -path '*/internal' -prune -o -name '*_test.go' -exec dirname {} \; | uniq); do
-  pkg="${base_pkg}${path:1}"
-  go test -covermode count -coverprofile "testing_"$n.coverprofile -coverpkg "$pkg" "$pkg" 2>/dev/null
-  n=$((n+1))
-done
-
-# shellcheck disable=SC2046
-gocovmerge $(ls -- *.coverprofile) > cover.out
-rm ./*.coverprofile
diff --git a/script/format b/script/format
deleted file mode 100755
index 1bbf74ce3..000000000
--- a/script/format
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-#
-# Run 'go fmt' to warn about unformatted code
-
-set -euxo pipefail
-
-go fmt ./...
diff --git a/script/test b/script/test
deleted file mode 100755
index e71e7514b..000000000
--- a/script/test
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-#
-# Run all the tests.
-
-set -euxo pipefail
-
-# shellcheck disable=SC2068
-go test -v -tags 'acceptance fixtures' ./... $@
diff --git a/script/unittest b/script/unittest
deleted file mode 100755
index 1f1b1864f..000000000
--- a/script/unittest
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-#
-# Run the unit tests.
-
-set -euxo pipefail
-
-# Do extra rounds of testing to help identify reauth concurrency issues.
-# All other packages are tested in the `coverage` tests.
-# shellcheck disable=SC2068
-go test -v -race -count=5 ./testing $@
diff --git a/script/vet b/script/vet
deleted file mode 100755
index ecb74ef37..000000000
--- a/script/vet
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/bin/bash
-#
-# Run go vet.
-
-set -euxo pipefail
-
-export GOFLAGS="-tags=acceptance"
-go vet ./...



Test patch:
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
index b3ed1948e..21e5f7885 100644
--- a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
+++ b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
@@ -1,3 +1,5 @@
+//go:build acceptance || baremetal || portgroups
+
 package httpbasic
 
 import (
diff --git a/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go
index c4b95bba6..c3ed33cd6 100644
--- a/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go
+++ b/internal/acceptance/openstack/baremetal/noauth/portgroups_test.go
@@ -1,3 +1,5 @@
+//go:build acceptance || baremetal || ports
+
 package noauth
 
 import (
diff --git a/internal/acceptance/openstack/baremetal/v1/portgroups_test.go b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go
index 6f04223a0..968244c45 100644
--- a/internal/acceptance/openstack/baremetal/v1/portgroups_test.go
+++ b/internal/acceptance/openstack/baremetal/v1/portgroups_test.go
@@ -1,3 +1,5 @@
+//go:build acceptance || baremetal || portgroups
+
 package v1
 
 import (
diff --git a/internal/acceptance/openstack/client_test.go b/internal/acceptance/openstack/client_test.go
index 69e9a0936..3b252a285 100644
--- a/internal/acceptance/openstack/client_test.go
+++ b/internal/acceptance/openstack/client_test.go
@@ -36,14 +36,14 @@ func TestAuthenticatedClient(t *testing.T) {
 
 	t.Logf("Client successfully acquired a token: %v", client.TokenID)
 
-	// Find the storage service in the service catalog.
-	storage, err := openstack.NewObjectStorageV1(client, gophercloud.EndpointOpts{
+	// Find the compute service in the service catalog.
+	compute, err := openstack.NewComputeV2(client, gophercloud.EndpointOpts{
 		Region: os.Getenv("OS_REGION_NAME"),
 	})
 	if err != nil {
-		t.Errorf("Unable to locate a storage service: %v", err)
+		t.Errorf("Unable to locate a compute service: %v", err)
 	} else {
-		t.Logf("Located a storage service at endpoint: [%s]", storage.Endpoint)
+		t.Logf("Located a compute service at endpoint: [%s]", compute.Endpoint)
 	}
 }
 
diff --git a/openstack/baremetal/v1/portgroups/testing/fixtures.go b/openstack/baremetal/v1/portgroups/testing/fixtures.go
index 9a8773fd8..c1f61d367 100644
--- a/openstack/baremetal/v1/portgroups/testing/fixtures.go
+++ b/openstack/baremetal/v1/portgroups/testing/fixtures.go
@@ -205,7 +205,7 @@ func HandlePortGroupListSuccessfully(t *testing.T) {
 		switch marker {
 		case "":
 			// Return both portgroups.
-			fmt.Fprintf(w, PortGroupsListBody)
+			fmt.Fprint(w, PortGroupsListBody)
 		case "d2b42f0d-c7e6-4f08-b9bc-e8b23a6ee796":
 			// No portgroups remain.
 			fmt.Fprintf(w, `{ "portgroups": [] }`)
@@ -241,7 +241,7 @@ func HandlePortGroupCreationSuccessfully(t *testing.T, response string) {
 
 		w.WriteHeader(http.StatusAccepted)
 		w.Header().Add("Content-Type", "application/json")
-		fmt.Fprintf(w, response)
+		fmt.Fprint(w, response)
 	})
 }
 
@@ -266,6 +266,6 @@ func HandlePortGroupGetSuccessfully(t *testing.T) {
 			th.TestHeader(t, r, "Accept", "application/json")
 
 			w.Header().Add("Content-Type", "application/json")
-			fmt.Fprintf(w, SinglePortGroupBody)
+			fmt.Fprint(w, SinglePortGroupBody)
 		})
 }
diff --git a/openstack/containerinfra/v1/clustertemplates/testing/fixtures_test.go b/openstack/containerinfra/v1/clustertemplates/testing/fixtures_test.go
index f269d99cb..8028ff836 100644
--- a/openstack/containerinfra/v1/clustertemplates/testing/fixtures_test.go
+++ b/openstack/containerinfra/v1/clustertemplates/testing/fixtures_test.go
@@ -278,6 +278,28 @@ func HandleCreateClusterTemplateSuccessfully(t *testing.T) {
 	th.Mux.HandleFunc("/v1/clustertemplates", func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "POST")
 		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+		th.TestJSONRequest(t, r, `{
+                  "coe": "kubernetes",
+                  "dns_nameserver": "8.8.8.8",
+                  "docker_storage_driver": "devicemapper",
+                  "docker_volume_size": 3,
+                  "external_network_id": "public",
+                  "flavor_id": "m1.small",
+                  "hidden": true,
+                  "http_proxy": "http://10.164.177.169:8080",
+                  "https_proxy": "http://10.164.177.169:8080",
+                  "image_id": "Fedora-Atomic-27-20180212.2.x86_64",
+                  "keypair_id": "kp",
+                  "master_lb_enabled": true,
+                  "name": "kubernetes-dev",
+                  "network_driver": "flannel",
+                  "no_proxy": "10.0.0.0/8,172.0.0.0/8,192.0.0.0/8,localhost",
+                  "public": false,
+                  "registry_enabled": false,
+                  "server_type": "vm",
+                  "tls_disabled": false,
+                  "volume_driver": "cinder"
+                }`)
 
 		w.Header().Add("Content-Type", "application/json")
 		w.Header().Add("OpenStack-API-Minimum-Version", "container-infra 1.1")
diff --git a/openstack/loadbalancer/v2/monitors/testing/fixtures_test.go b/openstack/loadbalancer/v2/monitors/testing/fixtures_test.go
index 6b12d2c11..b56d44dbe 100644
--- a/openstack/loadbalancer/v2/monitors/testing/fixtures_test.go
+++ b/openstack/loadbalancer/v2/monitors/testing/fixtures_test.go
@@ -31,11 +31,13 @@ const HealthmonitorsListBody = `
 			"admin_state_up":true,
 			"project_id":"83657cfcdfe44cd5920adaf26c48ceea",
 			"delay":5,
+			"domain_name": "www.example.com",
 			"name":"db",
 			"expected_codes":"200",
 			"max_retries":2,
 			"max_retries_down":4,
 			"http_method":"GET",
+			"http_version": 1.1,
 			"timeout":2,
 			"url_path":"/",
 			"type":"HTTP",
@@ -54,11 +56,13 @@ const SingleHealthmonitorBody = `
 		"admin_state_up":true,
 		"project_id":"83657cfcdfe44cd5920adaf26c48ceea",
 		"delay":5,
+		"domain_name": "www.example.com",
 		"name":"db",
 		"expected_codes":"200",
 		"max_retries":2,
 		"max_retries_down":4,
 		"http_method":"GET",
+		"http_version": 1.1,
 		"timeout":2,
 		"url_path":"/",
 		"type":"HTTP",
@@ -76,11 +80,13 @@ const PostUpdateHealthmonitorBody = `
 		"admin_state_up":true,
 		"project_id":"83657cfcdfe44cd5920adaf26c48ceea",
 		"delay":3,
+		"domain_name": "www.example.com",
 		"name":"NewHealthmonitorName",
 		"expected_codes":"301",
 		"max_retries":10,
 		"max_retries_down":8,
 		"http_method":"GET",
+		"http_version": 1.1,
 		"timeout":20,
 		"url_path":"/another_check",
 		"type":"HTTP",
@@ -107,6 +113,7 @@ var (
 	}
 	HealthmonitorDb = monitors.Monitor{
 		AdminStateUp:   true,
+		DomainName:     "www.example.com",
 		Name:           "db",
 		ProjectID:      "83657cfcdfe44cd5920adaf26c48ceea",
 		Delay:          5,
@@ -117,12 +124,14 @@ var (
 		URLPath:        "/",
 		Type:           "HTTP",
 		HTTPMethod:     "GET",
+		HTTPVersion:    "1.1",
 		ID:             "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
 		Pools:          []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
 		Tags:           []string{},
 	}
 	HealthmonitorUpdated = monitors.Monitor{
 		AdminStateUp:   true,
+		DomainName:     "www.example.com",
 		Name:           "NewHealthmonitorName",
 		ProjectID:      "83657cfcdfe44cd5920adaf26c48ceea",
 		Delay:          3,
@@ -133,6 +142,7 @@ var (
 		URLPath:        "/another_check",
 		Type:           "HTTP",
 		HTTPMethod:     "GET",
+		HTTPVersion:    "1.1",
 		ID:             "5d4b5228-33b0-4e60-b225-9b727c1a20e7",
 		Pools:          []monitors.PoolID{{ID: "d459f7d8-c6ee-439d-8713-d3fc08aeed8d"}},
 		Tags:           []string{},
@@ -173,7 +183,9 @@ func HandleHealthmonitorCreationSuccessfully(t *testing.T, response string) {
 				"pool_id":"84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
 				"project_id":"453105b9-1754-413f-aab1-55f1af620750",
 				"delay":20,
+				"domain_name": "www.example.com",
 				"name":"db",
+				"http_version": 1.1,
 				"timeout":10,
 				"max_retries":5,
 				"max_retries_down":4,
diff --git a/openstack/loadbalancer/v2/monitors/testing/requests_test.go b/openstack/loadbalancer/v2/monitors/testing/requests_test.go
index 0ca13f360..1af0aaf9a 100644
--- a/openstack/loadbalancer/v2/monitors/testing/requests_test.go
+++ b/openstack/loadbalancer/v2/monitors/testing/requests_test.go
@@ -60,6 +60,7 @@ func TestCreateHealthmonitor(t *testing.T) {
 
 	actual, err := monitors.Create(context.TODO(), fake.ServiceClient(), monitors.CreateOpts{
 		Type:           "HTTP",
+		DomainName:     "www.example.com",
 		Name:           "db",
 		PoolID:         "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d",
 		ProjectID:      "453105b9-1754-413f-aab1-55f1af620750",
@@ -67,6 +68,7 @@ func TestCreateHealthmonitor(t *testing.T) {
 		Timeout:        10,
 		MaxRetries:     5,
 		MaxRetriesDown: 4,
+		HTTPVersion:    "1.1",
 		Tags:           []string{},
 		URLPath:        "/check",
 		ExpectedCodes:  "200-299",