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/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()))
+        );
+    }
+}