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 490bbe686..6f2ddb2a6 100644
--- a/.github/workflows/functional-baremetal.yaml
+++ b/.github/workflows/functional-baremetal.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**baremetal**'
+      - '.github/workflows/functional-baremetal.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -31,7 +32,7 @@ jobs:
       - name: Work around broken dnsmasq
         run: sudo apt-get purge -y dnsmasq-base
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-basic.yaml b/.github/workflows/functional-basic.yaml
index b18ac67ce..6037748d0 100644
--- a/.github/workflows/functional-basic.yaml
+++ b/.github/workflows/functional-basic.yaml
@@ -32,7 +32,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           enabled_services: 's-account,s-container,s-object,s-proxy,${{ matrix.additional_services }}'
diff --git a/.github/workflows/functional-blockstorage.yaml b/.github/workflows/functional-blockstorage.yaml
index 7ef9173ef..27d841112 100644
--- a/.github/workflows/functional-blockstorage.yaml
+++ b/.github/workflows/functional-blockstorage.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**blockstorage**'
+      - '.github/workflows/functional-blockstorage.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-compute.yaml b/.github/workflows/functional-compute.yaml
index 5bd6eb8de..53d388afa 100644
--- a/.github/workflows/functional-compute.yaml
+++ b/.github/workflows/functional-compute.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**compute**'
+      - '.github/workflows/functional-compute.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-containerinfra.yaml b/.github/workflows/functional-containerinfra.yaml
index 2ec3d0228..508522dbd 100644
--- a/.github/workflows/functional-containerinfra.yaml
+++ b/.github/workflows/functional-containerinfra.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**containerinfra**'
+      - '.github/workflows/functional-containerinfra.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -44,7 +45,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-dns.yaml b/.github/workflows/functional-dns.yaml
index 810bd0b54..3d45c9a85 100644
--- a/.github/workflows/functional-dns.yaml
+++ b/.github/workflows/functional-dns.yaml
@@ -2,8 +2,8 @@ name: functional-dns
 on:
   pull_request:
     paths:
-      - '**openstack/dns**'
-      - '**functional-dns.yaml'
+      - '**dns**'
+      - '.github/workflows/functional-dns.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -36,7 +36,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-fwaas_v2.yaml b/.github/workflows/functional-fwaas_v2.yaml
index d12e98094..ce6eca319 100644
--- a/.github/workflows/functional-fwaas_v2.yaml
+++ b/.github/workflows/functional-fwaas_v2.yaml
@@ -1,8 +1,13 @@
+# TODO(stephenfin): neutron-fwaas may support OVN now. If so, we can combine
+# this job with the functional-networking job. See [1]
+#
+# [1] https://bugs.launchpad.net/neutron/+bug/1971958
 name: functional-fwaas_v2
 on:
   pull_request:
     paths:
       - '**networking/v2/extensions/fwaas_v2**'
+      - '.github/workflows/functional-fwaas_v2.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -28,8 +33,16 @@ jobs:
     steps:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
+      - name: Create additional neutron policies
+        run: |
+          mkdir /tmp/neutron-policies
+          cat << EOF >> /tmp/neutron-policies/port_binding.yaml
+          ---
+          "create_port:binding:profile": "rule:admin_only or rule:service_api"
+          "update_port:binding:profile": "rule:admin_only or rule:service_api"
+          EOF
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
@@ -39,6 +52,10 @@ jobs:
             Q_ML2_PLUGIN_TYPE_DRIVERS=flat,gre,vlan,vxlan
             Q_ML2_TENANT_NETWORK_TYPE=vxlan
             Q_TUNNEL_TYPES=vxlan,gre
+
+            [[post-config|\$NEUTRON_CONF]]
+            [oslo_policy]
+            policy_dirs = /tmp/neutron-policies
           enabled_services: 'q-svc,q-agt,q-dhcp,q-l3,q-meta,q-fwaas-v2,-cinder,-horizon,-tempest,-swift,-c-sch,-c-api,-c-vol,-c-bak,-ovn,-ovn-controller,-ovn-northd,-q-ovn-metadata-agent,${{ matrix.additional_services }}'
       - name: Checkout go
         uses: actions/setup-go@v5
