Merge "update mcapi version" into main
diff --git a/doc/requirements.txt b/doc/requirements.txt
index a2a321f..07d4540 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -5,3 +5,4 @@
 sphinx
 sphinx-autobuild
 sphinx-copybutton
+sphinxcontrib-mermaid
diff --git a/doc/source/admin/index.rst b/doc/source/admin/index.rst
index b149543..e9b844f 100644
--- a/doc/source/admin/index.rst
+++ b/doc/source/admin/index.rst
@@ -19,3 +19,4 @@
    maintenance
    monitoring
    troubleshooting
+   upgrading
diff --git a/doc/source/admin/upgrading.rst b/doc/source/admin/upgrading.rst
new file mode 100644
index 0000000..b3c3fcf
--- /dev/null
+++ b/doc/source/admin/upgrading.rst
@@ -0,0 +1,80 @@
+#############
+Upgrade Guide
+#############
+
+This document shows the most common way of upgrading your Atmosphere deployment.
+
+.. admonition:: Avoid jumping Atmosphere major releases
+    :class: warning
+
+    It is important to avoid jumping major versions in Atmosphere, which is the
+    same advice in OpenStack.
+
+    For example, If you are running Atmosphere Zed release (version 1) and want
+    to move to Bobcat (version 3) you should perform 2 upgrades: version 1 to
+    version 2 and then version 3.
+
+    If you dont do this, you may face database inconsistencies and failures on
+    services like Nova or Neutron, or failed upgrades of components such as
+    RabbitMQ.
+
+**************************
+Preparing the environment
+**************************
+
+On the deployment box, or any other place that you have your Ansible inventory,
+you should update the ``requirements.yml`` file and point to the target
+Atmosphere release you want to upgrade to.
+
+.. code-block:: yaml
+
+  collections:
+  - name: vexxhost.atmosphere
+    version: X.Y.Z
+
+Once that is done, you should update your collections by running:
+
+.. code-block:: console
+
+    $ ansible-galaxy install -r requirements.yml --force
+
+It's important to review your inventory, specifically image overrides to make
+sure that the image overrides are still necessary, otherwise you may end up
+with a broken deployment since the images will not be the ones the Atmosphere
+collection expects.
+
+*******************
+Running the upgrade
+*******************
+
+You can either execute the entire upgrade by running your site-local playbook
+which imports ``vexxhost.atmosphere.site``, call the individual playbooks out
+of Atmosphere or run a specific tag if you want to upgrade service-by-service
+which gives you the most granular control.
+
+.. code-block:: console
+
+    $ ansible-playbook -i hosts.ini playbooks/site.yml
+
+You can also run the Atmosphere provided playbooks by pointing to a specific
+playbook of the Ansible collection, in this case, the Ceph playbook:
+
+.. code-block:: console
+
+    $ ansible-playbook -i hosts.ini vexxhost.atmosphere.ceph
+
+You also have the most granular control by running the tags of the playbooks,
+for example, if you want to upgrade the Keystone service, you can run the
+following command:
+
+.. code-block:: console
+
+    $ ansible-playbook -i hosts.ini vexxhost.atmosphere.openstack --tags keystone
+
+During the upgrade, you may find it useful to have a monitor on all of the pods
+in the cluster to ensure that they are becoming ready and not failing. You can
+do this by running the following command:
+
+.. code-block:: console
+
+    $ watch -n1 "kubectl get pods --all-namespaces -owide | egrep -v '(Completed|1/1|2/2|3/3|4/4|6/6|7/7)'"
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 239e656..82c0c07 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -20,9 +20,10 @@
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
 
 extensions = [
-    "sphinx_copybutton",
     "pbr.sphinxext",
     "reno.sphinxext",
+    "sphinx_copybutton",
+    "sphinxcontrib.mermaid",
 ]
 
 templates_path = ["_templates"]
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 9703ada..de86dca 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -28,6 +28,7 @@
    config/index
    deploy/index
    admin/index
+   user/index
    release-notes
 
 
