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 df5e4c88d..eb04f8c92 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:
@@ -28,18 +29,10 @@ 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
-        uses: gophercloud@devstack-action@v0.17
+        uses: gophercloud/devstack-action@v0.17
         with:
           branch: ${{ matrix.openstack_version }}
           conf_overrides: |
@@ -82,10 +75,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 }}
           USE_SYSTEM_SCOPE: true
       - name: Generate logs on failure
diff --git a/.github/workflows/functional-basic.yaml b/.github/workflows/functional-basic.yaml
index 6c3587c35..80808ac18 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 }}'
@@ -41,10 +41,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 f94bef265..1a36b0da0 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: |
@@ -40,10 +41,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 b26c82b8d..87f0ef7e6 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: |
@@ -40,10 +41,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 59da6bf1d..ad3c275b6 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: |
@@ -61,10 +62,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 cbf27b04f..43e8123cc 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: |
@@ -49,10 +49,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 880fa8994..cb84f9f46 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,16 +52,21 @@ 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
         with:
           go-version: '^1.22'
       - 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 9c58e24d5..d3cb23f02 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 }}"
@@ -38,10 +39,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 74164ae7e..26579dd4e 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 }}"
@@ -38,10 +39,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 bcdbe5c61..099939e5a 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: |
@@ -48,10 +49,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 142a30d12..6b720aeca 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: |
@@ -49,10 +50,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 f47e6f620..0ddb5f1ef 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: |
@@ -41,10 +42,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 7127596ff..a0608a2b9 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: |
@@ -55,10 +56,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 9b98a32d3..b22f5ef45 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: |
@@ -44,10 +45,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 111a1c1a8..a301e4568 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: |
@@ -40,10 +41,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 261bf7420..c6356e95e 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 }}"
@@ -38,10 +39,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 75704987d..08aca0f0e 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: |
@@ -62,10 +63,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 376cc181e..d4d389581 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: |
@@ -43,10 +44,11 @@ jobs:
         with:
           go-version: '^1.22'
       - 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 0ae498baf..abab3a523 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.22.3"
           - "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/alexfalkowski/gocovmerge@v1.4.0
-
       - 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/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/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")
+	}
+}
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/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/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/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/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 5934805ba..0c5d03449 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()