Page MenuHomePhabricator

No OneTemporary

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<Self> {
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<Self> {
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<Self> {
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::<Ipv4Addr>() {
Self::ipv4(ipv4, prefix)
} else if let Ok(ipv6) = parts[0].parse::<Ipv6Addr>() {
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<String>;
/// Create provider from JSON
fn from_json(json: &str) -> Result<Self> 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<String>;
/// Create assigner from JSON
fn from_json(json: &str) -> Result<Self> where Self: Sized;
}
/// Basic network provider implementation
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BasicProvider {
pub config: HashMap<String, String>,
}
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<String> {
serde_json::to_string(self).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})
}
fn from_json(json: &str) -> Result<Self> {
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<String, String>,
}
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<String> {
serde_json::to_string(self).map_err(|e| {
Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
})
}
fn from_json(json: &str) -> Result<Self> {
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<String, String>,
+}
+
+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<u64> {
+ 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<Option<String>> {
+ 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<bool> {
+ 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<String> {
+ serde_json::to_string(self).map_err(|e| {
+ Error::StoreError(sled::Error::Unsupported(format!("JSON serialization error: {}", e)))
+ })
+ }
+
+ fn from_json(json: &str) -> Result<Self> {
+ 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<String>,
pub assigner_config: Option<String>,
}
/// 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<crate::range::RangeStore>,
}
impl NetworkStore {
/// Open a new network store
pub fn open(db: &sled::Db) -> Result<Self> {
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<Self> {
+ 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<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> 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<Option<Network>> {
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<P, A>(&self, name: &str, netrange: NetRange, provider: P, assigner: Option<A>) -> 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<bool> {
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<Vec<String>> {
self.namespaces.list_keys("networks")
}
/// Check if a network exists
pub fn exists(&self, name: &str) -> Result<bool> {
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<BasicAssigner> = 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<BasicAssigner> = 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<BasicAssigner> = 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<BasicAssigner> = 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<BasicAssigner> = None;
store.create(name, netrange, provider, assigner)?;
}
let mut network_names = store.list()?;
network_names.sort();
let mut expected: Vec<String> = 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<BasicAssigner> = 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<Self> {
let db = sled::open(path)?;
Ok(Db { prefix, db })
}
pub fn open_tree(&self, name: &str) -> Result<sled::Tree> {
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<Store> {
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

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jun 9, 7:57 AM (8 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47607
Default Alt Text
(42 KB)

Event Timeline