1use anyhow::{Result, bail};
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8pub struct DevNetwork {
9 pub name: String,
11 pub bridge_name: String,
13 pub subnet: String,
15 pub gateway: String,
17 pub created_at: String,
19}
20
21impl DevNetwork {
22 pub fn default_network() -> Self {
24 Self {
25 name: "default".to_string(),
26 bridge_name: "br-mvm".to_string(),
27 subnet: "172.16.0.0/24".to_string(),
28 gateway: "172.16.0.1".to_string(),
29 created_at: chrono::Utc::now().to_rfc3339(),
30 }
31 }
32
33 pub fn new(name: &str, slot: u8) -> Result<Self> {
38 validate_network_name(name)?;
39 if slot == 0 {
40 bail!("slot 0 is reserved for the default network");
41 }
42 Ok(Self {
43 name: name.to_string(),
44 bridge_name: format!("br-mvm-{name}"),
45 subnet: format!("172.16.{slot}.0/24"),
46 gateway: format!("172.16.{slot}.1"),
47 created_at: chrono::Utc::now().to_rfc3339(),
48 })
49 }
50
51 pub fn gateway_cidr(&self) -> String {
53 let prefix = self.subnet.split('/').nth(1).unwrap_or("24");
54 format!("{}/{prefix}", self.gateway)
55 }
56}
57
58pub fn validate_network_name(name: &str) -> Result<()> {
60 crate::naming::validate_id(name, "network name")
61}
62
63pub fn networks_dir() -> String {
65 format!("{}/networks", crate::config::mvm_share_dir())
66}
67
68pub fn network_path(name: &str) -> String {
70 format!("{}/{name}.json", networks_dir())
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_default_network() {
79 let net = DevNetwork::default_network();
80 assert_eq!(net.name, "default");
81 assert_eq!(net.bridge_name, "br-mvm");
82 assert_eq!(net.subnet, "172.16.0.0/24");
83 assert_eq!(net.gateway, "172.16.0.1");
84 assert!(!net.created_at.is_empty());
85 }
86
87 #[test]
88 fn test_new_network() {
89 let net = DevNetwork::new("isolated", 1).unwrap();
90 assert_eq!(net.name, "isolated");
91 assert_eq!(net.bridge_name, "br-mvm-isolated");
92 assert_eq!(net.subnet, "172.16.1.0/24");
93 assert_eq!(net.gateway, "172.16.1.1");
94 }
95
96 #[test]
97 fn test_new_network_slot_0_rejected() {
98 assert!(DevNetwork::new("bad", 0).is_err());
99 }
100
101 #[test]
102 fn test_gateway_cidr() {
103 let net = DevNetwork::default_network();
104 assert_eq!(net.gateway_cidr(), "172.16.0.1/24");
105
106 let net2 = DevNetwork::new("test", 5).unwrap();
107 assert_eq!(net2.gateway_cidr(), "172.16.5.1/24");
108 }
109
110 #[test]
111 fn test_serde_roundtrip() {
112 let net = DevNetwork::new("mynet", 3).unwrap();
113 let json = serde_json::to_string(&net).unwrap();
114 let parsed: DevNetwork = serde_json::from_str(&json).unwrap();
115 assert_eq!(net, parsed);
116 }
117
118 #[test]
119 fn test_validate_network_name() {
120 assert!(validate_network_name("default").is_ok());
121 assert!(validate_network_name("my-net-1").is_ok());
122 assert!(validate_network_name("").is_err());
123 assert!(validate_network_name("UPPER").is_err());
124 assert!(validate_network_name("-leading").is_err());
125 }
126
127 #[test]
128 fn test_network_path() {
129 let path = network_path("default");
130 assert!(path.ends_with("/networks/default.json"));
131 }
132}