Merge "Fix incorrect state in Loadbalancer monitoring" into main
diff --git a/.github/workflows/manila.yml b/.github/workflows/manila.yml
index f43339d..6d00a97 100644
--- a/.github/workflows/manila.yml
+++ b/.github/workflows/manila.yml
@@ -32,7 +32,7 @@
     runs-on: ubuntu-latest
     steps:
       - name: Checkout project
-        uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
@@ -44,7 +44,8 @@
           pipx inject python-swiftclient python-keystoneclient
 
       - name: Cache DIB_IMAGE_CACHE
-        uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4
+        id: cache-dib-image-cache
+        uses: actions/cache/restore@v4
         with:
           path: ~/.cache/image-create
           key: dib-image-cache
@@ -52,6 +53,13 @@
       - name: Build image
         run: tox -ebuild-manila-image
 
+      - name: Save DIB_IMAGE_CACHE
+        id: dib-image-cache-save
+        uses: actions/cache/save@v4
+        with:
+          path: ~/.cache/image-create
+          key: ${{ steps.cache-dib-image-cache.outputs.cache-primary-key }}
+
       - name: Publish image
         if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'release'
         run: |
diff --git a/Cargo.lock b/Cargo.lock
index 6c32272..91bb852 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -118,7 +118,14 @@
 name = "atmosphere"
 version = "0.0.0"
 dependencies = [
+ "git2",
+ "git2-hooks",
+ "indoc",
+ "md5",
+ "regex",
+ "reqwest",
  "rustainers",
+ "serde",
  "tokio",
 ]
 
@@ -146,7 +153,7 @@
  "miniz_oxide",
  "object",
  "rustc-demangle",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -240,6 +247,8 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
 dependencies = [
+ "jobserver",
+ "libc",
  "shlex",
 ]
 
@@ -261,7 +270,7 @@
  "num-traits",
  "serde",
  "wasm-bindgen",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -347,6 +356,27 @@
 ]
 
 [[package]]
+name = "dirs"
+version = "5.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
 name = "displaydoc"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -570,7 +600,7 @@
  "cfg-if",
  "libc",
  "wasi 0.13.3+wasi-0.2.2",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -580,6 +610,33 @@
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
 [[package]]
+name = "git2"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fda788993cc341f69012feba8bf45c0ba4f3291fcc08e214b4d5a7332d88aff"
+dependencies = [
+ "bitflags",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
+name = "git2-hooks"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f930f5fe956eb55418c0da7e40736636e3fe80b577d8b60d9e54da0fc038619"
+dependencies = [
+ "git2",
+ "log",
+ "shellexpand",
+ "thiserror 1.0.69",
+]
+
+[[package]]
 name = "gitea-sdk"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1007,6 +1064,12 @@
 ]
 
 [[package]]
+name = "indoc"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+
+[[package]]
 name = "ipnet"
 version = "2.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1035,6 +1098,15 @@
 checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
 [[package]]
+name = "jobserver"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
 name = "js-sys"
 version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1066,6 +1138,20 @@
 checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
 
 [[package]]
+name = "libgit2-sys"
+version = "0.18.0+1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1a117465e7e1597e8febea8bb0c410f1c7fb93b1e1cddf34363f8390367ffec"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
 name = "libredox"
 version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1077,6 +1163,32 @@
 ]
 
 [[package]]
