Add Rust based ovsinit binary
This binary allows us to more reliably migrate IPs, which also
includes routes as well, with proper rollback in place to avoid
a system falling off the network.
This first iteration is used in the Neutron & OVN charts only,
but the long term plan is to leverage it into the Open vSwitch
charts potentially to have a single "auto_bridge_add" source
of truth.
Change-Id: Ic4de23297b67a602d9aba4b00f0fb234d9d37cfe
(cherry picked from commit 62c4dd963918ea193aea48be8db93f0ea52fa308)
diff --git a/Cargo.lock b/Cargo.lock
index fb7ed86..1ea12db 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
-version = 3
+version = 4
[[package]]
name = "addr2line"
@@ -18,6 +18,15 @@
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -33,6 +42,62 @@
]
[[package]]
+name = "anstream"
+version = "0.6.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+dependencies = [
+ "anstyle",
+ "once_cell",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
+
+[[package]]
name = "atmosphere"
version = "0.0.0"
dependencies = [
@@ -98,7 +163,7 @@
"serde_json",
"serde_repr",
"serde_urlencoded",
- "thiserror",
+ "thiserror 2.0.11",
"tokio",
"tokio-util",
"tower-service",
@@ -164,6 +229,52 @@
]
[[package]]
+name = "clap"
+version = "4.5.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+
+[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -191,6 +302,29 @@
]
[[package]]
+name = "env_filter"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "env_filter",
+ "humantime",
+ "log",
+]
+
+[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -234,12 +368,28 @@
]
[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
+ "futures-sink",
]
[[package]]
@@ -249,6 +399,23 @@
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -277,9 +444,13 @@
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
+ "futures-channel",
"futures-core",
+ "futures-io",
"futures-macro",
+ "futures-sink",
"futures-task",
+ "memchr",
"pin-project-lite",
"pin-utils",
"slab",
@@ -315,6 +486,12 @@
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -367,6 +544,12 @@
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]]
+name = "humantime"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
+
+[[package]]
name = "hyper"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -620,6 +803,18 @@
]
[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
+[[package]]
name = "itoa"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -697,6 +892,81 @@
]
[[package]]
+name = "netlink-packet-core"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72724faf704479d67b388da142b186f916188505e7e0b26719019c525882eda4"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "netlink-packet-utils",
+]
+
+[[package]]
+name = "netlink-packet-route"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74c171cd77b4ee8c7708da746ce392440cb7bcf618d122ec9ecc607b12938bf4"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "libc",
+ "log",
+ "netlink-packet-core",
+ "netlink-packet-utils",
+]
+
+[[package]]
+name = "netlink-packet-utils"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34"
+dependencies = [
+ "anyhow",
+ "byteorder",
+ "paste",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "netlink-proto"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72452e012c2f8d612410d89eea01e2d9b56205274abb35d53f60200b2ec41d60"
+dependencies = [
+ "bytes",
+ "futures",
+ "log",
+ "netlink-packet-core",
+ "netlink-sys",
+ "thiserror 2.0.11",
+]
+
+[[package]]
+name = "netlink-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16c903aa70590cb93691bf97a767c8d1d6122d2cc9070433deb3bbf36ce8bd23"
+dependencies = [
+ "bytes",
+ "futures",
+ "libc",
+ "log",
+ "tokio",
+]
+
+[[package]]
+name = "nix"
+version = "0.27.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -727,13 +997,38 @@
checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]]
+name = "ovsinit"
+version = "0.1.0"
+dependencies = [
+ "clap",
+ "env_logger",
+ "futures",
+ "futures-util",
+ "ipnet",
+ "libc",
+ "log",
+ "netlink-packet-route",
+ "rtnetlink",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.11",
+ "tokio",
+]
+
+[[package]]
name = "passwd"
version = "0.1.0"
dependencies = [
- "thiserror",
+ "thiserror 2.0.11",
]
[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
name = "percent-encoding"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -824,6 +1119,53 @@
]
[[package]]
+name = "regex"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
+
+[[package]]
+name = "rtnetlink"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b684475344d8df1859ddb2d395dd3dac4f8f3422a1aa0725993cb375fc5caba5"
+dependencies = [
+ "futures",
+ "log",
+ "netlink-packet-core",
+ "netlink-packet-route",
+ "netlink-packet-utils",
+ "netlink-proto",
+ "netlink-sys",
+ "nix",
+ "thiserror 1.0.69",
+ "tokio",
+]
+
+[[package]]
name = "rustainers"
version = "0.1.0"
dependencies = [
@@ -833,7 +1175,7 @@
"passwd",
"rand",
"tar",
- "thiserror",
+ "thiserror 2.0.11",
"tokio",
]
@@ -978,6 +1320,12 @@
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
name = "syn"
version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1012,11 +1360,31 @@
[[package]]
name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [
- "thiserror-impl",
+ "thiserror-impl 2.0.11",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
@@ -1172,6 +1540,12 @@
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index e5a3d8e..746bdd0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,4 +7,4 @@
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[workspace]
-members = ["crates/passwd", "crates/rustainers"]
+members = [ "crates/ovsinit","crates/passwd", "crates/rustainers"]
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 bd0a64a..c15e40a 100644
--- a/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+++ b/charts/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
@@ -435,13 +435,14 @@
if [ -n "$iface" ] && [ "$iface" != "null" ] && ( ip link show $iface 1>/dev/null 2>&1 );
then
ovs-vsctl --db=unix:${OVS_SOCKET} --may-exist add-port $bridge $iface
- migrate_ip_from_nic $iface $bridge
if [[ "${DPDK_ENABLED}" != "true" ]]; then
ip link set dev $iface up
fi
fi
done
+/usr/local/bin/ovsinit /tmp/auto_bridge_add
+
tunnel_types="{{- .Values.conf.plugins.openvswitch_agent.agent.tunnel_types -}}"
if [[ -n "${tunnel_types}" ]] ; then
tunnel_interface="{{- .Values.network.interface.tunnel -}}"
diff --git a/charts/ovn/templates/bin/_ovn-controller-init.sh.tpl b/charts/ovn/templates/bin/_ovn-controller-init.sh.tpl
index 1d303c8..049f731 100644
--- a/charts/ovn/templates/bin/_ovn-controller-init.sh.tpl
+++ b/charts/ovn/templates/bin/_ovn-controller-init.sh.tpl
@@ -25,58 +25,6 @@
echo ${ip}
}
-function get_ip_prefix_from_interface {
- local interface=$1
- local prefix=$(ip -4 -o addr s "${interface}" | awk '{ print $4; exit }' | awk -F '/' 'NR==1 {print $2}')
- if [ -z "${prefix}" ] ; then
- exit 1
- fi
- echo ${prefix}
-}
-
-function migrate_ip_from_nic {
- src_nic=$1
- bridge_name=$2
-
- # Enabling explicit error handling: We must avoid to lose the IP
- # address in the migration process. Hence, on every error, we
- # attempt to assign the IP back to the original NIC and exit.
- set +e
-
- ip=$(get_ip_address_from_interface ${src_nic})
- prefix=$(get_ip_prefix_from_interface ${src_nic})
-
- bridge_ip=$(get_ip_address_from_interface "${bridge_name}")
- bridge_prefix=$(get_ip_prefix_from_interface "${bridge_name}")
-
- ip link set ${bridge_name} up
-
- if [[ -n "${ip}" && -n "${prefix}" ]]; then
- ip addr flush dev ${src_nic}
- if [ $? -ne 0 ] ; then
- ip addr add ${ip}/${prefix} dev ${src_nic}
- echo "Error while flushing IP from ${src_nic}."
- exit 1
- fi
-
- ip addr add ${ip}/${prefix} dev "${bridge_name}"
- if [ $? -ne 0 ] ; then
- echo "Error assigning IP to bridge "${bridge_name}"."
- ip addr add ${ip}/${prefix} dev ${src_nic}
- exit 1
- fi
- elif [[ -n "${bridge_ip}" && -n "${bridge_prefix}" ]]; then
- echo "Bridge '${bridge_name}' already has IP assigned. Keeping the same:: IP:[${bridge_ip}]; Prefix:[${bridge_prefix}]..."
- elif [[ -z "${bridge_ip}" && -z "${ip}" ]]; then
- echo "Interface and bridge have no ips configured. Leaving as is."
- else
- echo "Interface ${src_nic} has invalid IP address. IP:[${ip}]; Prefix:[${prefix}]..."
- exit 1
- fi
-
- set -e
-}
-
function get_current_system_id {
ovs-vsctl --if-exists get Open_vSwitch . external_ids:system-id | tr -d '"'
}
@@ -174,6 +122,7 @@
if [ -n "$iface" ] && [ "$iface" != "null" ] && ( ip link show $iface 1>/dev/null 2>&1 );
then
ovs-vsctl --may-exist add-port $bridge $iface
- migrate_ip_from_nic $iface $bridge
fi
done
+
+/usr/local/bin/ovsinit /tmp/auto_bridge_add
diff --git a/charts/patches/neutron/0001-Switch-Neutron-to-ovsinit.patch b/charts/patches/neutron/0001-Switch-Neutron-to-ovsinit.patch
new file mode 100644
index 0000000..0c1d7d3
--- /dev/null
+++ b/charts/patches/neutron/0001-Switch-Neutron-to-ovsinit.patch
@@ -0,0 +1,32 @@
+From 3e0120d8457faf947f6f5d3ed79a1f08a0d271cd Mon Sep 17 00:00:00 2001
+From: Mohammed Naser <mnaser@vexxhost.com>
+Date: Mon, 17 Feb 2025 10:58:17 -0500
+Subject: [PATCH] Switch Neutron to ovsinit
+
+---
+ neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl b/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+index bd0a64ac..c15e40a5 100644
+--- a/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
++++ b/neutron/templates/bin/_neutron-openvswitch-agent-init.sh.tpl
+@@ -435,13 +435,14 @@ do
+ if [ -n "$iface" ] && [ "$iface" != "null" ] && ( ip link show $iface 1>/dev/null 2>&1 );
+ then
+ ovs-vsctl --db=unix:${OVS_SOCKET} --may-exist add-port $bridge $iface
+- migrate_ip_from_nic $iface $bridge
+ if [[ "${DPDK_ENABLED}" != "true" ]]; then
+ ip link set dev $iface up
+ fi
+ fi
+ done
+
++/usr/local/bin/ovsinit /tmp/auto_bridge_add
++
+ tunnel_types="{{- .Values.conf.plugins.openvswitch_agent.agent.tunnel_types -}}"
+ if [[ -n "${tunnel_types}" ]] ; then
+ tunnel_interface="{{- .Values.network.interface.tunnel -}}"
+--
+2.47.0
+
diff --git a/charts/patches/ovn/0003-Switch-OVN-to-ovsinit.patch b/charts/patches/ovn/0003-Switch-OVN-to-ovsinit.patch
new file mode 100644
index 0000000..ba04dcf
--- /dev/null
+++ b/charts/patches/ovn/0003-Switch-OVN-to-ovsinit.patch
@@ -0,0 +1,84 @@
+From 6c2dac4c0bcd71d400c113b922ba862d7945a09e Mon Sep 17 00:00:00 2001
+From: Mohammed Naser <mnaser@vexxhost.com>
+Date: Mon, 17 Feb 2025 11:00:30 -0500
+Subject: [PATCH] Switch OVN to ovsinit
+
+---
+ ovn/templates/bin/_ovn-controller-init.sh.tpl | 55 +------------------
+ 1 file changed, 2 insertions(+), 53 deletions(-)
+
+diff --git a/ovn/templates/bin/_ovn-controller-init.sh.tpl b/ovn/templates/bin/_ovn-controller-init.sh.tpl
+index 357c069d..006582f9 100644
+--- a/ovn/templates/bin/_ovn-controller-init.sh.tpl
++++ b/ovn/templates/bin/_ovn-controller-init.sh.tpl
+@@ -25,58 +25,6 @@ function get_ip_address_from_interface {
+ echo ${ip}
+ }
+
+-function get_ip_prefix_from_interface {
+- local interface=$1
+- local prefix=$(ip -4 -o addr s "${interface}" | awk '{ print $4; exit }' | awk -F '/' 'NR==1 {print $2}')
+- if [ -z "${prefix}" ] ; then
+- exit 1
+- fi
+- echo ${prefix}
+-}
+-
+-function migrate_ip_from_nic {
+- src_nic=$1
+- bridge_name=$2
+-
+- # Enabling explicit error handling: We must avoid to lose the IP
+- # address in the migration process. Hence, on every error, we
+- # attempt to assign the IP back to the original NIC and exit.
+- set +e
+-
+- ip=$(get_ip_address_from_interface ${src_nic})
+- prefix=$(get_ip_prefix_from_interface ${src_nic})
+-
+- bridge_ip=$(get_ip_address_from_interface "${bridge_name}")
+- bridge_prefix=$(get_ip_prefix_from_interface "${bridge_name}")
+-
+- ip link set ${bridge_name} up
+-
+- if [[ -n "${ip}" && -n "${prefix}" ]]; then
+- ip addr flush dev ${src_nic}
+- if [ $? -ne 0 ] ; then
+- ip addr add ${ip}/${prefix} dev ${src_nic}
+- echo "Error while flushing IP from ${src_nic}."
+- exit 1
+- fi
+-
+- ip addr add ${ip}/${prefix} dev "${bridge_name}"
+- if [ $? -ne 0 ] ; then
+- echo "Error assigning IP to bridge "${bridge_name}"."
+- ip addr add ${ip}/${prefix} dev ${src_nic}
+- exit 1
+- fi
+- elif [[ -n "${bridge_ip}" && -n "${bridge_prefix}" ]]; then
+- echo "Bridge '${bridge_name}' already has IP assigned. Keeping the same:: IP:[${bridge_ip}]; Prefix:[${bridge_prefix}]..."
+- elif [[ -z "${bridge_ip}" && -z "${ip}" ]]; then
+- echo "Interface and bridge have no ips configured. Leaving as is."
+- else
+- echo "Interface ${src_nic} has invalid IP address. IP:[${ip}]; Prefix:[${prefix}]..."
+- exit 1
+- fi
+-
+- set -e
+-}
+-
+ function get_current_system_id {
+ ovs-vsctl --if-exists get Open_vSwitch . external_ids:system-id | tr -d '"'
+ }
+@@ -174,6 +122,7 @@ do
+ if [ -n "$iface" ] && [ "$iface" != "null" ] && ( ip link show $iface 1>/dev/null 2>&1 );
+ then
+ ovs-vsctl --may-exist add-port $bridge $iface
+- migrate_ip_from_nic $iface $bridge
+ fi
+ done
++
++/usr/local/bin/ovsinit /tmp/auto_bridge_add
+--
+2.47.0
+
diff --git a/crates/ovsinit/Cargo.toml b/crates/ovsinit/Cargo.toml
new file mode 100644
index 0000000..ac7d810
--- /dev/null
+++ b/crates/ovsinit/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "ovsinit"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.5.29", features = ["derive"] }
+env_logger = { version = "0.11.6", features = ["unstable-kv"] }
+futures = "0.3.31"
+futures-util = "0.3.31"
+ipnet = "2.11.0"
+libc = "0.2.169"
+log = { version = "0.4.25", features = ["kv"] }
+netlink-packet-route = "0.19.0"
+rtnetlink = "0.14.1"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+thiserror = "2.0.11"
+tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
diff --git a/crates/ovsinit/src/config.rs b/crates/ovsinit/src/config.rs
new file mode 100644
index 0000000..7c3d6b7
--- /dev/null
+++ b/crates/ovsinit/src/config.rs
@@ -0,0 +1,82 @@
+use serde::Deserialize;
+use std::collections::HashMap;
+use std::{fs::File, path::PathBuf};
+use thiserror::Error;
+use log::{error, info};
+
+#[derive(Deserialize)]
+pub struct NetworkConfig {
+ #[serde(flatten)]
+ pub bridges: HashMap<String, Option<String>>,
+}
+
+#[derive(Debug, Error)]
+pub enum NetworkConfigError {
+ #[error("Failed to open file: {0}")]
+ OpenFile(#[from] std::io::Error),
+
+ #[error("Failed to parse JSON: {0}")]
+ ParseJson(#[from] serde_json::Error),
+}
+
+impl NetworkConfig {
+ pub fn from_path(path: &PathBuf) -> Result<Self, NetworkConfigError> {
+ let file = File::open(path)?;
+ NetworkConfig::from_file(file)
+ }
+
+ pub fn from_file(file: File) -> Result<Self, NetworkConfigError> {
+ let config: NetworkConfig = serde_json::from_reader(file)?;
+ Ok(config)
+ }
+
+ pub fn bridges_with_interfaces_iter(&self) -> impl Iterator<Item = (&String, &String)> {
+ self.bridges.iter().filter_map(|(k, v)| {
+ if let Some(v) = v {
+ Some((k, v))
+ } else {
+ info!(bridge = k.as_str(); "Bridge has no interface, skipping.");
+
+ None
+ }
+ })
+ }
+
+ #[allow(dead_code)]
+ pub fn from_string(json: &str) -> Result<Self, NetworkConfigError> {
+ let config: NetworkConfig = serde_json::from_str(json)?;
+ Ok(config)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_null_interface() {
+ let config = NetworkConfig::from_string("{\"br-ex\": null}").unwrap();
+
+ assert_eq!(config.bridges.len(), 1);
+ assert_eq!(config.bridges.get("br-ex"), Some(&None));
+ }
+
+ #[test]
+ fn test_bridges_with_interfaces_iter_with_null_interface() {
+ let config = NetworkConfig::from_string("{\"br-ex\": null}").unwrap();
+
+ let mut iter = config.bridges_with_interfaces_iter();
+ assert_eq!(iter.next(), None);
+ }
+
+ #[test]
+ fn test_bridges_with_interfaces_iter_with_interface() {
+ let config = NetworkConfig::from_string("{\"br-ex\": \"bond0\"}").unwrap();
+
+ let mut iter = config.bridges_with_interfaces_iter();
+ assert_eq!(
+ iter.next(),
+ Some((&"br-ex".to_string(), &"bond0".to_string()))
+ );
+ }
+}
diff --git a/crates/ovsinit/src/lib.rs b/crates/ovsinit/src/lib.rs
new file mode 100644
index 0000000..80fb9cd
--- /dev/null
+++ b/crates/ovsinit/src/lib.rs
@@ -0,0 +1,353 @@
+extern crate ipnet;
+
+mod routes;
+
+use futures_util::stream::TryStreamExt;
+use ipnet::IpNet;
+use log::{error, info};
+use netlink_packet_route::{
+ address::{AddressAttribute, AddressMessage},
+ route::{RouteAttribute, RouteMessage, RouteScope},
+ AddressFamily,
+};
+use rtnetlink::{Handle, IpVersion};
+use std::net::IpAddr;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum InterfaceError {
+ #[error("Interface {0} not found")]
+ NotFound(String),
+
+ #[error(transparent)]
+ NetlinkError(#[from] rtnetlink::Error),
+
+ #[error(transparent)]
+ IpNetError(#[from] ipnet::PrefixLenError),
+
+ #[error(transparent)]
+ RouteError(#[from] routes::RouteError),
+}
+
+#[derive(Error, Debug)]
+pub enum InterfaceMigrationError {
+ #[error(transparent)]
+ InterfaceError(#[from] InterfaceError),
+
+ #[error("IP configuration on both interfaces")]
+ IpConflict,
+}
+
+pub struct Interface {
+ name: String,
+ index: u32,
+ address_messages: Vec<AddressMessage>,
+ route_messages: Vec<RouteMessage>,
+}
+
+impl Interface {
+ pub async fn new(handle: &Handle, name: String) -> Result<Self, InterfaceError> {
+ let index = handle
+ .link()
+ .get()
+ .match_name(name.clone())
+ .execute()
+ .try_next()
+ .await
+ .map_err(|e| match e {
+ rtnetlink::Error::NetlinkError(inner) if -inner.raw_code() == libc::ENODEV => {
+ InterfaceError::NotFound(name.clone())
+ }
+ _ => InterfaceError::NetlinkError(e),
+ })?
+ .map(|link| link.header.index)
+ .ok_or_else(|| InterfaceError::NotFound(name.clone()))?;
+
+ let address_messages: Vec<AddressMessage> = handle
+ .address()
+ .get()
+ .set_link_index_filter(index)
+ .execute()
+ .map_err(InterfaceError::NetlinkError)
+ .try_filter(|msg| futures::future::ready(msg.header.family == AddressFamily::Inet))
+ .try_collect()
+ .await?;
+
+ let route_messages: Vec<RouteMessage> = handle
+ .route()
+ .get(IpVersion::V4)
+ .execute()
+ .map_err(InterfaceError::NetlinkError)
+ .try_filter(move |route_msg| {
+ let matches = route_msg
+ .attributes
+ .iter()
+ .any(|attr| matches!(attr, RouteAttribute::Oif(idx) if *idx == index))
+ && route_msg.header.kind != netlink_packet_route::route::RouteType::Local;
+
+ futures_util::future::ready(matches)
+ })
+ .try_collect()
+ .await?;
+
+ Ok(Self {
+ name,
+ index,
+ address_messages,
+ route_messages,
+ })
+ }
+
+ fn addresses(&self) -> Vec<IpNet> {
+ self.address_messages
+ .iter()
+ .filter_map(|msg| {
+ msg.attributes.iter().find_map(|nla| {
+ if let AddressAttribute::Address(ip) = nla {
+ IpNet::new(*ip, msg.header.prefix_len).ok()
+ } else {
+ None
+ }
+ })
+ })
+ .collect()
+ }
+
+ fn routes(&self) -> Result<Vec<routes::Route>, routes::RouteError> {
+ self.route_messages
+ .iter()
+ .filter_map(|msg| {
+ if msg.header.scope == RouteScope::Link {
+ return None;
+ }
+
+ Some(routes::Route::from_message(msg.clone()))
+ })
+ .collect::<Result<Vec<routes::Route>, routes::RouteError>>()
+ }
+
+ async fn up(&self, handle: &Handle) -> Result<(), InterfaceError> {
+ handle
+ .link()
+ .set(self.index)
+ .up()
+ .execute()
+ .await
+ .map_err(InterfaceError::NetlinkError)
+ }
+
+ async fn restore(&self, handle: &Handle) -> Result<(), InterfaceError> {
+ self.migrate_addresses_from_interface(handle, self).await?;
+ self.migrate_routes_from_interface(handle, self).await?;
+
+ Ok(())
+ }
+
+ async fn flush(&self, handle: &Handle) -> Result<(), InterfaceError> {
+ for msg in self.address_messages.iter() {
+ handle.address().del(msg.clone()).execute().await?;
+ }
+
+ // NOTE(mnaser): Once the interface has no more addresses, it will
+ // automatically lose all of it's routes.
+
+ Ok(())
+ }
+
+ async fn migrate_addresses_from_interface(
+ &self,
+ handle: &Handle,
+ src_interface: &Interface,
+ ) -> Result<(), InterfaceError> {
+ for msg in src_interface.address_messages.iter() {
+ let ip = msg.attributes.iter().find_map(|nla| match nla {
+ AddressAttribute::Address(ip) => Some(ip),
+ _ => None,
+ });
+
+ if let Some(ip) = ip {
+ handle
+ .address()
+ .add(self.index, *ip, msg.header.prefix_len)
+ .replace()
+ .execute()
+ .await?;
+ }
+ }
+
+ Ok(())
+ }
+
+ async fn migrate_routes_from_interface(
+ &self,
+ handle: &Handle,
+ src_interface: &Interface,
+ ) -> Result<(), InterfaceError> {
+ for route in src_interface.routes()?.iter() {
+ let mut request = handle.route().add();
+ request = request.protocol(route.protocol);
+
+ match route.destination.addr() {
+ IpAddr::V4(ipv4) => {
+ let mut request = request
+ .v4()
+ .replace()
+ .destination_prefix(ipv4, route.destination.prefix_len());
+
+ if let IpAddr::V4(gateway) = route.gateway {
+ request = request.gateway(gateway);
+ }
+
+ request.execute().await?;
+ }
+ IpAddr::V6(ipv6) => {
+ let mut request = request
+ .v6()
+ .replace()
+ .destination_prefix(ipv6, route.destination.prefix_len());
+
+ if let IpAddr::V6(gateway) = route.gateway {
+ request = request.gateway(gateway);
+ }
+
+ request.execute().await?;
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ pub async fn migrate_from_interface(
+ &self,
+ handle: &Handle,
+ src_interface: &Interface,
+ ) -> Result<(), InterfaceMigrationError> {
+ self.up(handle).await?;
+
+ match (
+ src_interface.address_messages.is_empty(),
+ self.address_messages.is_empty(),
+ ) {
+ (false, false) => {
+ // Both source and destination interfaces have IPs assigned
+ error!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str(),
+ src_ip_addresses = format!("{:?}", src_interface.addresses()).as_str(),
+ dst_ip_addresses = format!("{:?}", self.addresses()).as_str();
+ "Both source and destination interfaces have IPs assigned. This is not safe in production, please fix manually."
+ );
+
+ Err(InterfaceMigrationError::IpConflict)
+ }
+ (false, true) => {
+ // Source interface has IPs, destination interface has no IPs
+ info!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str(),
+ ip_addresses = format!("{:?}", src_interface.addresses()).as_str(),
+ routes = format!("{:?}", src_interface.routes()).as_str();
+ "Migrating IP addresses from interface to bridge."
+ );
+
+ if let Err(e) = src_interface.flush(handle).await {
+ error!(
+ src_interface = src_interface.name.as_str(),
+ error = e.to_string().as_str();
+ "Error while flushing IPs from source interface."
+ );
+
+ if let Err(restore_err) = src_interface.restore(handle).await {
+ error!(
+ src_interface = src_interface.name.as_str(),
+ error = restore_err.to_string().as_str();
+ "Error while restoring IPs to source interface."
+ );
+ }
+
+ return Err(InterfaceMigrationError::InterfaceError(e));
+ }
+
+ info!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str();
+ "Successfully flushed IP addresses from source interface."
+ );
+
+ if let Err(e) = self
+ .migrate_addresses_from_interface(handle, src_interface)
+ .await
+ {
+ error!(
+ dst_interface = self.name.as_str(),
+ error = e.to_string().as_str();
+ "Error while migrating IP addresses to destination interface."
+ );
+
+ if let Err(restore_err) = src_interface.restore(handle).await {
+ error!(
+ src_interface = src_interface.name.as_str(),
+ error = restore_err.to_string().as_str();
+ "Error while restoring IPs to source interface."
+ );
+ }
+
+ return Err(InterfaceMigrationError::InterfaceError(e));
+ }
+
+ info!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str();
+ "Successfully migrated IP addresseses to new interface."
+ );
+
+ if let Err(e) = self
+ .migrate_routes_from_interface(handle, src_interface)
+ .await
+ {
+ error!(
+ dst_interface = self.name.as_str(),
+ routes = format!("{:?}", src_interface.routes()).as_str(),
+ error = e.to_string().as_str();
+ "Error while migrating routes to destination interface."
+ );
+
+ if let Err(restore_err) = src_interface.restore(handle).await {
+ error!(
+ src_interface = src_interface.name.as_str(),
+ routes = format!("{:?}", src_interface.routes()).as_str(),
+ error = restore_err.to_string().as_str();
+ "Error while restoring source interface."
+ );
+ }
+
+ return Err(InterfaceMigrationError::InterfaceError(e));
+ }
+
+ Ok(())
+ }
+ (true, false) => {
+ // Destination interface has IPs, source interface has no IPs
+ info!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str(),
+ ip_addresses = format!("{:?}", self.addresses()).as_str();
+ "Bridge already has IPs assigned. Skipping migration."
+ );
+
+ Ok(())
+ }
+ (true, true) => {
+ // Neither interface has IPs
+ info!(
+ src_interface = src_interface.name.as_str(),
+ dst_interface = self.name.as_str();
+ "Neither interface nor bridge have IPs assigned. Skipping migration."
+ );
+
+ Ok(())
+ }
+ }
+ }
+}
diff --git a/crates/ovsinit/src/main.rs b/crates/ovsinit/src/main.rs
new file mode 100644
index 0000000..fb77530
--- /dev/null
+++ b/crates/ovsinit/src/main.rs
@@ -0,0 +1,63 @@
+mod config;
+
+use clap::Parser;
+use env_logger::Env;
+use log::error;
+use rtnetlink::Handle;
+use std::{path::PathBuf, process};
+
+#[derive(Parser, Debug)]
+#[command(version, about, long_about = None)]
+struct Cli {
+ #[arg(default_value = "/tmp/auto_bridge_add", help = "Path to the JSON file")]
+ config: PathBuf,
+}
+
+#[tokio::main]
+async fn main() {
+ let cli = Cli::parse();
+
+ let env = Env::default()
+ .filter_or("MY_LOG_LEVEL", "info")
+ .write_style_or("MY_LOG_STYLE", "always");
+ env_logger::init_from_env(env);
+
+ let network_config = match config::NetworkConfig::from_path(&cli.config) {
+ Ok(network_config) => network_config,
+ Err(e) => {
+ error!("Failed to load network config: {}", e);
+
+ process::exit(1);
+ }
+ };
+
+ let (connection, handle, _) = rtnetlink::new_connection().expect("Failed to create connection");
+ tokio::spawn(connection);
+
+ for (bridge_name, interface_name) in network_config.bridges_with_interfaces_iter() {
+ let interface = get_interface(&handle, interface_name).await;
+ let bridge = get_interface(&handle, bridge_name).await;
+
+ if let Err(e) = bridge.migrate_from_interface(&handle, &interface).await {
+ error!(
+ "Failed to migrate from {} to {}: {}",
+ interface_name, bridge_name, e
+ );
+ process::exit(1);
+ }
+ }
+}
+
+async fn get_interface(handle: &Handle, name: &str) -> ovsinit::Interface {
+ match ovsinit::Interface::new(handle, name.to_string()).await {
+ Ok(interface) => interface,
+ Err(ovsinit::InterfaceError::NotFound(name)) => {
+ error!(interface = name.as_str(); "Interface not found.");
+ process::exit(1);
+ }
+ Err(e) => {
+ error!(error = e.to_string().as_str(); "Failed to lookup interface.");
+ process::exit(1);
+ }
+ }
+}
diff --git a/crates/ovsinit/src/routes.rs b/crates/ovsinit/src/routes.rs
new file mode 100644
index 0000000..a4e0130
--- /dev/null
+++ b/crates/ovsinit/src/routes.rs
@@ -0,0 +1,150 @@
+use ipnet::IpNet;
+use log::error;
+use netlink_packet_route::{
+ route::{RouteAddress, RouteAttribute, RouteMessage, RouteProtocol},
+ AddressFamily,
+};
+use std::{
+ fmt,
+ net::{IpAddr, Ipv4Addr, Ipv6Addr},
+};
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum RouteError {
+ #[error("Invalid gateway")]
+ InvalidGateway,
+
+ #[error("Invalid destination")]
+ InvalidDestination,
+
+ #[error("Invalid prefix length")]
+ InvalidPrefixLength,
+
+ #[error("Missing gateway")]
+ MissingGateway,
+
+ #[error("Missing destination")]
+ MissingDestination,
+}
+
+pub struct Route {
+ pub protocol: RouteProtocol,
+ pub destination: IpNet,
+ pub gateway: IpAddr,
+}
+
+impl fmt::Debug for Route {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} via {}", self.destination, self.gateway)
+ }
+}
+
+impl Route {
+ pub fn from_message(message: RouteMessage) -> Result<Self, RouteError> {
+ let mut gateway = None;
+ let mut destination = None;
+
+ for nla in message.attributes.iter() {
+ if let RouteAttribute::Gateway(ip) = nla {
+ gateway = match ip {
+ RouteAddress::Inet(ip) => Some(IpAddr::V4(*ip)),
+ RouteAddress::Inet6(ip) => Some(IpAddr::V6(*ip)),
+ _ => return Err(RouteError::InvalidGateway),
+ };
+ }
+
+ if let RouteAttribute::Destination(ref ip) = nla {
+ destination = match ip {
+ RouteAddress::Inet(ip) => Some(
+ IpNet::new(IpAddr::V4(*ip), message.header.destination_prefix_length)
+ .map_err(|_| RouteError::InvalidPrefixLength)?,
+ ),
+ RouteAddress::Inet6(ip) => Some(
+ IpNet::new(IpAddr::V6(*ip), message.header.destination_prefix_length)
+ .map_err(|_| RouteError::InvalidPrefixLength)?,
+ ),
+ _ => return Err(RouteError::InvalidDestination),
+ };
+ }
+ }
+
+ let gateway = match gateway {
+ Some(gateway) => gateway,
+ None => return Err(RouteError::MissingGateway),
+ };
+
+ let destination = match destination {
+ Some(destination) => destination,
+ None => match message.header.address_family {
+ AddressFamily::Inet => IpNet::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)
+ .map_err(|_| RouteError::InvalidPrefixLength)?,
+ AddressFamily::Inet6 => IpNet::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0)
+ .map_err(|_| RouteError::InvalidPrefixLength)?,
+ _ => return Err(RouteError::InvalidDestination),
+ },
+ };
+
+ Ok(Route {
+ protocol: message.header.protocol,
+ destination,
+ gateway,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use netlink_packet_route::AddressFamily;
+ use std::net::Ipv4Addr;
+
+ #[tokio::test]
+ async fn test_default_ipv4_route() {
+ let mut message = RouteMessage::default();
+
+ message.header.address_family = AddressFamily::Inet;
+ message.header.destination_prefix_length = 0;
+ message.header.protocol = RouteProtocol::Static;
+ message
+ .attributes
+ .push(RouteAttribute::Gateway(RouteAddress::Inet(Ipv4Addr::new(
+ 192, 168, 1, 1,
+ ))));
+
+ let route = Route::from_message(message).unwrap();
+
+ assert_eq!(route.protocol, RouteProtocol::Static);
+ assert_eq!(
+ route.destination,
+ IpNet::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0).unwrap()
+ );
+ assert_eq!(route.gateway, IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)));
+ }
+
+ #[tokio::test]
+ async fn test_default_ipv6_route() {
+ let mut message = RouteMessage::default();
+
+ message.header.address_family = AddressFamily::Inet6;
+ message.header.destination_prefix_length = 0;
+ message.header.protocol = RouteProtocol::Static;
+ message
+ .attributes
+ .push(RouteAttribute::Gateway(RouteAddress::Inet6(Ipv6Addr::new(
+ 0, 0, 0, 0, 0, 0, 0, 1,
+ ))));
+
+ let route = Route::from_message(message).unwrap();
+
+ assert_eq!(route.protocol, RouteProtocol::Static);
+ assert_eq!(
+ route.destination,
+ IpNet::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0).unwrap()
+ );
+ assert_eq!(
+ route.gateway,
+ IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
+ );
+ }
+}
diff --git a/docker-bake.hcl b/docker-bake.hcl
index 60b0937..29b68ba 100644
--- a/docker-bake.hcl
+++ b/docker-bake.hcl
@@ -15,6 +15,17 @@
}
}
+target "ovsinit" {
+ context = "images/ovsinit"
+ platforms = ["linux/amd64", "linux/arm64"]
+
+ contexts = {
+ "runtime" = "docker-image://docker.io/library/debian:bullseye-slim"
+ "rust" = "docker-image://docker.io/library/rust:1.84-bullseye"
+ "src" = "./crates/ovsinit"
+ }
+}
+
target "ubuntu-cloud-archive" {
context = "images/ubuntu-cloud-archive"
platforms = ["linux/amd64", "linux/arm64"]
@@ -161,6 +172,7 @@
contexts = {
"golang" = "docker-image://docker.io/library/golang:1.20"
"openvswitch" = "target:openvswitch"
+ "ovsinit" = "target:ovsinit"
}
args = {
@@ -218,8 +230,9 @@
}
contexts = {
- "openstack-venv-builder" = "target:openstack-venv-builder"
"openstack-python-runtime" = "target:openstack-python-runtime"
+ "openstack-venv-builder" = "target:openstack-venv-builder"
+ "ovsinit" = "target:ovsinit"
}
tags = [
diff --git a/images/neutron/Dockerfile b/images/neutron/Dockerfile
index dc97db5..9935d08 100644
--- a/images/neutron/Dockerfile
+++ b/images/neutron/Dockerfile
@@ -38,4 +38,5 @@
apt-get clean
rm -rf /var/lib/apt/lists/*
EOF
+COPY --from=ovsinit /usr/local/bin/ovsinit /usr/local/bin/ovsinit
COPY --from=build --link /var/lib/openstack /var/lib/openstack
diff --git a/images/ovn/Dockerfile b/images/ovn/Dockerfile
index 7a9bd06..961d611 100644
--- a/images/ovn/Dockerfile
+++ b/images/ovn/Dockerfile
@@ -34,7 +34,7 @@
COPY --from=ovn-kubernetes --link /src/dist/images/ovndb-raft-functions.sh /root/ovndb-raft-functions.sh
COPY --from=ovn-kubernetes --link /src/dist/images/ovnkube.sh /root/ovnkube.sh
COPY --from=ovn-kubernetes --link /usr/bin/ovn-kube-util /usr/bin/ovn-kube-util
-
+COPY --from=ovsinit /usr/local/bin/ovsinit /usr/local/bin/ovsinit
RUN <<EOF bash -xe
usermod -u 42424 openvswitch
mkdir -p /var/log/ovn /var/lib/ovn /var/run/ovn
diff --git a/images/ovsinit/Dockerfile b/images/ovsinit/Dockerfile
new file mode 100644
index 0000000..edb2201
--- /dev/null
+++ b/images/ovsinit/Dockerfile
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: © 2025 VEXXHOST, Inc.
+# SPDX-License-Identifier: GPL-3.0-or-later
+# Atmosphere-Rebuild-Time: 2025-02-16T12:56:04Z
+
+FROM rust AS builder
+WORKDIR /src
+COPY --from=src / /src
+RUN cargo install --path .
+
+FROM runtime
+COPY --from=builder /usr/local/cargo/bin/ovsinit /usr/local/bin/ovsinit
diff --git a/releasenotes/notes/add-ovsinit-56990eaaf93c6f9d.yaml b/releasenotes/notes/add-ovsinit-56990eaaf93c6f9d.yaml
new file mode 100644
index 0000000..5482a80
--- /dev/null
+++ b/releasenotes/notes/add-ovsinit-56990eaaf93c6f9d.yaml
@@ -0,0 +1,9 @@
+---
+features:
+ - Introduced a new Rust-based binary ``ovsinit`` which focuses on handling
+ the migration of IP addresses from a physical interface to an OVS bridge
+ during the Neutron or OVN initialization process.
+fixes:
+ - During a Neutron or OVN initialization process, the routes assigned to
+ the physical interface are now removed and added to the OVS bridge
+ to maintain the connectivity of the host.