diff --git a/.github/workflows/functional-identity.yaml b/.github/workflows/functional-identity.yaml
index 96e5df55b..e8d29aa4e 100644
--- a/.github/workflows/functional-identity.yaml
+++ b/.github/workflows/functional-identity.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**identity**'
+      - '.github/workflows/functional-identity.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           enabled_services: "${{ matrix.additional_services }}"
diff --git a/.github/workflows/functional-image.yaml b/.github/workflows/functional-image.yaml
index 5ca33ef50..8dc1f1fb2 100644
--- a/.github/workflows/functional-image.yaml
+++ b/.github/workflows/functional-image.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**image**'
+      - '.github/workflows/functional-image.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           enabled_services: "${{ matrix.additional_services }}"
diff --git a/.github/workflows/functional-keymanager.yaml b/.github/workflows/functional-keymanager.yaml
index 823136b73..a9a2867e2 100644
--- a/.github/workflows/functional-keymanager.yaml
+++ b/.github/workflows/functional-keymanager.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**keymanager**'
+      - '.github/workflows/functional-keymanager.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -35,7 +36,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-loadbalancer.yaml b/.github/workflows/functional-loadbalancer.yaml
index bae7266b3..228b05217 100644
--- a/.github/workflows/functional-loadbalancer.yaml
+++ b/.github/workflows/functional-loadbalancer.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**loadbalancer**'
+      - '.github/workflows/functional-loadbalancer.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -35,7 +36,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-messaging.yaml b/.github/workflows/functional-messaging.yaml
index 46f0ddf88..92b9890c5 100644
--- a/.github/workflows/functional-messaging.yaml
+++ b/.github/workflows/functional-messaging.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**messaging**'
+      - '.github/workflows/functional-messaging.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-networking.yaml b/.github/workflows/functional-networking.yaml
index 97b0e1ffb..8c6ff3bc9 100644
--- a/.github/workflows/functional-networking.yaml
+++ b/.github/workflows/functional-networking.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**networking**'
+      - '.github/workflows/functional-networking.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -37,7 +38,7 @@ jobs:
           "update_port:binding:profile": "rule:admin_only or rule:service_api"
           EOF
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-objectstorage.yaml b/.github/workflows/functional-objectstorage.yaml
index 5dd46bbea..985683985 100644
--- a/.github/workflows/functional-objectstorage.yaml
+++ b/.github/workflows/functional-objectstorage.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**objectstorage**'
+      - '.github/workflows/functional-objectstorage.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-orchestration.yaml b/.github/workflows/functional-orchestration.yaml
index afd0e654c..9cecd0dda 100644
--- a/.github/workflows/functional-orchestration.yaml
+++ b/.github/workflows/functional-orchestration.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**orchestration**'
+      - '.github/workflows/functional-orchestration.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-placement.yaml b/.github/workflows/functional-placement.yaml
index c32a89981..5eb80fbe1 100644
--- a/.github/workflows/functional-placement.yaml
+++ b/.github/workflows/functional-placement.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**placement**'
+      - '.github/workflows/functional-placement.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -29,7 +30,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           enabled_services: "${{ matrix.additional_services }}"
diff --git a/.github/workflows/functional-sharedfilesystems.yaml b/.github/workflows/functional-sharedfilesystems.yaml
index 308049c12..32d0c5a89 100644
--- a/.github/workflows/functional-sharedfilesystems.yaml
+++ b/.github/workflows/functional-sharedfilesystems.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**sharedfilesystems**'
+      - '.github/workflows/functional-sharedfilesystems.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -35,7 +36,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/.github/workflows/functional-workflow.yaml b/.github/workflows/functional-workflow.yaml
index f8af427e9..1af47db79 100644
--- a/.github/workflows/functional-workflow.yaml
+++ b/.github/workflows/functional-workflow.yaml
@@ -3,6 +3,7 @@ on:
   pull_request:
     paths:
       - '**workflow**'
+      - '.github/workflows/functional-workflow.yaml'
   schedule:
     - cron: '0 0 */3 * *'
 jobs:
@@ -32,7 +33,7 @@ jobs:
       - name: Checkout Gophercloud
         uses: actions/checkout@v4
       - name: Deploy devstack
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
diff --git a/internal/acceptance/clients/conditions.go b/internal/acceptance/clients/conditions.go
index c48ac208a..6077f4bde 100644
--- a/internal/acceptance/clients/conditions.go
+++ b/internal/acceptance/clients/conditions.go
@@ -14,14 +14,6 @@ func RequiredSystemScope(t *testing.T) {
 	}
 }
 
