Skip to main content

mvm_core/
naming.rs

1use anyhow::{Result, bail};
2
3/// Validate a tenant or pool ID: lowercase alphanumeric + hyphens, 1-63 chars.
4pub 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
24/// Generate a random instance ID: "i-" followed by 8 hex chars.
25pub 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
36/// Generate a TAP device name: tn<net_id>i<ip_offset>.
37/// Max 12 chars, fits within Linux 15-char IFNAMSIZ limit.
38pub fn tap_name(tenant_net_id: u16, ip_offset: u8) -> String {
39    format!("tn{}i{}", tenant_net_id, ip_offset)
40}
41
42/// Deterministic MAC address from tenant_net_id and ip_offset.
43/// Format: 02:xx:xx:xx:xx:xx (locally administered).
44pub 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
52/// Simple random bytes using uuid crate (already a dependency).
53fn 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
59/// Parse a "tenant/pool" or "tenant/pool/instance" path.
60pub 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
68/// Parse a "tenant/pool/instance" path.
69pub 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        // Worst case: tn4095i254 = 10 chars, under 15
107        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); // "i-" + 8 hex chars
123    }
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}