diff --git a/doc/source/user/auth.rst b/doc/source/user/auth.rst
new file mode 100644
index 0000000..3afd6fd
--- /dev/null
+++ b/doc/source/user/auth.rst
@@ -0,0 +1,223 @@
+##############
+Authentication
+##############
+
+*******************************************
+Using external token from identity provider
+*******************************************
+
+Since OpenStack is configured to trust Keycloak as an identity provider, you will
+need to generate a token from Keycloak and use it to authenticate with the OpenStack
+API.
+
+If you are using Keycloak with an OpenID Connect (OIDC) identity provider, you
+may want to exchange a token generated from your identity provider for a token
+from Keycloak, which can then be used to authenticate with the OpenStack API.
+
+.. mermaid::
+   :align: center
+   :config: {"theme": "dark"}
+
+   sequenceDiagram
+       participant Client
+       participant OIDC Provider
+       participant Keycloak
+       participant Keystone
+       participant OpenStack
+
+       Client->>OIDC Provider: Request Token (Client Credentials)
+       OIDC Provider-->>Client: Returns OIDC Token
+
+       Client->>Keycloak: Exchange OIDC Token
+       Keycloak-->>Client: Returns Keycloak OIDC Token
+
+       Client->>Keystone: Authenticate with Keycloak Token
+       Keystone-->>Client: Returns Keystone Token
+
+       Client->>OpenStack: Use Keystone Token
+       OpenStack-->>Client: OpenStack API Access Granted
+
+In order to get started with this process, you'll need a OpenID connect token
+issued by an identity provider which exists in the Keycloak realm.
+
+1. **Exchange the OpenID connect (OIDC) Token with Keycloak**
+
+   Use the *OpenID connect token* from your identity provider and exchange it for
+   a *Keycloak-issued token*.  The following ``curl`` command is provided as an
+   example but you can use any tool that can make HTTP requests.
+
+   You will need to replace the following placeholders in the example code:
+
+   - ``<KEYCLOAK_URL>``: The URL of your Keycloak instance.
+   - ``<KEYCLOAK_CLIENT_ID>``: The client ID of the Keycloak client.
+   - ``<KEYCLOAK_CLIENT_SECRET>``: The client secret of the Keycloak client.
+
+   .. code-block:: sh
+
+      curl -X POST "https://<KEYCLOAK_URL>/realms/atmosphere/protocol/openid-connect/token" \
+        -H "Content-Type: application/x-www-form-urlencoded" \
+        -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
+        -d "client_id=<KEYCLOAK_CLIENT_ID>" \
+        -d "client_secret=<KEYCLOAK_CLIENT_SECRET>" \
+        -d "subject_token=<OIDC_TOKEN>" \
+        -d "subject_token_type=urn:ietf:params:oauth:token-type:access_token"
+
+   The response will return a token that is issued by Keycloak which you can use
+   to authenticate with the OpenStack API.
+
+   .. code-block:: json
+
+      {
+        "access_token" : ".....",
+        "refresh_token" : ".....",
+        "expires_in" : "...."
+      }
+
+
+2. **Authenticate with Keystone Using the Keycloak Token**
+
+   With the token issued by Keycloak, you can now authenticate with Keystone in order
+   to obtain a fernet token which can be used to talk to all of the OpenStack APIs.
+
+   You will need to replace the following placeholders in the example code:
+
+   - ``<OPENSTACK_AUTH_URL>``: The URL of your Keystone authentication endpoint.
+   - ``<KEYCLOAK_OIDC_TOKEN>``: The token issued by Keycloak.
+
+   .. code-block:: sh
+
+      curl "<OPENSTACK_AUTH_URL>/v3/OS-FEDERATION/identity_providers/atmosphere/protocols/openid/auth" \
+        -H "Authorization: Bearer <KEYCLOAK_OIDC_TOKEN>"
+
+   This response will return an unscoped Keystone token (not tied to any project) which
+   will be in the ``X-Subject-Token`` header.
+
+   .. code-block:: http
+
+      HTTP/1.1 201 Created
+      X-Subject-Token: <UNSCOPED_KEYSTONE_TOKEN>
+
+3. **List projects using the Keystone Token** (optional, if you already know the project ID)
+
+   At this point, you have an unscoped token issued by Keystone which is not bound
+   to any project.  You will need to exchange that token for a project-scoped token
+   in order to be able to interact with the OpenStack APIs.
+
+   You can choose to list what projects you have access to using the Keystone token
+   that you have obtained.
+
+   You will need to replace the following placeholders in the example code:
+
+   - ``<OPENSTACK_AUTH_URL>``: The URL of your Keystone authentication endpoint.
+   - ``<UNSCOPED_KEYSTONE_TOKEN>``: The token issued by Keystone.
+
+   .. code-block:: sh
+
+      curl "<OPENSTACK_AUTH_URL>/v3/projects" \
+        -H "X-Auth-Token: <UNSCOPED_KEYSTONE_TOKEN>"
+
+   This response will return a list of projects that you have access to.
+
+   .. code-block:: json
+
+      {
+        "projects": [
+          {
+            "id": "....",
+            "name": "....",
+            "description": "...."
+          }
+        ]
+      }
+
+4. **Exchange the unscoped token for a project-scoped token**
+
+   Once you have identified the project that you want to interact with, you can
+   exchange the unscoped token for a project-scoped token.
+
+   You will need to replace the following placeholders in the example code:
+
+   - ``<OPENSTACK_AUTH_URL>``: The URL of your Keystone authentication endpoint.
+   - ``<UNSCOPED_KEYSTONE_TOKEN>``: The token issued by Keystone.
+   - ``<PROJECT_ID>``: The ID of the project that you want to interact with.
+
+   .. code-block:: sh
+
+      curl "<OPENSTACK_AUTH_URL>/v3/auth/projects" \
+        -H "Content-Type: application/json" \
+        -H "X-Auth-Token: <UNSCOPED_KEYSTONE_TOKEN>" \
+        -d '{
+          "auth": {
+            "identity": {
+              "methods": ["token"],
+              "token": {
+                "id": "<UNSCOPED_KEYSTONE_TOKEN>"
+              }
+            },
+            "scope": {
+              "project": {
+                "id": "<PROJECT_ID>"
+              }
+            }
+          }
+        }'
+
+   This response will return a project-scoped token which you can use to interact
+   with the OpenStack APIs which will be in the ``X-Subject-Token`` header.
+
+   .. code-block:: http
+
+      HTTP/1.1 201 Created
+      X-Subject-Token: <SCOPED_KEYSTONE_TOKEN>
+
+   OpenStack Keystone will provide the token details in the response body, including
+   the full catalog of services that you have access to.
+
+   .. code-block:: json
+
+      {
+        "token": {
+          "methods": [
+            "token"
+          ],
+          "expires_at": "....",
+          "issued_at": "....",
+          "user": {
+            "domain": {
+              "id": "....",
+              "name": "...."
+            },
+            "id": "....",
+            "name": "...."
+          },
+          "audit_ids": [
+            "...."
+          ],
+          "catalog": [
+            {
+              "endpoints": [
+                {
+                  "id": "....",
+                  "interface": "....",
+                  "region": "....",
+                  "url": "...."
+                }
+              ],
+              "id": "....",
+              "name": "....",
+              "type": "...."
+            }
+          ],
+          "project": {
+            "domain": {
+              "id": "....",
+              "name": "...."
+            },
+            "id": "....",
+            "name": "...."
+          }
+        }
+      }
+
+   You can then use the project-scoped token to interact with the OpenStack APIs,
+   such as creating a server, listing servers, etc.
diff --git a/doc/source/user/index.rst b/doc/source/user/index.rst
new file mode 100644
index 0000000..9ed7b12
--- /dev/null
+++ b/doc/source/user/index.rst
@@ -0,0 +1,8 @@
+##########
+User Guide
+##########
+
+.. toctree::
+   :maxdepth: 2
+
+   auth
diff --git a/images/barbican/Dockerfile b/images/barbican/Dockerfile
index 8f0dfcb..1ccb14f 100644
--- a/images/barbican/Dockerfile
+++ b/images/barbican/Dockerfile
@@ -6,8 +6,8 @@
 ARG BARBICAN_GIT_REF=b5841df387e5ab38caf173950a1d98ab37a51453
 ADD --keep-git-dir=true https://opendev.org/openstack/barbican.git#${BARBICAN_GIT_REF} /src/barbican
 RUN git -C /src/barbican fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/barbican \
         pykmip