+name = "libssh2-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
 name = "linux-raw-sys"
 version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1095,6 +1207,12 @@
 checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
 
 [[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
 name = "memchr"
 version = "2.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1362,6 +1480,12 @@
 ]
 
 [[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
 name = "ovsinit"
 version = "0.1.0"
 dependencies = [
@@ -1520,6 +1644,17 @@
 ]
 
 [[package]]
+name = "redox_users"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
+dependencies = [
+ "getrandom 0.2.15",
+ "libredox",
+ "thiserror 1.0.69",
+]
+
+[[package]]
 name = "regex"
 version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1778,18 +1913,18 @@
 
 [[package]]
 name = "serde"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.217"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1859,6 +1994,15 @@
 ]
 
 [[package]]
+name = "shellexpand"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
 name = "shlex"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2436,7 +2580,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2447,7 +2591,7 @@
 dependencies = [
  "windows-result",
  "windows-strings",
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2456,7 +2600,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2466,7 +2610,16 @@
 checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
 dependencies = [
  "windows-result",
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
@@ -2475,7 +2628,7 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -2484,7 +2637,22 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
 ]
 
 [[package]]
@@ -2493,30 +2661,48 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
  "windows_i686_gnullvm",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
 ]
 
 [[package]]
 name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -2529,24 +2715,48 @@
 
 [[package]]
 name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index d874e76..2e10d47 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,9 +2,18 @@
 name = "atmosphere"
 edition = "2021"
 
+[dependencies]
+git2 = "0.20.0"
+git2-hooks = "0.4.0"
+indoc = "2.0.5"
+md5 = "0.7.0"
+regex = "1.11.1"
+reqwest = { version = "0.12.12", features = ["json", "native-tls-vendored"] }
+serde = { version = "1.0.218", features = ["derive"] }
+tokio = { version = "1.43.0", features = ["fs", "macros", "rt-multi-thread"] }
+
 [dev-dependencies]
 rustainers = { path = "crates/rustainers" }
-tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
 
 [workspace]
 members = [ "crates/*" ]
diff --git a/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl b/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
index c15e40a..909622a 100644
--- a/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+++ b/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
@@ -196,6 +196,12 @@
   while IFS= read -r nic; do
     local port_name=$(get_dpdk_config_value ${nic} '.name')
     local pci_id=$(get_dpdk_config_value ${nic} '.pci_id')
+    local iface=$(get_dpdk_config_value ${nic} '.iface')
+    if [ -n ${iface} ] && [ -z ${pci_id} ]; then
+      local pci_id=$(get_address_by_nicname ${iface})
+    else
+      iface=$(get_name_by_pci_id "${pci_id}")
+    fi
     local bridge=$(get_dpdk_config_value ${nic} '.bridge')
     local vf_index=$(get_dpdk_config_value ${nic} '.vf_index')
 
@@ -203,8 +209,6 @@
       migrate_ip "${pci_id}" "${bridge}"
     fi
 
-    iface=$(get_name_by_pci_id "${pci_id}")
-
     if [ -n "${iface}" ]; then
       ip link set ${iface} promisc on
       if [ -n "${vf_index}" ]; then
@@ -292,6 +296,12 @@
     echo $bond | jq -r -c '.nics[]' > /tmp/nics_array
     while IFS= read -r nic; do
       local pci_id=$(get_dpdk_config_value ${nic} '.pci_id')
+      local iface=$(get_dpdk_config_value ${nic} '.iface')
+      if [ -n ${iface} ] && [ -z ${pci_id} ]; then
+        local pci_id=$(get_address_by_nicname ${iface})
+      else
+        iface=$(get_name_by_pci_id "${pci_id}")
+      fi
       local nic_name=$(get_dpdk_config_value ${nic} '.name')
       local pmd_rxq_affinity=$(get_dpdk_config_value ${nic} '.pmd_rxq_affinity')
       local vf_index=$(get_dpdk_config_value ${nic} '.vf_index')
@@ -302,8 +312,6 @@
         ip_migrated=true
       fi
 
-      iface=$(get_name_by_pci_id "${pci_id}")
-
       if [ -n "${iface}" ]; then
         ip link set ${iface} promisc on
         if [ -n "${vf_index}" ]; then
@@ -407,6 +415,12 @@
   fi
 }
 
+function get_address_by_nicname {
+  if [[ -e /sys/class/net/$1/device ]]; then
+    readlink -f /sys/class/net/$1/device | xargs basename
+  fi
+}
+
 function init_ovs_dpdk_bridge {
   bridge=$1
   ovs-vsctl --db=unix:${OVS_SOCKET} --may-exist add-br ${bridge} \
diff --git a/charts/patches/neutron/0004-nic-name-feature.patch b/charts/patches/neutron/0007-dpdk-nic-name-support.patch
similarity index 87%
rename from charts/patches/neutron/0004-nic-name-feature.patch
rename to charts/patches/neutron/0007-dpdk-nic-name-support.patch
index c2325e7..3fa2963 100644
--- a/charts/patches/neutron/0004-nic-name-feature.patch
+++ b/charts/patches/neutron/0007-dpdk-nic-name-support.patch
@@ -1,7 +1,7 @@
-diff --git a/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl b/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+diff --git a/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl b/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
 index bd0a64a..08833a5 100644
---- a/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
-+++ b/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+--- a/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
++++ b/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
 @@ -196,6 +196,12 @@ function process_dpdk_nics {
    while IFS= read -r nic; do
      local port_name=$(get_dpdk_config_value ${nic} '.name')
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/monitoring.rst b/doc/source/admin/monitoring.rst
index d196e15..d72a1b5 100644
--- a/doc/source/admin/monitoring.rst
+++ b/doc/source/admin/monitoring.rst
@@ -461,3 +461,17 @@
   .. code-block:: console
 
     openstack server list --all-projects --long -n --ip $IP
+
+``EtcdMembersDown``
+  If any alarms are fired from Promethetus for ``etcd`` issues such as ``TargetDown``,
+  ``etcdMembersDown``, or ``etcdInsufficientMembers``), it could be due to expired
+  certificates.  You can update the certificates that ``kube-prometheus-stack`` uses for
+  talking with ``etcd`` with the following commands:
+
+  .. code-block:: console
+
+    kubectl -n monitoring delete secret/kube-prometheus-stack-etcd-client-cert
+    kubectl -n monitoring create secret generic kube-prometheus-stack-etcd-client-cert \
+        --from-file=/etc/kubernetes/pki/etcd/ca.crt \
+        --from-file=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
+        --from-file=/etc/kubernetes/pki/etcd/healthcheck-client.key
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/flake.lock b/flake.lock
index 1f709e6..fd96ecb 100644
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1705309234,
-        "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
+        "lastModified": 1731533236,
+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
         "type": "github"
       },
       "original": {
@@ -20,11 +20,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1706925685,
-        "narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=",
+        "lastModified": 1740396192,
+        "narHash": "sha256-ATMHHrg3sG1KgpQA5x8I+zcYpp5Sf17FaFj/fN+8OoQ=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "79a13f1437e149dc7be2d1290c74d378dad60814",
+        "rev": "d9b69c3ec2a2e2e971c534065bdd53374bd68b97",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 467b8bb..f0e044f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -19,10 +19,12 @@
               earthly
               glibcLocales
               go
+              just
               kubernetes-helm
               nixpkgs-fmt
               patchutils
               python311Packages.tox
+              reno
             ];
           };
         }