-// RequireManilaReplicas will restrict a test to only be run with enabled
-// manila replicas.
-func RequireManilaReplicas(t *testing.T) {
-	if os.Getenv("OS_MANILA_REPLICAS") != "true" {
-		t.Skip("manila replicas must be enabled to run this test")
-	}
-}
-
 // RequireAdmin will restrict a test to only be run by admin users.
 func RequireAdmin(t *testing.T) {
 	if os.Getenv("OS_USERNAME") != "admin" {
@@ -36,38 +28,6 @@ func RequireNonAdmin(t *testing.T) {
 	}
 }
 
-// RequirePortForwarding will restrict a test to only be run in environments
-// that support port forwarding
-func RequirePortForwarding(t *testing.T) {
-	if os.Getenv("OS_PORTFORWARDING_ENVIRONMENT") == "" {
-		t.Skip("this test requires support for port forwarding")
-	}
-}
-
-// RequireGuestAgent will restrict a test to only be run in
-// environments that support the QEMU guest agent.
-func RequireGuestAgent(t *testing.T) {
-	if os.Getenv("OS_GUEST_AGENT") == "" {
-		t.Skip("this test requires support for qemu guest agent and to set OS_GUEST_AGENT to 1")
-	}
-}
-
-// RequireIdentityV2 will restrict a test to only be run in
-// environments that support the Identity V2 API.
-func RequireIdentityV2(t *testing.T) {
-	if os.Getenv("OS_IDENTITY_API_VERSION") != "2.0" {
-		t.Skip("this test requires support for the identity v2 API")
-	}
-}
-
-// RequireLiveMigration will restrict a test to only be run in
-// environments that support live migration.
-func RequireLiveMigration(t *testing.T) {
-	if os.Getenv("OS_LIVE_MIGRATE") == "" {
-		t.Skip("this test requires support for live migration and to set OS_LIVE_MIGRATE to 1")
-	}
-}
-
 // RequireLong will ensure long-running tests can run.
 func RequireLong(t *testing.T) {
 	if testing.Short() {
@@ -75,38 +35,6 @@ func RequireLong(t *testing.T) {
 	}
 }
 
-// RequireNovaNetwork will restrict a test to only be run in
-// environments that support nova-network.
-func RequireNovaNetwork(t *testing.T) {
-	if os.Getenv("OS_NOVANET") == "" {
-		t.Skip("this test requires nova-network and to set OS_NOVANET to 1")
-	}
-}
-
-// RequireCinderNoAuth will restrict a test to be only run in environments that
-// have Cinder using noauth.
-func RequireCinderNoAuth(t *testing.T) {
-	if os.Getenv("CINDER_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" {
-		t.Skip("this test requires Cinder using noauth, set OS_USERNAME and CINDER_ENDPOINT")
-	}
-}
-
-// RequireIronicNoAuth will restrict a test to be only run in environments that
-// have Ironic using noauth.
-func RequireIronicNoAuth(t *testing.T) {
-	if os.Getenv("IRONIC_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" {
-		t.Skip("this test requires IRONIC using noauth, set OS_USERNAME and IRONIC_ENDPOINT")
-	}
-}
-
-// RequireIronicHTTPBasic will restrict a test to be only run in environments
-// that have Ironic using http_basic.
-func RequireIronicHTTPBasic(t *testing.T) {
-	if os.Getenv("IRONIC_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" || os.Getenv("OS_PASSWORD") == "" {
-		t.Skip("this test requires Ironic using http_basic, set OS_USERNAME, OS_PASSWORD and IRONIC_ENDPOINT")
-	}
-}
-
 func getReleaseFromEnv(t *testing.T) string {
 	current := strings.TrimPrefix(os.Getenv("OS_BRANCH"), "stable/")
 	if current == "" {
@@ -115,8 +43,8 @@ func getReleaseFromEnv(t *testing.T) string {
 	return current
 }
 
-// SkipRelease will have the test be skipped on a certain
-// release. Releases are named such as 'stable/mitaka', master, etc.
+// SkipRelease will have the test be skipped on a certain release.
+// Releases are named such as 'stable/dalmatian', master, etc.
 func SkipRelease(t *testing.T, release string) {
 	current := getReleaseFromEnv(t)
 	if current == strings.TrimPrefix(release, "stable/") {
@@ -124,8 +52,8 @@ func SkipRelease(t *testing.T, release string) {
 	}
 }
 
-// SkipReleasesBelow will have the test be skipped on releases below a certain
-// one. Releases are named such as 'stable/mitaka', master, etc.
+// SkipReleasesBelow will have the test be skipped on releases below a certain one.
+// Releases are named such as 'stable/dalmatian', master, etc.
 func SkipReleasesBelow(t *testing.T, release string) {
 	current := getReleaseFromEnv(t)
 
@@ -134,9 +62,9 @@ func SkipReleasesBelow(t *testing.T, release string) {
 	}
 }
 
-// SkipReleasesAbove will have the test be skipped on releases above a certain
-// one. The test is always skipped on master release. Releases are named such
-// as 'stable/mitaka', master, etc.
+// SkipReleasesAbove will have the test be skipped on releases above a certain one.
+// The test is always skipped on master release.
+// Releases are named such as 'stable/dalmatian', master, etc.
 func SkipReleasesAbove(t *testing.T, release string) {
 	current := getReleaseFromEnv(t)
 
@@ -150,9 +78,9 @@ func isReleaseNumeral(release string) bool {
 	return err == nil
 }
 
-// IsCurrentAbove will return true on releases above a certain
-// one. The result is always true on master release. Releases are named such
-// as 'stable/mitaka', master, etc.
+// IsCurrentAbove will return true on releases above a certain one.
+// The result is always true on master release.
+// Releases are named such as 'stable/dalmatian', master, etc.
 func IsCurrentAbove(t *testing.T, release string) bool {
 	current := getReleaseFromEnv(t)
 	release = strings.TrimPrefix(release, "stable/")
@@ -174,8 +102,9 @@ func IsCurrentAbove(t *testing.T, release string) bool {
 	return false
 }
 
-// IsCurrentBelow will return true on releases below a certain
-// one. Releases are named such as 'stable/mitaka', master, etc.
+// IsCurrentBelow will return true on releases below a certain one.
+// The result is always false on master release.
+// Releases are named such as 'stable/dalmatian', master, etc.
 func IsCurrentBelow(t *testing.T, release string) bool {
 	current := getReleaseFromEnv(t)
 	release = strings.TrimPrefix(release, "stable/")
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/conditions.go b/internal/acceptance/openstack/baremetal/httpbasic/conditions.go
new file mode 100644
index 000000000..15d1c9f72
--- /dev/null
+++ b/internal/acceptance/openstack/baremetal/httpbasic/conditions.go
@@ -0,0 +1,14 @@
+package httpbasic
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireIronicHTTPBasic will restrict a test to be only run in environments
+// that have Ironic using http_basic.
+func RequireIronicHTTPBasic(t *testing.T) {
+	if os.Getenv("IRONIC_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" || os.Getenv("OS_PASSWORD") == "" {
+		t.Skip("this test requires Ironic using http_basic, set OS_USERNAME, OS_PASSWORD and IRONIC_ENDPOINT")
+	}
+}
diff --git a/internal/acceptance/openstack/baremetal/noauth/conditions.go b/internal/acceptance/openstack/baremetal/noauth/conditions.go
new file mode 100644
index 000000000..762f141f5
--- /dev/null
+++ b/internal/acceptance/openstack/baremetal/noauth/conditions.go
@@ -0,0 +1,15 @@
+package noauth
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireIronicNoAuth will restrict a test to be only run in environments that
+
+// have Ironic using noauth.
+func RequireIronicNoAuth(t *testing.T) {
+	if os.Getenv("IRONIC_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" {
+		t.Skip("this test requires IRONIC using noauth, set OS_USERNAME and IRONIC_ENDPOINT")
+	}
+}
diff --git a/internal/acceptance/openstack/blockstorage/noauth/conditions.go b/internal/acceptance/openstack/blockstorage/noauth/conditions.go
new file mode 100644
index 000000000..b1fd42e3b
--- /dev/null
+++ b/internal/acceptance/openstack/blockstorage/noauth/conditions.go
@@ -0,0 +1,14 @@
+package noauth
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireCinderNoAuth will restrict a test to be only run in environments that
+// have Cinder using noauth.
+func RequireCinderNoAuth(t *testing.T) {
+	if os.Getenv("CINDER_ENDPOINT") == "" || os.Getenv("OS_USERNAME") == "" {
+		t.Skip("this test requires Cinder using noauth, set OS_USERNAME and CINDER_ENDPOINT")
+	}
+}
diff --git a/internal/acceptance/openstack/compute/v2/conditions.go b/internal/acceptance/openstack/compute/v2/conditions.go
new file mode 100644
index 000000000..565a3e1cf
--- /dev/null
+++ b/internal/acceptance/openstack/compute/v2/conditions.go
@@ -0,0 +1,22 @@
+package v2
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireGuestAgent will restrict a test to only be run in
+// environments that support the QEMU guest agent.
+func RequireGuestAgent(t *testing.T) {
+	if os.Getenv("OS_GUEST_AGENT") == "" {
+		t.Skip("this test requires support for qemu guest agent and to set OS_GUEST_AGENT to 1")
+	}
+}
+
+// RequireLiveMigration will restrict a test to only be run in
+// environments that support live migration.
+func RequireLiveMigration(t *testing.T) {
+	if os.Getenv("OS_LIVE_MIGRATE") == "" {
+		t.Skip("this test requires support for live migration and to set OS_LIVE_MIGRATE to 1")
+	}
+}
diff --git a/internal/acceptance/openstack/identity/v2/conditions.go b/internal/acceptance/openstack/identity/v2/conditions.go
new file mode 100644
index 000000000..9871afaf3
--- /dev/null
+++ b/internal/acceptance/openstack/identity/v2/conditions.go
@@ -0,0 +1,14 @@
+package v2
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireIdentityV2 will restrict a test to only be run in
+// environments that support the Identity V2 API.
+func RequireIdentityV2(t *testing.T) {
+	if os.Getenv("OS_IDENTITY_API_VERSION") != "2.0" {
+		t.Skip("this test requires support for the identity v2 API")
+	}
+}
diff --git a/internal/acceptance/openstack/networking/v2/conditions.go b/internal/acceptance/openstack/networking/v2/conditions.go
index 036cba604..6ce6b145f 100644
--- a/internal/acceptance/openstack/networking/v2/conditions.go
+++ b/internal/acceptance/openstack/networking/v2/conditions.go
@@ -2,6 +2,7 @@ package v2
 
 import (
 	"context"
+	"os"
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2"
@@ -16,3 +17,11 @@ func RequireNeutronExtension(t *testing.T, client *gophercloud.ServiceClient, ex
 		t.Skipf("this test requires %s Neutron extension", extension)
 	}
 }
+
+// RequirePortForwarding will restrict a test to only be run in environments
+// that support port forwarding
+func RequirePortForwarding(t *testing.T) {
+	if os.Getenv("OS_PORTFORWARDING_ENVIRONMENT") == "" {
+		t.Skip("this test requires support for port forwarding")
+	}
+}
diff --git a/internal/acceptance/openstack/sharedfilesystems/v2/conditions.go b/internal/acceptance/openstack/sharedfilesystems/v2/conditions.go
new file mode 100644
index 000000000..de48b6939
--- /dev/null
+++ b/internal/acceptance/openstack/sharedfilesystems/v2/conditions.go
@@ -0,0 +1,14 @@
+package v2
+
+import (
+	"os"
+	"testing"
+)
+
+// RequireManilaReplicas will restrict a test to only be run with enabled
+// manila replicas.
+func RequireManilaReplicas(t *testing.T) {
+	if os.Getenv("OS_MANILA_REPLICAS") != "true" {
+		t.Skip("manila replicas must be enabled to run this test")
+	}
+}



Test patch:
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/allocations_test.go b/internal/acceptance/openstack/baremetal/httpbasic/allocations_test.go
index 932c067ad..faa667d05 100644
--- a/internal/acceptance/openstack/baremetal/httpbasic/allocations_test.go
+++ b/internal/acceptance/openstack/baremetal/httpbasic/allocations_test.go
@@ -15,7 +15,7 @@ import (
 
 func TestAllocationsCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/nodes_test.go b/internal/acceptance/openstack/baremetal/httpbasic/nodes_test.go
index e35ae1e25..a0c656d7f 100644
--- a/internal/acceptance/openstack/baremetal/httpbasic/nodes_test.go
+++ b/internal/acceptance/openstack/baremetal/httpbasic/nodes_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestNodesCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
@@ -49,7 +49,7 @@ func TestNodesCreateDestroy(t *testing.T) {
 
 func TestNodesUpdate(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
@@ -74,7 +74,7 @@ func TestNodesUpdate(t *testing.T) {
 func TestNodesRAIDConfig(t *testing.T) {
 	clients.SkipReleasesBelow(t, "stable/ussuri")
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
@@ -104,7 +104,7 @@ func TestNodesRAIDConfig(t *testing.T) {
 func TestNodesFirmwareInterface(t *testing.T) {
 	clients.SkipReleasesBelow(t, "stable/2023.2")
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
index 21e5f7885..8db6da007 100644
--- a/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
+++ b/internal/acceptance/openstack/baremetal/httpbasic/portgroups_test.go
@@ -15,7 +15,7 @@ import (
 
 func TestPortGroupsCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/httpbasic/ports_test.go b/internal/acceptance/openstack/baremetal/httpbasic/ports_test.go
index 9912c4932..7773f8b23 100644
--- a/internal/acceptance/openstack/baremetal/httpbasic/ports_test.go
+++ b/internal/acceptance/openstack/baremetal/httpbasic/ports_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestPortsCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
@@ -52,7 +52,7 @@ func TestPortsCreateDestroy(t *testing.T) {
 
 func TestPortsUpdate(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicHTTPBasic(t)
+	RequireIronicHTTPBasic(t)
 
 	client, err := clients.NewBareMetalV1HTTPBasic()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/noauth/allocations_test.go b/internal/acceptance/openstack/baremetal/noauth/allocations_test.go
index e6cb33ac3..d0b34737c 100644
--- a/internal/acceptance/openstack/baremetal/noauth/allocations_test.go
+++ b/internal/acceptance/openstack/baremetal/noauth/allocations_test.go
@@ -15,7 +15,7 @@ import (
 
 func TestAllocationsCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/noauth/nodes_test.go b/internal/acceptance/openstack/baremetal/noauth/nodes_test.go
index bd05203b2..64b9826a0 100644
--- a/internal/acceptance/openstack/baremetal/noauth/nodes_test.go
+++ b/internal/acceptance/openstack/baremetal/noauth/nodes_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestNodesCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
@@ -49,7 +49,7 @@ func TestNodesCreateDestroy(t *testing.T) {
 
 func TestNodesUpdate(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
@@ -74,7 +74,7 @@ func TestNodesUpdate(t *testing.T) {
 func TestNodesRAIDConfig(t *testing.T) {
 	clients.SkipReleasesBelow(t, "stable/ussuri")
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/baremetal/noauth/ports_test.go b/internal/acceptance/openstack/baremetal/noauth/ports_test.go
index 980b2dec3..3f10b3b85 100644
--- a/internal/acceptance/openstack/baremetal/noauth/ports_test.go
+++ b/internal/acceptance/openstack/baremetal/noauth/ports_test.go
@@ -16,7 +16,7 @@ import (
 
 func TestPortsCreateDestroy(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
@@ -52,7 +52,7 @@ func TestPortsCreateDestroy(t *testing.T) {
 
 func TestPortsUpdate(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireIronicNoAuth(t)
+	RequireIronicNoAuth(t)
 
 	client, err := clients.NewBareMetalV1NoAuthClient()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/blockstorage/noauth/snapshots_test.go b/internal/acceptance/openstack/blockstorage/noauth/snapshots_test.go
index 5b8bb2a98..f1daa352e 100644
--- a/internal/acceptance/openstack/blockstorage/noauth/snapshots_test.go
+++ b/internal/acceptance/openstack/blockstorage/noauth/snapshots_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestSnapshotsList(t *testing.T) {
-	clients.RequireCinderNoAuth(t)
+	RequireCinderNoAuth(t)
 
 	client, err := clients.NewBlockStorageV3NoAuthClient()
 	if err != nil {
@@ -35,7 +35,7 @@ func TestSnapshotsList(t *testing.T) {
 }
 
 func TestSnapshotsCreateDelete(t *testing.T) {
-	clients.RequireCinderNoAuth(t)
+	RequireCinderNoAuth(t)
 
 	client, err := clients.NewBlockStorageV3NoAuthClient()
 	if err != nil {
diff --git a/internal/acceptance/openstack/blockstorage/noauth/volumes_test.go b/internal/acceptance/openstack/blockstorage/noauth/volumes_test.go
index 0c993adda..15d481542 100644
--- a/internal/acceptance/openstack/blockstorage/noauth/volumes_test.go
+++ b/internal/acceptance/openstack/blockstorage/noauth/volumes_test.go
@@ -12,7 +12,7 @@ import (
 )
 
 func TestVolumesList(t *testing.T) {
-	clients.RequireCinderNoAuth(t)
+	RequireCinderNoAuth(t)
 
 	client, err := clients.NewBlockStorageV3NoAuthClient()
 	if err != nil {
@@ -35,7 +35,7 @@ func TestVolumesList(t *testing.T) {
 }
 
 func TestVolumesCreateDestroy(t *testing.T) {
-	clients.RequireCinderNoAuth(t)
+	RequireCinderNoAuth(t)
 
 	client, err := clients.NewBlockStorageV3NoAuthClient()
 	if err != nil {
diff --git a/internal/acceptance/openstack/compute/v2/migrate_test.go b/internal/acceptance/openstack/compute/v2/migrate_test.go
index f22d6e67c..6db845c0b 100644
--- a/internal/acceptance/openstack/compute/v2/migrate_test.go
+++ b/internal/acceptance/openstack/compute/v2/migrate_test.go
@@ -31,7 +31,7 @@ func TestMigrate(t *testing.T) {
 func TestLiveMigrate(t *testing.T) {
 	clients.RequireLong(t)
 	clients.RequireAdmin(t)
-	clients.RequireLiveMigration(t)
+	RequireLiveMigration(t)
 
 	client, err := clients.NewComputeV2Client()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/compute/v2/servers_test.go b/internal/acceptance/openstack/compute/v2/servers_test.go
index 67ef63028..7f769e6e9 100644
--- a/internal/acceptance/openstack/compute/v2/servers_test.go
+++ b/internal/acceptance/openstack/compute/v2/servers_test.go
@@ -234,7 +234,7 @@ func TestServersMetadata(t *testing.T) {
 
 func TestServersActionChangeAdminPassword(t *testing.T) {
 	clients.RequireLong(t)
-	clients.RequireGuestAgent(t)
+	RequireGuestAgent(t)
 
 	client, err := clients.NewComputeV2Client()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/identity/v2/extension_test.go b/internal/acceptance/openstack/identity/v2/extension_test.go
index ada4ddef4..f43523340 100644
--- a/internal/acceptance/openstack/identity/v2/extension_test.go
+++ b/internal/acceptance/openstack/identity/v2/extension_test.go
@@ -13,7 +13,7 @@ import (
 )
 
 func TestExtensionsList(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2Client()
@@ -37,7 +37,7 @@ func TestExtensionsList(t *testing.T) {
 }
 
 func TestExtensionsGet(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2Client()
diff --git a/internal/acceptance/openstack/identity/v2/role_test.go b/internal/acceptance/openstack/identity/v2/role_test.go
index 2eed42a3c..49635f7f9 100644
--- a/internal/acceptance/openstack/identity/v2/role_test.go
+++ b/internal/acceptance/openstack/identity/v2/role_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 func TestRolesAddToUser(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2AdminClient()
@@ -53,7 +53,7 @@ func TestRolesAddToUser(t *testing.T) {
 }
 
 func TestRolesList(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2AdminClient()
diff --git a/internal/acceptance/openstack/identity/v2/tenant_test.go b/internal/acceptance/openstack/identity/v2/tenant_test.go
index d390f14d3..2c7092e91 100644
--- a/internal/acceptance/openstack/identity/v2/tenant_test.go
+++ b/internal/acceptance/openstack/identity/v2/tenant_test.go
@@ -13,7 +13,7 @@ import (
 )
 
 func TestTenantsList(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2Client()
@@ -38,7 +38,7 @@ func TestTenantsList(t *testing.T) {
 }
 
 func TestTenantsCRUD(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2AdminClient()
diff --git a/internal/acceptance/openstack/identity/v2/token_test.go b/internal/acceptance/openstack/identity/v2/token_test.go
index fba2dbc0a..61d93f218 100644
--- a/internal/acceptance/openstack/identity/v2/token_test.go
+++ b/internal/acceptance/openstack/identity/v2/token_test.go
@@ -14,7 +14,7 @@ import (
 )
 
 func TestTokenAuthenticate(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2UnauthenticatedClient()
@@ -38,7 +38,7 @@ func TestTokenAuthenticate(t *testing.T) {
 }
 
 func TestTokenValidate(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2Client()
diff --git a/internal/acceptance/openstack/identity/v2/user_test.go b/internal/acceptance/openstack/identity/v2/user_test.go
index f54e218d3..e0f614e51 100644
--- a/internal/acceptance/openstack/identity/v2/user_test.go
+++ b/internal/acceptance/openstack/identity/v2/user_test.go
@@ -13,7 +13,7 @@ import (
 )
 
 func TestUsersList(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2AdminClient()
@@ -38,7 +38,7 @@ func TestUsersList(t *testing.T) {
 }
 
 func TestUsersCreateUpdateDelete(t *testing.T) {
-	clients.RequireIdentityV2(t)
+	RequireIdentityV2(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewIdentityV2AdminClient()
diff --git a/internal/acceptance/openstack/networking/v2/extensions/agents/agents_test.go b/internal/acceptance/openstack/networking/v2/extensions/agents/agents_test.go
index 37b5e39c3..068ea001c 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/agents/agents_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/agents/agents_test.go
@@ -16,8 +16,8 @@ import (
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
 )
 
-func TestAgentsRUD(t *testing.T) {
-	t.Skip("TestAgentsRUD needs to be re-worked to work with both ML2/OVS and OVN")
+func TestAgentsCRUD(t *testing.T) {
+	t.Skip("TestAgentsCRUD needs to be re-worked to work with both ML2/OVS and OVN")
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewNetworkV2Client()
@@ -96,13 +96,16 @@ func TestAgentsRUD(t *testing.T) {
 	th.AssertNoErr(t, err)
 }
 
-func TestBGPAgentRUD(t *testing.T) {
+func TestBGPAgentCRUD(t *testing.T) {
 	timeout := 15 * time.Minute
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgp")
+
 	// List BGP Agents
 	listOpts := &agents.ListOpts{
 		AgentType: "BGP Dynamic Routing Agent",
diff --git a/internal/acceptance/openstack/networking/v2/extensions/bgp/peers/bgppeers_test.go b/internal/acceptance/openstack/networking/v2/extensions/bgp/peers/bgppeers_test.go
index 8fdee3c03..baad9a8a5 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/bgp/peers/bgppeers_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/bgp/peers/bgppeers_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/bgp/peers"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -18,6 +19,9 @@ func TestBGPPeerCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgp")
+
 	// Create a BGP Peer
 	bgpPeerCreated, err := CreateBGPPeer(t, client)
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/bgp/speakers/bgpspeakers_test.go b/internal/acceptance/openstack/networking/v2/extensions/bgp/speakers/bgpspeakers_test.go
index 5b50d6df5..2ab5584d5 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/bgp/speakers/bgpspeakers_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/bgp/speakers/bgpspeakers_test.go
@@ -17,9 +17,13 @@ import (
 
 func TestBGPSpeakerCRUD(t *testing.T) {
 	clients.RequireAdmin(t)
+
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgp")
+
 	// Create a BGP Speaker
 	bgpSpeaker, err := CreateBGPSpeaker(t, client)
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/bgpvpns/bgpvpns_test.go b/internal/acceptance/openstack/networking/v2/extensions/bgpvpns/bgpvpns_test.go
index 01542d6ec..3c915901e 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/bgpvpns/bgpvpns_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/bgpvpns/bgpvpns_test.go
@@ -20,6 +20,9 @@ func TestBGPVPNCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgpvpn")
+
 	// Create a BGP VPN
 	bgpVpnCreated, err := CreateBGPVPN(t, client)
 	th.AssertNoErr(t, err)
@@ -60,12 +63,15 @@ func TestBGPVPNCRUD(t *testing.T) {
 	t.Logf("BGP VPN %s deleted", bgpVpnUpdated.Name)
 }
 
-func TestBGPVPNNetworkAssociationCRD(t *testing.T) {
+func TestBGPVPNNetworkAssociationCRUD(t *testing.T) {
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgpvpn")
+
 	// Create a BGP VPN
 	bgpVpnCreated, err := CreateBGPVPN(t, client)
 	th.AssertNoErr(t, err)
@@ -117,6 +123,9 @@ func TestBGPVPNRouterAssociationCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgpvpn")
+
 	// Create a BGP VPN
 	bgpVpnCreated, err := CreateBGPVPN(t, client)
 	th.AssertNoErr(t, err)
@@ -182,6 +191,9 @@ func TestBGPVPNPortAssociationCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "bgpvpn")
+
 	// Create a BGP VPN
 	bgpVpnCreated, err := CreateBGPVPN(t, client)
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/dns/dns_test.go b/internal/acceptance/openstack/networking/v2/extensions/dns/dns_test.go
index d709a4252..fd239f8a4 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/dns/dns_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/dns/dns_test.go
@@ -141,8 +141,8 @@ func TestDNSPortCRUDL(t *testing.T) {
 	th.AssertDeepEquals(t, newPort, getNewPort)
 }
 
-func TestDNSFloatingIPCRDL(t *testing.T) {
-	t.Skip("Skipping TestDNSFloatingIPCRDL for now, as it doesn't work with ML2/OVN.")
+func TestDNSFloatingIPCRUD(t *testing.T) {
+	t.Skip("Skipping TestDNSFloatingIPCRUD for now, as it doesn't work with ML2/OVN.")
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewNetworkV2Client()
diff --git a/internal/acceptance/openstack/networking/v2/extensions/layer3/portforwardings_test.go b/internal/acceptance/openstack/networking/v2/extensions/layer3/portforwardings_test.go
index 3d3f20852..d6f3e9595 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/layer3/portforwardings_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/layer3/portforwardings_test.go
@@ -15,7 +15,7 @@ import (
 )
 
 func TestLayer3PortForwardingsCreateDelete(t *testing.T) {
-	clients.RequirePortForwarding(t)
+	networking.RequirePortForwarding(t)
 
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/mtu/mtu_test.go b/internal/acceptance/openstack/networking/v2/extensions/mtu/mtu_test.go
index a71c96cc4..63e4f7790 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/mtu/mtu_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/mtu/mtu_test.go
@@ -16,7 +16,7 @@ import (
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
 )
 
-func TestMTUNetworkCRUDL(t *testing.T) {
+func TestMTUNetworkCRUD(t *testing.T) {
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewNetworkV2Client()
diff --git a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go
index 8fe5e0638..862324736 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/group_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/vpnaas/endpointgroups"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -16,6 +17,9 @@ func TestGroupList(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	allPages, err := endpointgroups.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
 
@@ -31,6 +35,9 @@ func TestGroupCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	group, err := CreateEndpointGroup(t, client)
 	th.AssertNoErr(t, err)
 	defer DeleteEndpointGroup(t, client, group.ID)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go
index c0407c99a..eeb573edf 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ikepolicy_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/vpnaas/ikepolicies"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -16,6 +17,9 @@ func TestIKEPolicyList(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	allPages, err := ikepolicies.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
 
@@ -31,6 +35,9 @@ func TestIKEPolicyCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	policy, err := CreateIKEPolicy(t, client)
 	th.AssertNoErr(t, err)
 	defer DeleteIKEPolicy(t, client, policy.ID)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go
index f9aed9b2a..74dc5a375 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/ipsecpolicy_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/vpnaas/ipsecpolicies"
 	th "github.com/gophercloud/gophercloud/v2/testhelper"
@@ -16,6 +17,9 @@ func TestIPSecPolicyList(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	allPages, err := ipsecpolicies.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
 
@@ -31,6 +35,9 @@ func TestIPSecPolicyCRUD(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	policy, err := CreateIPSecPolicy(t, client)
 	th.AssertNoErr(t, err)
 	defer DeleteIPSecPolicy(t, client, policy.ID)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go
index 2ac1c7734..b932dbdeb 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/service_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	layer3 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2/extensions/layer3"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/vpnaas/services"
@@ -17,6 +18,9 @@ func TestServiceList(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	allPages, err := services.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
 
@@ -29,10 +33,15 @@ func TestServiceList(t *testing.T) {
 }
 
 func TestServiceCRUD(t *testing.T) {
+	// TODO(stephenfin): Why are we skipping this? Can we unskip? If not, we should remove.
 	clients.SkipReleasesAbove(t, "stable/wallaby")
+
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	router, err := layer3.CreateExternalRouter(t, client)
 	th.AssertNoErr(t, err)
 	defer layer3.DeleteRouter(t, client, router.ID)
diff --git a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go
index 8aa8e49f5..68a7f4519 100644
--- a/internal/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go
+++ b/internal/acceptance/openstack/networking/v2/extensions/vpnaas/siteconnection_test.go
@@ -7,7 +7,7 @@ import (
 	"testing"
 
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/clients"
-	networks "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
+	networking "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2"
 	layer3 "github.com/gophercloud/gophercloud/v2/internal/acceptance/openstack/networking/v2/extensions/layer3"
 	"github.com/gophercloud/gophercloud/v2/internal/acceptance/tools"
 	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
@@ -19,6 +19,9 @@ func TestConnectionList(t *testing.T) {
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	allPages, err := siteconnections.List(client, nil).AllPages(context.TODO())
 	th.AssertNoErr(t, err)
 
@@ -31,19 +34,24 @@ func TestConnectionList(t *testing.T) {
 }
 
 func TestConnectionCRUD(t *testing.T) {
+	// TODO(stephenfin): Why are we skipping this? Can we unskip? If not, we should remove.
 	clients.SkipReleasesAbove(t, "stable/wallaby")
+
 	client, err := clients.NewNetworkV2Client()
 	th.AssertNoErr(t, err)
 
+	// Skip these tests if we don't have the required extension
+	networking.RequireNeutronExtension(t, client, "vpnaas")
+
 	// Create Network
-	network, err := networks.CreateNetwork(t, client)
+	network, err := networking.CreateNetwork(t, client)
 	th.AssertNoErr(t, err)
-	defer networks.DeleteNetwork(t, client, network.ID)
+	defer networking.DeleteNetwork(t, client, network.ID)
 
 	// Create Subnet
-	subnet, err := networks.CreateSubnet(t, client, network.ID)
+	subnet, err := networking.CreateSubnet(t, client, network.ID)
 	th.AssertNoErr(t, err)
-	defer networks.DeleteSubnet(t, client, subnet.ID)
+	defer networking.DeleteSubnet(t, client, subnet.ID)
 
 	router, err := layer3.CreateExternalRouter(t, client)
 	th.AssertNoErr(t, err)
diff --git a/internal/acceptance/openstack/sharedfilesystems/v2/replicas_test.go b/internal/acceptance/openstack/sharedfilesystems/v2/replicas_test.go
index 41913ac26..8432afeb7 100644
--- a/internal/acceptance/openstack/sharedfilesystems/v2/replicas_test.go
+++ b/internal/acceptance/openstack/sharedfilesystems/v2/replicas_test.go
@@ -18,7 +18,7 @@ import (
 const replicasPathMicroversion = "2.56"
 
 func TestReplicaCreate(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()
 	if err != nil {
@@ -55,7 +55,7 @@ func TestReplicaCreate(t *testing.T) {
 }
 
 func TestReplicaPromote(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()
 	if err != nil {
@@ -131,7 +131,7 @@ func TestReplicaPromote(t *testing.T) {
 }
 
 func TestReplicaExportLocations(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()
 	if err != nil {
@@ -198,7 +198,7 @@ func TestReplicaExportLocations(t *testing.T) {
 }
 
 func TestReplicaListDetail(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()
 	if err != nil {
@@ -231,7 +231,7 @@ func TestReplicaListDetail(t *testing.T) {
 }
 
 func TestReplicaResetStatus(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()
 	if err != nil {
@@ -272,7 +272,7 @@ func TestReplicaResetStatus(t *testing.T) {
 
 // This test available only for cloud admins
 func TestReplicaForceDelete(t *testing.T) {
-	clients.RequireManilaReplicas(t)
+	RequireManilaReplicas(t)
 	clients.RequireAdmin(t)
 
 	client, err := clients.NewSharedFileSystemV2Client()