1use anyhow::{Result, bail};
2
3pub fn validate_id(id: &str, kind: &str) -> Result<()> {
5 if id.is_empty() || id.len() > 63 {
6 bail!("{} ID must be 1-63 characters, got {}", kind, id.len());
7 }
8 if !id
9 .chars()
10 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
11 {
12 bail!(
13 "{} ID must be lowercase alphanumeric + hyphens: {:?}",
14 kind,
15 id
16 );
17 }
18 if id.starts_with('-') || id.ends_with('-') {
19 bail!("{} ID must not start or end with a hyphen: {:?}", kind, id);
20 }
21 Ok(())
22}
23
24pub fn generate_instance_id() -> String {
26 let bytes: [u8; 4] = rand_bytes();
27 format!(
28 "i-{}",
29 bytes
30 .iter()
31 .map(|b| format!("{:02x}", b))
32 .collect::<String>()
33 )
34}
35
36pub fn tap_name(tenant_net_id: u16, ip_offset: u8) -> String {
39 format!("tn{}i{}", tenant_net_id, ip_offset)
40}
41
42pub fn mac_address(tenant_net_id: u16, ip_offset: u8) -> String {
45 let net_bytes = tenant_net_id.to_be_bytes();
46 format!(
47 "02:fc:{:02x}:{:02x}:00:{:02x}",
48 net_bytes[0], net_bytes[1], ip_offset
49 )
50}
51
52fn rand_bytes() -> [u8; 4] {
54 let id = uuid::Uuid::new_v4();
55 let bytes = id.as_bytes();
56 [bytes[0], bytes[1], bytes[2], bytes[3]]
57}
58
59pub fn parse_pool_path(path: &str) -> Result<(&str, &str)> {
61 let parts: Vec<&str> = path.splitn(3, '/').collect();
62 if parts.len() < 2 {
63 bail!("Expected <tenant>/<pool>, got {:?}", path);
64 }
65 Ok((parts[0], parts[1]))
66}
67
68pub fn parse_instance_path(path: &str) -> Result<(&str, &str, &str)> {
70 let parts: Vec<&str> = path.splitn(4, '/').collect();
71 if parts.len() < 3 {
72 bail!("Expected <tenant>/<pool>/<instance>, got {:?}", path);
73 }
74 Ok((parts[0], parts[1], parts[2]))
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn test_validate_id_valid() {
83 assert!(validate_id("acme", "Tenant").is_ok());
84 assert!(validate_id("my-pool-1", "Pool").is_ok());
85 assert!(validate_id("a", "Tenant").is_ok());
86 }
87
88 #[test]
89 fn test_validate_id_invalid() {
90 assert!(validate_id("", "Tenant").is_err());
91 assert!(validate_id("UPPER", "Tenant").is_err());
92 assert!(validate_id("-leading", "Tenant").is_err());
93 assert!(validate_id("trailing-", "Tenant").is_err());
94 assert!(validate_id("has space", "Tenant").is_err());
95 assert!(validate_id(&"a".repeat(64), "Tenant").is_err());
96 }
97
98 #[test]
99 fn test_tap_name() {
100 assert_eq!(tap_name(3, 5), "tn3i5");
101 assert_eq!(tap_name(4095, 254), "tn4095i254");
102 }
103
104 #[test]
105 fn test_tap_name_fits_linux_limit() {
106 let name = tap_name(4095, 254);
108 assert!(name.len() <= 15, "TAP name too long: {}", name);
109 }
110
111 #[test]
112 fn test_mac_address_format() {
113 let mac = mac_address(3, 5);
114 assert!(mac.starts_with("02:fc:"));
115 assert_eq!(mac.len(), 17);
116 }
117
118 #[test]
119 fn test_generate_instance_id_format() {
120 let id = generate_instance_id();
121 assert!(id.starts_with("i-"));
122 assert_eq!(id.len(), 10); }
124
125 #[test]
126 fn test_parse_pool_path() {
127 let (t, p) = parse_pool_path("acme/workers").unwrap();
128 assert_eq!(t, "acme");
129 assert_eq!(p, "workers");
130 }
131
132 #[test]
133 fn test_parse_instance_path() {
134 let (t, p, i) = parse_instance_path("acme/workers/i-a3f7b2c1").unwrap();
135 assert_eq!(t, "acme");
136 assert_eq!(p, "workers");
137 assert_eq!(i, "i-a3f7b2c1");
138 }
139}