diff --git a/images/cinder/Dockerfile b/images/cinder/Dockerfile
index 6c384ff..274323a 100644
--- a/images/cinder/Dockerfile
+++ b/images/cinder/Dockerfile
@@ -8,8 +8,8 @@
 RUN git -C /src/cinder fetch --unshallow
 COPY patches/cinder /patches/cinder
 RUN git -C /src/cinder apply --verbose /patches/cinder/*
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/cinder \
         purestorage \
diff --git a/images/designate/Dockerfile b/images/designate/Dockerfile
index 72bf3d2..edd6b97 100644
--- a/images/designate/Dockerfile
+++ b/images/designate/Dockerfile
@@ -8,8 +8,8 @@
 RUN git -C /src/designate fetch --unshallow
 COPY patches/designate /patches/designate
 RUN git -C /src/designate apply --verbose /patches/designate/*
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/designate
 EOF
diff --git a/images/glance/Dockerfile b/images/glance/Dockerfile
index 1d2ad18..440f3bd 100644
--- a/images/glance/Dockerfile
+++ b/images/glance/Dockerfile
@@ -8,8 +8,8 @@
 RUN git -C /src/glance fetch --unshallow
 ADD --keep-git-dir=true https://opendev.org/openstack/glance_store.git#stable/2024.2 /src/glance_store
 RUN git -C /src/glance_store fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/glance \
         /src/glance_store[cinder] \
diff --git a/images/heat/Dockerfile b/images/heat/Dockerfile
index 72493d7..a2c6ccb 100644
--- a/images/heat/Dockerfile
+++ b/images/heat/Dockerfile
@@ -6,8 +6,8 @@
 ARG HEAT_GIT_REF=64bdbb9bc66c38760989dd7bb2574ccc14069872
 ADD --keep-git-dir=true https://opendev.org/openstack/heat.git#${HEAT_GIT_REF} /src/heat
 RUN git -C /src/heat fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/heat
 EOF
diff --git a/images/horizon/Dockerfile b/images/horizon/Dockerfile
index 62cb2b3..1d0ff5b 100644
--- a/images/horizon/Dockerfile
+++ b/images/horizon/Dockerfile
@@ -25,8 +25,8 @@
 RUN git -C /src/horizon apply --verbose /patches/horizon/*
 COPY patches/magnum-ui /patches/magnum-ui
 RUN git -C /src/magnum-ui apply --verbose /patches/magnum-ui/*
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/designate-dashboard \
         /src/heat-dashboard \
diff --git a/images/ironic/Dockerfile b/images/ironic/Dockerfile
index 5f81ed0..2047e27 100644
--- a/images/ironic/Dockerfile
+++ b/images/ironic/Dockerfile
@@ -6,8 +6,8 @@
 ARG IRONIC_GIT_REF=5aa51d6985d25acd6abfb161c62c66facc20a6ca
 ADD --keep-git-dir=true https://opendev.org/openstack/ironic.git#${IRONIC_GIT_REF} /src/ironic
 RUN git -C /src/ironic fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/ironic \
         python-dracclient \
diff --git a/images/keystone/Dockerfile b/images/keystone/Dockerfile
index 084869f..abb2d99 100644
--- a/images/keystone/Dockerfile
+++ b/images/keystone/Dockerfile
@@ -6,8 +6,8 @@
 ARG KEYSTONE_GIT_REF=47891f4ae8fd7876e5a7657f58c32c371feeddc3
 ADD --keep-git-dir=true https://opendev.org/openstack/keystone.git#${KEYSTONE_GIT_REF} /src/keystone
 RUN git -C /src/keystone fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/keystone[ldap] \
         keystone-keycloak-backend==0.2.0
diff --git a/images/magnum/Dockerfile b/images/magnum/Dockerfile
index c60d80f..b6abbe4 100644
--- a/images/magnum/Dockerfile
+++ b/images/magnum/Dockerfile
@@ -14,8 +14,8 @@
 ARG MAGNUM_GIT_REF=db197e08a09da93062fc4222180051dadfc0f0d8
 ADD --keep-git-dir=true https://opendev.org/openstack/magnum.git#${MAGNUM_GIT_REF} /src/magnum
 RUN git -C /src/magnum fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/magnum \
         magnum-cluster-api==0.26.0
diff --git a/images/manila/Dockerfile b/images/manila/Dockerfile
index 52ac86e..9a11c8f 100644
--- a/images/manila/Dockerfile
+++ b/images/manila/Dockerfile
@@ -6,8 +6,8 @@
 ARG MANILA_GIT_REF=09f3ab0a229362c00bb55f704cfeae43bccd3c8d
 ADD --keep-git-dir=true https://opendev.org/openstack/manila.git#${MANILA_GIT_REF} /src/manila
 RUN git -C /src/manila fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/manila
 EOF
diff --git a/images/neutron/Dockerfile b/images/neutron/Dockerfile
index 854ad0a..8745625 100644
--- a/images/neutron/Dockerfile
+++ b/images/neutron/Dockerfile
@@ -6,6 +6,8 @@
 ARG NEUTRON_GIT_REF=804d6006e3f09c214d6de8a3f23de70c44f1d51d
 ADD --keep-git-dir=true https://opendev.org/openstack/neutron.git#${NEUTRON_GIT_REF} /src/neutron
 RUN git -C /src/neutron fetch --unshallow
+COPY patches/neutron /patches/neutron
+RUN git -C /src/neutron apply --verbose /patches/neutron/*
 ARG NEUTRON_VPNAAS_GIT_REF=990e478b1e6db459b6cb9aec53ce808e2957bb65
 ADD --keep-git-dir=true https://opendev.org/openstack/neutron-vpnaas.git#${NEUTRON_VPNAAS_GIT_REF} /src/neutron-vpnaas
 RUN git -C /src/neutron-vpnaas fetch --unshallow
@@ -18,8 +20,8 @@
 ARG LOG_PASER_GIT_REF=9bc923c1294864ec709c538ba5c309065ef710d5
 ADD --keep-git-dir=true https://github.com/vexxhost/neutron-ovn-network-logging-parser.git#${LOG_PASER_GIT_REF} /src/neutron-ovn-network-logging-parser
 RUN git -C /src/neutron-ovn-network-logging-parser fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/neutron \
         /src/neutron-vpnaas \
diff --git a/images/neutron/patches/neutron/0001-fix-ovn-set-mtu-in-external_ids-correctly.patch b/images/neutron/patches/neutron/0001-fix-ovn-set-mtu-in-external_ids-correctly.patch
new file mode 100644
index 0000000..f27bfe6
--- /dev/null
+++ b/images/neutron/patches/neutron/0001-fix-ovn-set-mtu-in-external_ids-correctly.patch
@@ -0,0 +1,39 @@
+From cabb48e339a1f646b4a820d41eaef5c34e678708 Mon Sep 17 00:00:00 2001
+From: Mohammed Naser <mnaser@vexxhost.com>
+Date: Fri, 21 Feb 2025 15:20:18 -0500
+Subject: [PATCH] Add MTU for all non-external ports
+
+In the previous fix, it had brought the MTU lookup within the
+if branch if the vnic_type was VNIC_REMOTELY_MANAGED which
+meant that it didn't get added for anything other than that
+typer of interface.
+
+Since the MTU is used further later, we want to capture it
+because it goes into the OvnPortInfo which will go into the
+external_ids which will be captured by the metadata agent
+to fix the correct MTU.
+
+Closes-Bug: #2053274
+Related-Change-Id: I7ff300e9634e5e3fc68d70540392109fd8b9babc
+Change-Id: Ib4ee7e0e8b13141739f5a8600333be856afd4a4e
+---
+
+diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py
+index aa1c989..f18b558 100644
+--- a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py
++++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/ovn_client.py
+@@ -473,11 +473,11 @@
+         # HA Chassis Group will bind the port to the highest
+         # priority Chassis
+         if port_type != ovn_const.LSP_TYPE_EXTERNAL:
++            port_net = self._plugin.get_network(context, port['network_id'])
++            mtu = str(port_net['mtu'])
++
+             if (bp_info.vnic_type == portbindings.VNIC_REMOTE_MANAGED and
+                     ovn_const.VIF_DETAILS_PF_MAC_ADDRESS in bp_info.bp_param):
+-                port_net = self._plugin.get_network(
+-                    context, port['network_id'])
+-                mtu = str(port_net['mtu'])
+                 options.update({
+                     ovn_const.LSP_OPTIONS_VIF_PLUG_TYPE_KEY: 'representor',
+                     ovn_const.LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY: mtu,
diff --git a/images/nova/Dockerfile b/images/nova/Dockerfile
index 497d9f2..f96dcdf 100644
--- a/images/nova/Dockerfile
+++ b/images/nova/Dockerfile
@@ -9,8 +9,8 @@
 ARG SCHEDULER_FILTERS_GIT_REF=77ed1c2ca70f4166a6d0995c7d3d90822f0ca6c0
 ADD --keep-git-dir=true https://github.com/vexxhost/nova-scheduler-filters.git#${SCHEDULER_FILTERS_GIT_REF} /src/nova-scheduler-filters
 RUN git -C /src/nova-scheduler-filters fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/nova \
         /src/nova-scheduler-filters \
diff --git a/images/octavia/Dockerfile b/images/octavia/Dockerfile
index ef9a147..9160156 100644
--- a/images/octavia/Dockerfile
+++ b/images/octavia/Dockerfile
@@ -8,8 +8,8 @@
 RUN git -C /src/octavia fetch --unshallow
 ADD --keep-git-dir=true https://opendev.org/openstack/ovn-octavia-provider.git#stable/2024.2 /src/ovn-octavia-provider
 RUN git -C /src/ovn-octavia-provider fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/octavia \
         /src/ovn-octavia-provider
diff --git a/images/openstack-venv-builder/Dockerfile b/images/openstack-venv-builder/Dockerfile
index 70bab69..54a100d 100644
--- a/images/openstack-venv-builder/Dockerfile
+++ b/images/openstack-venv-builder/Dockerfile
@@ -24,18 +24,19 @@
     lsb-release \
     openssh-client \
     python3 \
-    python3-dev \
-    python3-pip \
-    python3-venv
+    python3-dev
 apt-get clean
 rm -rf /var/lib/apt/lists/*
 EOF
-RUN python3 -m venv --upgrade-deps --system-site-packages /var/lib/openstack
+COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
 COPY --from=requirements --link /upper-constraints.txt /upper-constraints.txt
-RUN pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv venv --system-site-packages /var/lib/openstack
+uv pip install \
     --constraint /upper-constraints.txt \
         cryptography \
         pymysql \
         python-binary-memcached \
         python-memcached \
         uwsgi
+EOF
diff --git a/images/placement/Dockerfile b/images/placement/Dockerfile
index 1ce55c8..b2529f2 100644
--- a/images/placement/Dockerfile
+++ b/images/placement/Dockerfile
@@ -6,8 +6,8 @@
 ARG PLACEMENT_GIT_REF=828b2559a1b3c0b59c543e851c6ea3efb1baae20
 ADD --keep-git-dir=true https://opendev.org/openstack/placement.git#${PLACEMENT_GIT_REF} /src/placement
 RUN git -C /src/placement fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/placement
 EOF
diff --git a/images/python-openstackclient/Dockerfile b/images/python-openstackclient/Dockerfile
index abafa29..b940b3b 100644
--- a/images/python-openstackclient/Dockerfile
+++ b/images/python-openstackclient/Dockerfile
@@ -3,8 +3,8 @@
 # Atmosphere-Rebuild-Time: 2024-06-25T22:49:25Z
 
 FROM openstack-venv-builder AS build
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         osc-placement \
         python-barbicanclient \
diff --git a/images/staffeln/Dockerfile b/images/staffeln/Dockerfile
index 0f63af1..991ea96 100644
--- a/images/staffeln/Dockerfile
+++ b/images/staffeln/Dockerfile
@@ -6,8 +6,8 @@
 ARG STAFFELN_GIT_REF=v2.2.3
 ADD --keep-git-dir=true https://github.com/vexxhost/staffeln.git#${STAFFELN_GIT_REF} /src/staffeln
 RUN git -C /src/staffeln fetch --unshallow
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/staffeln
 EOF
diff --git a/images/tempest/Dockerfile b/images/tempest/Dockerfile
index 4edec8e..dc558d9 100644
--- a/images/tempest/Dockerfile
+++ b/images/tempest/Dockerfile
@@ -26,8 +26,8 @@
 ADD --keep-git-dir=true https://opendev.org/openstack/octavia-tempest-plugin.git#master /src/octavia-tempest-plugin
 RUN git -C /src/octavia-tempest-plugin fetch --unshallow
 ADD https://releases.openstack.org/constraints/upper/master /upper-constraints.txt
-RUN --mount=type=cache,mode=0755,target=/root/.cache/pip,sharing=private <<EOF bash -xe
-pip3 install \
+RUN --mount=type=cache,target=/root/.cache/uv <<EOF bash -xe
+uv pip install \
     --constraint /upper-constraints.txt \
         /src/tempest \
         /src/barbican-tempest-plugin \
diff --git a/releasenotes/notes/add-keycloak-token-exchange-283b38032dda9baf.yaml b/releasenotes/notes/add-keycloak-token-exchange-283b38032dda9baf.yaml
new file mode 100644
index 0000000..f3d7c4a
--- /dev/null
+++ b/releasenotes/notes/add-keycloak-token-exchange-283b38032dda9baf.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - Keycloak is now configured to have the ``token-exchange`` and the
+    ``admin-fine-grained-authz`` features enabled to allow for use of the
+    `OAuth Token Exchange <https://www.keycloak.org/securing-apps/token-exchange>`_
+    protocol.
diff --git a/releasenotes/notes/add-upgrade-docs-1w3fr5tgvdf678iuy.yaml b/releasenotes/notes/add-upgrade-docs-1w3fr5tgvdf678iuy.yaml
new file mode 100644
index 0000000..616320f
--- /dev/null
+++ b/releasenotes/notes/add-upgrade-docs-1w3fr5tgvdf678iuy.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - |
+    Adding basic Atmosphere upgrade process.
diff --git a/releasenotes/notes/fix-ovn-mtu-d33352771a65e744.yaml b/releasenotes/notes/fix-ovn-mtu-d33352771a65e744.yaml
new file mode 100644
index 0000000..2a27a9f
--- /dev/null
+++ b/releasenotes/notes/fix-ovn-mtu-d33352771a65e744.yaml
@@ -0,0 +1,6 @@
+---
+issues:
+  - The MTU for the metadata interfaces for OVN was not being set correctly,
+    leading to a mismatch between the MTU of the metadata interface and the
+    MTU of the network.  This has been fixed with a Neutron change to ensure
+    the ``neutron:mtu`` value in ``external_ids`` is set correctly.
diff --git a/releasenotes/notes/use-uv-for-venv-ec400e1d456a8c5f.yaml b/releasenotes/notes/use-uv-for-venv-ec400e1d456a8c5f.yaml
new file mode 100644
index 0000000..b8ff8a8
--- /dev/null
+++ b/releasenotes/notes/use-uv-for-venv-ec400e1d456a8c5f.yaml
@@ -0,0 +1,4 @@
+---
+other:
+  - The images now use the `uv` tool to create the virtual environment which
+    is faster and more reliable than the previous method.
diff --git a/roles/keycloak/vars/main.yml b/roles/keycloak/vars/main.yml
index 41ee1ee..ead80fb 100644
--- a/roles/keycloak/vars/main.yml
+++ b/roles/keycloak/vars/main.yml
@@ -26,6 +26,8 @@
   #                        we have to define jdbc connection string explicitly along side
   #                        `externalDatabase` helm values.
   extraEnvVars:
+    - name: KC_FEATURES
+      value: "token-exchange,admin-fine-grained-authz"
     - name: KC_PROXY
       value: edge
     - name: KC_DB