diff --git a/images/magnum/Dockerfile b/images/magnum/Dockerfile
index 724e7b8..1aefaca 100644
--- a/images/magnum/Dockerfile
+++ b/images/magnum/Dockerfile
@@ -18,7 +18,7 @@
 uv pip install \
     --constraint /upper-constraints.txt \
         /src/magnum \
-        magnum-cluster-api==0.24.2
+        magnum-cluster-api==0.27.0
 EOF
 
 FROM openstack-python-runtime
diff --git a/justfile b/justfile
new file mode 100644
index 0000000..e79c16f
--- /dev/null
+++ b/justfile
@@ -0,0 +1,5 @@
+default:
+  just --list
+
+bump-magnum-cluster-api:
+  cargo run --bin mcapibumper
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/bump-mcapi-b1b3ac5df67ee216.yaml b/releasenotes/notes/bump-mcapi-b1b3ac5df67ee216.yaml
new file mode 100644
index 0000000..254dfc6
--- /dev/null
+++ b/releasenotes/notes/bump-mcapi-b1b3ac5df67ee216.yaml
@@ -0,0 +1,3 @@
+fixes:
+  - The Cluster API driver for Magnum has been bumped to 0.27.0 to improve
+    stability, fix bugs and add new features.
diff --git a/releasenotes/notes/bump-mcapi-bde5d8909e7f6268.yaml b/releasenotes/notes/bump-mcapi-bde5d8909e7f6268.yaml
new file mode 100644
index 0000000..931d671
--- /dev/null
+++ b/releasenotes/notes/bump-mcapi-bde5d8909e7f6268.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+  - The Cluster API driver for Magnum has been bumped to 0.26.2 to address
+    bugs around cluster deletion.
diff --git a/releasenotes/notes/magnum-update-mcapi-to-0.25.1-fbf7f3dd8b81489c.yaml b/releasenotes/notes/magnum-update-mcapi-to-0.25.1-fbf7f3dd8b81489c.yaml
new file mode 100644
index 0000000..c563d8c
--- /dev/null
+++ b/releasenotes/notes/magnum-update-mcapi-to-0.25.1-fbf7f3dd8b81489c.yaml
@@ -0,0 +1,3 @@
+---
+upgrade:
+  - Upgrade Cluster API driver for Magnum to 0.26.0.
diff --git a/src/bin/mcapibumper.rs b/src/bin/mcapibumper.rs
new file mode 100644
index 0000000..3a0adbc
--- /dev/null
+++ b/src/bin/mcapibumper.rs
@@ -0,0 +1,71 @@
+use git2::Repository;
+use indoc::indoc;
+use regex::Regex;
+use serde::Deserialize;
+use std::path::Path;
+use tokio::fs;
+
+#[derive(Deserialize)]
+struct PyPiPackageResponse {
+    info: PyPiPackageInfo,
+}
+
+#[derive(Deserialize)]
+struct PyPiPackageInfo {
+    version: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let version = reqwest::get("https://pypi.org/pypi/magnum-cluster-api/json")
+        .await?
+        .json::<PyPiPackageResponse>()
+        .await?
+        .info
+        .version;
+
+    // Dockerfile
+    let path = "images/magnum/Dockerfile";
+    let content = fs::read_to_string(path).await?;
+    let re = Regex::new(r"(magnum-cluster-api==)(\S+)")?;
+    let updated = re.replace(&content, format!("${{1}}{}", version));
+    fs::write(path, updated.into_owned()).await?;
+
+    // Release notes
+    let version_hash = format!("{:x}", md5::compute(&version));
+    let release_note_path = format!("releasenotes/notes/bump-mcapi-{}.yaml", &version_hash[..16]);
+    let release_note = format!(
+        indoc!(
+            r#"
+            fixes:
+              - The Cluster API driver for Magnum has been bumped to {} to improve
+                stability, fix bugs and add new features.
+            "#
+        ),
+        version
+    );
+    fs::write(&release_note_path, &release_note).await?;
+
+    // Git commit
+    let repo = Repository::discover(".")?;
+    let mut index = repo.index()?;
+    index.add_path(Path::new(path))?;
+    index.add_path(Path::new(&release_note_path))?;
+    index.write()?;
+    let tree_id = index.write_tree()?;
+    let tree = repo.find_tree(tree_id)?;
+    let parent = repo.head()?.peel_to_commit()?;
+    let sig = repo.signature()?;
+    let mut commit_message = format!("Bump Magnum Cluster API to {}", version);
+    git2_hooks::hooks_commit_msg(&repo, None, &mut commit_message)?;
+    repo.commit(
+        Some("HEAD"),
+        &sig,
+        &sig,
+        commit_message.as_str(),
+        &tree,
+        &[&parent],
+    )?;
+
+    Ok(())
+}