diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 3122aed..00faadf 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -1,22 +1,22 @@ mod error; mod store; pub use error::{Result, Error}; pub use store::Store; pub use store::open; pub mod namespace; pub use namespace::NamespaceStore; pub mod range; pub use range::RangeStore; pub mod network; -pub use network::NetworkStore; +pub use network::{NetworkStore, RangeAssigner}; pub mod combined; pub use combined::{ Transaction, TransactionContext, TransactionProvider, CombinedTransaction, CombinedTransactionContext }; diff --git a/crates/store/src/network.rs b/crates/store/src/network.rs index 5041775..ea08989 100644 --- a/crates/store/src/network.rs +++ b/crates/store/src/network.rs @@ -1,563 +1,868 @@ //! Network management module for storing and managing network configurations. //! //! This module provides functionality to create, read, update, and delete network //! configurations. Each network is defined by a name, network range (IPv4 or IPv6), //! a provider implementation, and an optional assigner implementation. //! //! # Example //! //! ``` //! use store::network::*; //! # use tempfile::TempDir; //! # fn example() -> store::Result<()> { //! # let temp_dir = tempfile::tempdir().unwrap(); //! # let db = sled::open(temp_dir.path())?; //! let store = NetworkStore::open(&db)?; //! //! // Create a network with IPv4 range //! let netrange = NetRange::from_cidr("192.168.1.0/24")?; //! let provider = BasicProvider::new() //! .with_config("type", "aws") //! .with_config("region", "us-west-2"); //! let assigner = Some(BasicAssigner::new("dhcp")); //! //! store.create("production", netrange, provider, assigner)?; //! //! // Retrieve the network //! let network = store.get("production")?.unwrap(); //! assert_eq!(network.name, "production"); //! //! // List all networks //! let networks = store.list()?; //! assert!(networks.contains(&"production".to_string())); //! # Ok(()) //! # } //! ``` -use crate::{Result, Error}; +use crate::{Result, Error, RangeStore}; use sled::Transactional; use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr}; /// Represents an IPv4 or IPv6 network range #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] pub enum NetRange { /// IPv4 network with address and prefix length V4 { addr: Ipv4Addr, prefix: u8 }, /// IPv6 network with address and prefix length V6 { addr: Ipv6Addr, prefix: u8 }, } impl NetRange { /// Create a new IPv4 network range pub fn ipv4(addr: Ipv4Addr, prefix: u8) -> Result { if prefix > 32 { return Err(Error::StoreError(sled::Error::Unsupported( format!("Invalid IPv4 prefix length: {}", prefix) ))); } Ok(NetRange::V4 { addr, prefix }) } /// Create a new IPv6 network range pub fn ipv6(addr: Ipv6Addr, prefix: u8) -> Result { if prefix > 128 { return Err(Error::StoreError(sled::Error::Unsupported( format!("Invalid IPv6 prefix length: {}", prefix) ))); } Ok(NetRange::V6 { addr, prefix }) } /// Parse a network range from CIDR notation pub fn from_cidr(cidr: &str) -> Result { let parts: Vec<&str> = cidr.split('/').collect(); if parts.len() != 2 { return Err(Error::StoreError(sled::Error::Unsupported( "Invalid CIDR format".to_string() ))); } let prefix: u8 = parts[1].parse().map_err(|_| { Error::StoreError(sled::Error::Unsupported("Invalid prefix length".to_string())) })?; if let Ok(ipv4) = parts[0].parse::() { Self::ipv4(ipv4, prefix) } else if let Ok(ipv6) = parts[0].parse::() { Self::ipv6(ipv6, prefix) } else { Err(Error::StoreError(sled::Error::Unsupported( "Invalid IP address format".to_string() ))) } } /// Convert to CIDR notation string pub fn to_cidr(&self) -> String { match self { NetRange::V4 { addr, prefix } => format!("{}/{}", addr, prefix), NetRange::V6 { addr, prefix } => format!("{}/{}", addr, prefix), } } } /// Trait for network providers pub trait NetworkProvider: std::fmt::Debug { /// Get the provider type identifier fn provider_type(&self) -> &'static str; /// Serialize provider configuration to JSON fn to_json(&self) -> Result; /// Create provider from JSON fn from_json(json: &str) -> Result where Self: Sized; } /// Trait for network assigners pub trait NetworkAssigner: std::fmt::Debug { /// Get the assigner type identifier fn assigner_type(&self) -> &'static str; /// Serialize assigner configuration to JSON fn to_json(&self) -> Result; /// Create assigner from JSON fn from_json(json: &str) -> Result where Self: Sized; } /// Basic network provider implementation #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BasicProvider { pub config: HashMap, } impl BasicProvider { pub fn new() -> Self { Self { config: HashMap::new(), } } pub fn with_config(mut self, key: &str, value: &str) -> Self { self.config.insert(key.to_string(), value.to_string()); self } } impl NetworkProvider for BasicProvider { fn provider_type(&self) -> &'static str { "basic" } fn to_json(&self) -> Result { serde_json::to_string(self).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) }) } fn from_json(json: &str) -> Result { serde_json::from_str(json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) }) } } /// Basic network assigner implementation #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct BasicAssigner { pub strategy: String, pub config: HashMap, } impl BasicAssigner { pub fn new(strategy: &str) -> Self { Self { strategy: strategy.to_string(), config: HashMap::new(), } } pub fn with_config(mut self, key: &str, value: &str) -> Self { self.config.insert(key.to_string(), value.to_string()); self } } impl NetworkAssigner for BasicAssigner { fn assigner_type(&self) -> &'static str { "basic" } fn to_json(&self) -> Result { serde_json::to_string(self).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) }) } fn from_json(json: &str) -> Result { serde_json::from_str(json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) }) } } +/// Range-based network assigner that uses RangeStore for IP assignment +/// +/// The `RangeAssigner` provides IP address assignment functionality by integrating +/// with the `RangeStore`. It automatically manages IP address allocation within +/// a network range using a bitmap-based approach for efficient tracking. +/// +/// # Features +/// +/// - Automatic range initialization when creating networks +/// - Sequential IP assignment within the network range +/// - Assignment tracking with custom identifiers +/// - IP unassignment and reuse +/// - Integration with the NetworkStore lifecycle +/// +/// # Example +/// +/// ``` +/// use store::network::{RangeAssigner, NetRange, BasicProvider, NetworkStore}; +/// # use tempfile::TempDir; +/// # fn example() -> store::Result<()> { +/// # let temp_dir = tempfile::tempdir().unwrap(); +/// # let db = sled::open(temp_dir.path())?; +/// # let ranges = store::range::RangeStore::open(&db)?; +/// # let store = NetworkStore::open_with_ranges(&db, ranges.clone())?; +/// +/// // Create a RangeAssigner for a network +/// let assigner = RangeAssigner::new("my-network-range") +/// .with_config("strategy", "sequential"); +/// +/// // Create a network with the range assigner +/// let netrange = NetRange::from_cidr("192.168.1.0/24")?; +/// let provider = BasicProvider::new(); +/// store.create("my-network", netrange, provider, Some(assigner))?; +/// +/// // Use the assigner to allocate IPs +/// let range_assigner = RangeAssigner::new("my-network-range"); +/// let ip_bit = range_assigner.assign_ip(&ranges, "device1")?; +/// # Ok(()) +/// # } +/// ``` +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct RangeAssigner { + pub range_name: String, + pub config: HashMap, +} + +impl RangeAssigner { + /// Create a new RangeAssigner with the specified range name + /// + /// The range name is used as the identifier for the IP range in the RangeStore. + /// It should be unique across all ranges and typically matches or relates to + /// the network name. + /// + /// # Arguments + /// + /// * `range_name` - Unique identifier for the IP range + pub fn new(range_name: &str) -> Self { + Self { + range_name: range_name.to_string(), + config: HashMap::new(), + } + } + + /// Add configuration key-value pairs to the assigner + /// + /// Configuration options can be used to customize the behavior of the + /// assigner. Common configuration keys might include: + /// - `strategy`: Assignment strategy (e.g., "sequential", "random") + /// - `reserve_gateway`: Whether to reserve the first IP for gateway + /// - `pool_size`: Maximum number of IPs to manage + /// + /// # Arguments + /// + /// * `key` - Configuration key + /// * `value` - Configuration value + pub fn with_config(mut self, key: &str, value: &str) -> Self { + self.config.insert(key.to_string(), value.to_string()); + self + } + + /// Initialize the range for this assigner with the given network size + /// + /// This method calculates the number of available IP addresses in the network + /// range and creates a corresponding range in the RangeStore. It's automatically + /// called when creating a network with a RangeAssigner. + /// + /// # Arguments + /// + /// * `range_store` - Reference to the RangeStore + /// * `network` - Network range (IPv4 or IPv6) to initialize + /// + /// # Returns + /// + /// Returns `Ok(())` if the range was successfully initialized, or an error + /// if the network parameters are invalid or the range already exists. + pub fn initialize_range(&self, range_store: &RangeStore, network: &NetRange) -> Result<()> { + let size = match network { + NetRange::V4 { prefix, .. } => { + if *prefix >= 32 { + return Err(Error::StoreError(sled::Error::Unsupported( + "IPv4 prefix must be less than 32".to_string() + ))); + } + 1u64 << (32 - prefix) + } + NetRange::V6 { prefix, .. } => { + if *prefix >= 128 { + return Err(Error::StoreError(sled::Error::Unsupported( + "IPv6 prefix must be less than 128".to_string() + ))); + } + // For IPv6, we'll limit to a reasonable size to avoid huge ranges + let host_bits = 128 - prefix; + if host_bits > 32 { + 1u64 << 32 // Cap at 2^32 addresses + } else { + 1u64 << host_bits + } + } + }; + + range_store.define(&self.range_name, size) + } + + /// Assign an IP address from the range + /// + /// Assigns the next available IP address in the range to the specified identifier. + /// The assignment uses a sequential allocation strategy, finding the first + /// available bit position in the range bitmap. + /// + /// # Arguments + /// + /// * `range_store` - Reference to the RangeStore + /// * `identifier` - Unique identifier for the device/entity receiving the IP + /// + /// # Returns + /// + /// Returns the bit position of the assigned IP address, which can be used + /// to calculate the actual IP address within the network range. + /// + /// # Errors + /// + /// Returns an error if the range is full or doesn't exist. + pub fn assign_ip(&self, range_store: &RangeStore, identifier: &str) -> Result { + range_store.assign(&self.range_name, identifier) + } + + /// Get assigned IP information + /// + /// Retrieves the identifier associated with a specific bit position in the range. + /// This is useful for determining which device or entity is assigned to a + /// particular IP address. + /// + /// # Arguments + /// + /// * `range_store` - Reference to the RangeStore + /// * `bit_position` - The bit position to query + /// + /// # Returns + /// + /// Returns `Some(identifier)` if the bit position is assigned, or `None` + /// if it's free or out of range. + pub fn get_assignment(&self, range_store: &RangeStore, bit_position: u64) -> Result> { + range_store.get(&self.range_name, bit_position) + } + + /// Unassign an IP address + /// + /// Releases an IP address assignment, making it available for future assignments. + /// This is typically called when a device is removed or no longer needs its + /// assigned IP address. + /// + /// # Arguments + /// + /// * `range_store` - Reference to the RangeStore + /// * `bit_position` - The bit position to unassign + /// + /// # Returns + /// + /// Returns `true` if the IP was successfully unassigned, or `false` if it + /// was already free or the bit position is out of range. + pub fn unassign_ip(&self, range_store: &RangeStore, bit_position: u64) -> Result { + range_store.unassign_bit(&self.range_name, bit_position) + } +} + +impl NetworkAssigner for RangeAssigner { + fn assigner_type(&self) -> &'static str { + "range" + } + + fn to_json(&self) -> Result { + serde_json::to_string(self).map_err(|e| { + Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) + }) + } + + fn from_json(json: &str) -> Result { + serde_json::from_str(json).map_err(|e| { + Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) + }) + } +} + /// Network configuration #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Network { pub name: String, pub netrange: NetRange, pub provider_type: String, pub provider_config: String, pub assigner_type: Option, pub assigner_config: Option, } /// Network store for managing network configurations #[derive(Debug, Clone)] pub struct NetworkStore { pub(crate) namespaces: crate::namespace::NamespaceStore, pub(crate) networks: sled::Tree, + pub(crate) ranges: Option, } impl NetworkStore { /// Open a new network store pub fn open(db: &sled::Db) -> Result { Ok(NetworkStore { namespaces: crate::namespace::NamespaceStore::open(db)?, networks: db.open_tree("networks/1/data")?, + ranges: None, + }) + } + + /// Open a new network store with range store + pub fn open_with_ranges(db: &sled::Db, ranges: crate::range::RangeStore) -> Result { + Ok(NetworkStore { + namespaces: crate::namespace::NamespaceStore::open(db)?, + networks: db.open_tree("networks/1/data")?, + ranges: Some(ranges), }) } /// Create a new network with provider and optional assigner pub fn create(&self, name: &str, netrange: NetRange, provider: P, assigner: Option) -> Result<()> where P: NetworkProvider, A: NetworkAssigner, { // Ensure "networks" namespace exists if !self.namespaces.namespace_exists("networks")? { self.namespaces.define("networks")?; } + // Initialize range if assigner is RangeAssigner + if let Some(ref assigner_ref) = assigner { + if assigner_ref.assigner_type() == "range" { + if let Some(ref range_store) = self.ranges { + // Parse the assigner config to get the RangeAssigner + let assigner_json = assigner_ref.to_json()?; + let range_assigner: RangeAssigner = serde_json::from_str(&assigner_json) + .map_err(|e| Error::StoreError(sled::Error::Unsupported( + format!("Failed to parse RangeAssigner: {}", e) + )))?; + + // Initialize the range for this network + range_assigner.initialize_range(range_store, &netrange)?; + } else { + return Err(Error::StoreError(sled::Error::Unsupported( + "RangeAssigner requires RangeStore but none was provided".to_string() + ))); + } + } + } + let network = Network { name: name.to_string(), netrange, provider_type: provider.provider_type().to_string(), provider_config: provider.to_json()?, assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()), assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?, }; let network_json = serde_json::to_string(&network).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) })?; (&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction( |(names, spaces, networks)| { // Reserve the network name in the "networks" namespace if !self.namespaces.reserve_in_transaction(names, spaces, "networks", name, name)? { return Err(sled::transaction::ConflictableTransactionError::Abort(())); } // Store the network configuration networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } ).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::NamespaceKeyReserved("networks".to_string(), name.to_string()) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; Ok(()) } /// Get a network by name pub fn get(&self, name: &str) -> Result> { if let Some(data) = self.networks.get(name.as_bytes())? { let network_json = String::from_utf8(data.to_vec())?; let network: Network = serde_json::from_str(&network_json).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON deserialization error: {}", e))) })?; Ok(Some(network)) } else { Ok(None) } } /// Update an existing network pub fn update(&self, name: &str, netrange: NetRange, provider: P, assigner: Option) -> Result<()> where P: NetworkProvider, A: NetworkAssigner, { if !self.namespaces.key_exists("networks", name)? { return Err(Error::StoreError(sled::Error::Unsupported( format!("Network '{}' does not exist", name) ))); } let network = Network { name: name.to_string(), netrange, provider_type: provider.provider_type().to_string(), provider_config: provider.to_json()?, assigner_type: assigner.as_ref().map(|a| a.assigner_type().to_string()), assigner_config: assigner.as_ref().map(|a| a.to_json()).transpose()?, }; let network_json = serde_json::to_string(&network).map_err(|e| { Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e))) })?; self.networks.insert(name.as_bytes(), network_json.as_bytes())?; Ok(()) } /// Delete a network by name pub fn delete(&self, name: &str) -> Result { let result = (&self.namespaces.names, &self.namespaces.spaces, &self.networks).transaction( |(names, spaces, networks)| { // Remove from namespace let removed = self.namespaces.remove_in_transaction(names, spaces, "networks", name)?; if removed { // Remove network data networks.remove(name.as_bytes())?; } Ok(removed) } ).map_err(|e| match e { sled::transaction::TransactionError::Abort(()) => { Error::StoreError(sled::Error::Unsupported("Transaction aborted".to_string())) } sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err), })?; Ok(result) } /// List all network names pub fn list(&self) -> Result> { self.namespaces.list_keys("networks") } /// Check if a network exists pub fn exists(&self, name: &str) -> Result { self.namespaces.key_exists("networks", name) } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; fn create_test_store() -> Result<(NetworkStore, TempDir)> { let temp_dir = tempfile::tempdir().unwrap(); let db = sled::open(temp_dir.path())?; - let store = NetworkStore::open(&db)?; + let ranges = crate::range::RangeStore::open(&db)?; + let store = NetworkStore::open_with_ranges(&db, ranges)?; Ok((store, temp_dir)) } #[test] fn test_netrange_ipv4() -> Result<()> { let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?; assert_eq!(netrange.to_cidr(), "192.168.1.0/24"); Ok(()) } #[test] fn test_netrange_ipv6() -> Result<()> { let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?; assert_eq!(netrange.to_cidr(), "2001:db8::/64"); Ok(()) } #[test] fn test_netrange_from_cidr() -> Result<()> { let ipv4_range = NetRange::from_cidr("10.0.0.0/8")?; assert_eq!(ipv4_range.to_cidr(), "10.0.0.0/8"); let ipv6_range = NetRange::from_cidr("fe80::/10")?; assert_eq!(ipv6_range.to_cidr(), "fe80::/10"); Ok(()) } #[test] fn test_basic_provider() -> Result<()> { let provider = BasicProvider::new() .with_config("endpoint", "https://api.example.com") .with_config("timeout", "30"); let json = provider.to_json()?; let restored = BasicProvider::from_json(&json)?; assert_eq!(provider.config, restored.config); assert_eq!(provider.provider_type(), "basic"); Ok(()) } #[test] fn test_basic_assigner() -> Result<()> { let assigner = BasicAssigner::new("round_robin") .with_config("pool_size", "100"); let json = assigner.to_json()?; let restored = BasicAssigner::from_json(&json)?; assert_eq!(assigner.strategy, restored.strategy); assert_eq!(assigner.config, restored.config); assert_eq!(assigner.assigner_type(), "basic"); Ok(()) } #[test] fn test_create_and_get_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("192.168.1.0".parse().unwrap(), 24)?; let provider = BasicProvider::new().with_config("type", "test"); let assigner = Some(BasicAssigner::new("sequential")); store.create("test_network", netrange.clone(), provider, assigner)?; let network = store.get("test_network")?.unwrap(); assert_eq!(network.name, "test_network"); assert_eq!(network.netrange, netrange); assert_eq!(network.provider_type, "basic"); assert!(network.assigner_type.is_some()); Ok(()) } #[test] fn test_create_network_without_assigner() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv6("2001:db8::".parse().unwrap(), 64)?; let provider = BasicProvider::new(); let assigner: Option = None; store.create("ipv6_network", netrange.clone(), provider, assigner)?; let network = store.get("ipv6_network")?.unwrap(); assert_eq!(network.name, "ipv6_network"); assert_eq!(network.netrange, netrange); assert!(network.assigner_type.is_none()); assert!(network.assigner_config.is_none()); Ok(()) } #[test] fn test_create_duplicate_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("10.0.0.0".parse().unwrap(), 8)?; let provider = BasicProvider::new(); let assigner: Option = None; // First creation should succeed store.create("duplicate_test", netrange.clone(), provider.clone(), assigner.clone())?; // Second creation should fail let result = store.create("duplicate_test", netrange, provider, assigner); assert!(result.is_err()); Ok(()) } #[test] fn test_update_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create initial network let netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 12)?; let provider = BasicProvider::new().with_config("version", "1"); let assigner: Option = None; store.create("update_test", netrange, provider, assigner)?; // Update the network let new_netrange = NetRange::ipv4("172.16.0.0".parse().unwrap(), 16)?; let new_provider = BasicProvider::new().with_config("version", "2"); let new_assigner = Some(BasicAssigner::new("random")); store.update("update_test", new_netrange.clone(), new_provider, new_assigner)?; // Verify the update let network = store.get("update_test")?.unwrap(); assert_eq!(network.netrange, new_netrange); assert!(network.assigner_type.is_some()); Ok(()) } #[test] fn test_delete_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("203.0.113.0".parse().unwrap(), 24)?; let provider = BasicProvider::new(); let assigner: Option = None; store.create("delete_test", netrange, provider, assigner)?; assert!(store.exists("delete_test")?); let deleted = store.delete("delete_test")?; assert!(deleted); assert!(!store.exists("delete_test")?); // Try to delete non-existent network let deleted_again = store.delete("delete_test")?; assert!(!deleted_again); Ok(()) } #[test] fn test_list_networks() -> Result<()> { let (store, _temp_dir) = create_test_store()?; // Create multiple networks let networks = vec![ ("net1", "10.1.0.0/16"), ("net2", "10.2.0.0/16"), ("net3", "2001:db8:1::/48"), ]; for (name, cidr) in &networks { let netrange = NetRange::from_cidr(cidr)?; let provider = BasicProvider::new(); let assigner: Option = None; store.create(name, netrange, provider, assigner)?; } let mut network_names = store.list()?; network_names.sort(); let mut expected: Vec = networks.iter().map(|(name, _)| name.to_string()).collect(); expected.sort(); assert_eq!(network_names, expected); Ok(()) } #[test] fn test_update_nonexistent_network() -> Result<()> { let (store, _temp_dir) = create_test_store()?; let netrange = NetRange::ipv4("198.51.100.0".parse().unwrap(), 24)?; let provider = BasicProvider::new(); let assigner: Option = None; let result = store.update("nonexistent", netrange, provider, assigner); assert!(result.is_err()); Ok(()) } #[test] fn test_invalid_cidr() { let result = NetRange::from_cidr("invalid"); assert!(result.is_err()); let result = NetRange::from_cidr("192.168.1.0"); assert!(result.is_err()); let result = NetRange::from_cidr("192.168.1.0/33"); assert!(result.is_err()); } + + #[test] + fn test_range_assigner() -> Result<()> { + let (store, _temp_dir) = create_test_store()?; + + // Create a network with RangeAssigner + let netrange = NetRange::from_cidr("192.168.1.0/24")?; + let provider = BasicProvider::new() + .with_config("type", "test"); + let assigner = RangeAssigner::new("test-network-range") + .with_config("strategy", "sequential"); + + store.create("test-network", netrange, provider, Some(assigner))?; + + // Verify the network was created + let network = store.get("test-network")?.unwrap(); + assert_eq!(network.name, "test-network"); + assert_eq!(network.assigner_type, Some("range".to_string())); + + // Test range assignment functionality + if let Some(ref range_store) = store.ranges { + let range_assigner = RangeAssigner::new("test-network-range"); + + // Assign some IPs + let bit1 = range_assigner.assign_ip(range_store, "device1")?; + let bit2 = range_assigner.assign_ip(range_store, "device2")?; + + // Verify assignments + assert_eq!(bit1, 0); + assert_eq!(bit2, 1); + + // Get assignment info + let assignment1 = range_assigner.get_assignment(range_store, bit1)?.unwrap(); + let assignment2 = range_assigner.get_assignment(range_store, bit2)?.unwrap(); + + assert_eq!(assignment1, "device1"); + assert_eq!(assignment2, "device2"); + + // Test unassignment + let unassigned = range_assigner.unassign_ip(range_store, bit1)?; + assert!(unassigned); + let assignment1_after = range_assigner.get_assignment(range_store, bit1)?; + assert!(assignment1_after.is_none()); + } + + Ok(()) + } + + #[test] + fn test_range_assigner_serialization() -> Result<()> { + let assigner = RangeAssigner::new("test-range") + .with_config("strategy", "random") + .with_config("pool_size", "100"); + + // Test serialization + let json = assigner.to_json()?; + assert!(json.contains("test-range")); + assert!(json.contains("random")); + assert!(json.contains("100")); + + // Test deserialization + let deserialized: RangeAssigner = RangeAssigner::from_json(&json)?; + assert_eq!(deserialized.range_name, "test-range"); + assert_eq!(deserialized.config.get("strategy"), Some(&"random".to_string())); + assert_eq!(deserialized.config.get("pool_size"), Some(&"100".to_string())); + + Ok(()) + } } \ No newline at end of file diff --git a/crates/store/src/store.rs b/crates/store/src/store.rs index 0fe9637..879e3d6 100644 --- a/crates/store/src/store.rs +++ b/crates/store/src/store.rs @@ -1,58 +1,59 @@ use crate::Result; #[derive(Debug, Clone)] pub struct Db { prefix: String, db: sled::Db } #[derive(Debug, Clone)] pub struct Store { db: Db, namespaces: crate::namespace::NamespaceStore, ranges: crate::range::RangeStore, networks: crate::network::NetworkStore, } impl Db { pub fn open(path: String, prefix: String) -> Result { let db = sled::open(path)?; Ok(Db { prefix, db }) } pub fn open_tree(&self, name: &str) -> Result { Ok(self.db.open_tree(self.tree_path(name))?) } pub fn tree_path(&self, name: &str) -> String { format!("t1/{}/{}", self.prefix, name) } } pub fn open() -> Result { let db = Db::open("libcollar_store".to_string(), "bonefire".to_string())?; + let ranges = crate::range::RangeStore::open(&db.db)?; Ok(Store { namespaces: crate::namespace::NamespaceStore::open(&db.db)?, - ranges: crate::range::RangeStore::open(&db.db)?, - networks: crate::network::NetworkStore::open(&db.db)?, + ranges: ranges.clone(), + networks: crate::network::NetworkStore::open_with_ranges(&db.db, ranges)?, db: db, }) } impl Store { fn tree_path(&self, name: &str) -> String { self.db.tree_path(name) } pub fn namespaces(&self) -> &crate::namespace::NamespaceStore { &self.namespaces } pub fn ranges(&self) -> &crate::range::RangeStore { &self.ranges } pub fn networks(&self) -> &crate::network::NetworkStore { &self.networks } } diff --git a/docs/range_assigner.md b/docs/range_assigner.md new file mode 100644 index 0000000..f6dfe89 --- /dev/null +++ b/docs/range_assigner.md @@ -0,0 +1,163 @@ +# RangeAssigner Documentation + +The `RangeAssigner` is a `NetworkAssigner` implementation that provides automatic IP address assignment using the `RangeStore` for efficient bitmap-based tracking. + +## Overview + +The `RangeAssigner` integrates with the network management system to provide: + +- Automatic IP range initialization based on network CIDR blocks +- Sequential IP address assignment within network ranges +- Assignment tracking with custom identifiers +- IP address unassignment and reuse capabilities +- Integration with the `NetworkStore` lifecycle + +## Basic Usage + +### Creating a Network with RangeAssigner + +```rust +use store::network::{RangeAssigner, NetRange, BasicProvider, NetworkStore}; + +// Create a RangeAssigner +let assigner = RangeAssigner::new("production-network-pool") + .with_config("strategy", "sequential") + .with_config("reserve_gateway", "true"); + +// Create network with IPv4 range +let netrange = NetRange::from_cidr("10.0.1.0/24")?; +let provider = BasicProvider::new() + .with_config("type", "aws") + .with_config("region", "us-west-2"); + +// The range is automatically initialized when creating the network +store.create("production", netrange, provider, Some(assigner))?; +``` + +### Assigning IP Addresses + +```rust +// Create assigner instance for operations +let range_assigner = RangeAssigner::new("production-network-pool"); + +// Assign IPs to devices +let web_server_ip = range_assigner.assign_ip(store.ranges(), "web-server-01")?; +let db_server_ip = range_assigner.assign_ip(store.ranges(), "database-01")?; +let cache_server_ip = range_assigner.assign_ip(store.ranges(), "redis-cache-01")?; + +println!("Web server assigned bit position: {}", web_server_ip); +println!("Database assigned bit position: {}", db_server_ip); +``` + +### Managing Assignments + +```rust +// Check what's assigned to a specific bit position +let assignment = range_assigner.get_assignment(store.ranges(), web_server_ip)?; +match assignment { + Some(identifier) => println!("Bit {} assigned to: {}", web_server_ip, identifier), + None => println!("Bit {} is available", web_server_ip), +} + +// Unassign an IP when device is decommissioned +let unassigned = range_assigner.unassign_ip(store.ranges(), web_server_ip)?; +if unassigned { + println!("Successfully freed IP assignment"); +} +``` + +## Configuration Options + +The `RangeAssigner` supports various configuration options through the `with_config()` method: + +| Key | Description | Example Values | +|-----|-------------|----------------| +| `strategy` | Assignment strategy | `"sequential"`, `"random"` | +| `reserve_gateway` | Reserve first IP for gateway | `"true"`, `"false"` | +| `pool_size` | Maximum IPs to manage | `"100"`, `"1000"` | +| `start_offset` | Skip first N addresses | `"1"`, `"10"` | + +## Range Initialization + +When a network is created with a `RangeAssigner`, the range is automatically initialized based on the network's CIDR block: + +- **IPv4**: Range size = 2^(32 - prefix_length) +- **IPv6**: Range size = min(2^(128 - prefix_length), 2^32) (capped for practicality) + +### Examples + +| CIDR | Usable IPs | Range Size | +|------|------------|------------| +| `192.168.1.0/24` | 254 | 256 | +| `10.0.0.0/16` | 65,534 | 65,536 | +| `172.16.0.0/12` | 1,048,574 | 1,048,576 | +| `2001:db8::/64` | Large | 2^32 (capped) | + +## Converting Bit Positions to IP Addresses + +The `assign_ip()` method returns a bit position within the range. To convert this to an actual IP address: + +```rust +// For IPv4 network 192.168.1.0/24 +let network_base = Ipv4Addr::new(192, 168, 1, 0); +let bit_position = range_assigner.assign_ip(store.ranges(), "device-1")?; + +// Calculate actual IP (typically skip network address) +let actual_ip = Ipv4Addr::from(u32::from(network_base) + bit_position as u32 + 1); +println!("Device assigned IP: {}", actual_ip); // e.g., 192.168.1.1 +``` + +## Error Handling + +The `RangeAssigner` can return several types of errors: + +- **Range Full**: When all IP addresses in the range are assigned +- **Range Not Found**: When the specified range doesn't exist +- **Invalid Network**: When network parameters are invalid +- **Serialization Errors**: When JSON serialization/deserialization fails + +```rust +match range_assigner.assign_ip(store.ranges(), "new-device") { + Ok(bit_position) => println!("Assigned bit: {}", bit_position), + Err(Error::RangeFull(range_name)) => { + println!("Range '{}' is full, no IPs available", range_name); + } + Err(e) => println!("Assignment failed: {:?}", e), +} +``` + +## Integration with NetworkStore + +The `RangeAssigner` is fully integrated with the `NetworkStore` lifecycle: + +1. **Creation**: Range is automatically initialized when network is created +2. **Updates**: Range configuration can be updated with network updates +3. **Deletion**: Consider manually cleaning up ranges when networks are deleted + +## Best Practices + +### Range Naming +- Use descriptive, unique names for ranges +- Consider including network name: `"production-web-tier-range"` +- Avoid special characters that might cause issues + +### IP Management +- Always check return values from assignment operations +- Implement proper cleanup when devices are removed +- Monitor range utilization to prevent exhaustion + +### Configuration +- Set appropriate `pool_size` limits for large networks +- Use `reserve_gateway` for networks needing gateway addresses +- Document configuration choices for operational clarity + +## Thread Safety + +The `RangeAssigner` uses the underlying `RangeStore` which provides thread-safe operations through `sled`'s transactional system. Multiple `RangeAssigner` instances can safely operate on the same range concurrently. + +## Performance Considerations + +- Assignment operations are O(n) in worst case, where n is the range size +- Consider smaller subnet allocations for very large networks +- Range lookups and unassignments are generally fast O(1) operations +- Bitmap storage is memory-efficient even for large ranges \ No newline at end of file diff --git a/examples/range_assigner_example.rs b/examples/range_assigner_example.rs new file mode 100644 index 0000000..59ea2ce --- /dev/null +++ b/examples/range_assigner_example.rs @@ -0,0 +1,65 @@ +use store::{open, Result, RangeAssigner}; +use store::network::{NetRange, BasicProvider, NetworkStore}; + +fn main() -> Result<()> { + // Open the store + let store = open()?; + + // Create a network with RangeAssigner + let netrange = NetRange::from_cidr("10.0.0.0/24")?; + let provider = BasicProvider::new() + .with_config("type", "docker") + .with_config("subnet", "bridge"); + + let assigner = RangeAssigner::new("docker-network-range") + .with_config("strategy", "sequential") + .with_config("reserve_gateway", "true"); + + // Create the network - this will automatically initialize the range + store.networks().create("docker-network", netrange, provider, Some(assigner))?; + + println!("Created network 'docker-network' with range 10.0.0.0/24"); + + // Create another RangeAssigner instance to work with the range + let range_assigner = RangeAssigner::new("docker-network-range"); + + // Assign IP addresses to devices + let container1_ip = range_assigner.assign_ip(store.ranges(), "container-web-1")?; + let container2_ip = range_assigner.assign_ip(store.ranges(), "container-db-1")?; + let container3_ip = range_assigner.assign_ip(store.ranges(), "container-cache-1")?; + + println!("Assigned IP addresses:"); + println!(" container-web-1: 10.0.0.{}", container1_ip + 1); // +1 to skip network address + println!(" container-db-1: 10.0.0.{}", container2_ip + 1); + println!(" container-cache-1: 10.0.0.{}", container3_ip + 1); + + // Retrieve assignment information + let web_assignment = range_assigner.get_assignment(store.ranges(), container1_ip)?; + let db_assignment = range_assigner.get_assignment(store.ranges(), container2_ip)?; + + println!("\nAssignment verification:"); + println!(" Bit {} assigned to: {:?}", container1_ip, web_assignment); + println!(" Bit {} assigned to: {:?}", container2_ip, db_assignment); + + // Unassign an IP (e.g., when container is removed) + let unassigned = range_assigner.unassign_ip(store.ranges(), container2_ip)?; + println!("\nUnassigned container-db-1: {}", unassigned); + + // Verify unassignment + let db_assignment_after = range_assigner.get_assignment(store.ranges(), container2_ip)?; + println!("DB assignment after unassign: {:?}", db_assignment_after); + + // Assign a new container to the freed IP + let new_container_ip = range_assigner.assign_ip(store.ranges(), "container-api-1")?; + println!("New container assigned to bit: {}", new_container_ip); + + // List all networks + let networks = store.networks().list()?; + println!("\nAll networks: {:?}", networks); + + // Get network details + let network = store.networks().get("docker-network")?.unwrap(); + println!("Network details: {:#?}", network); + + Ok(()) +} \ No newline at end of file