Page MenuHomePhabricator

No OneTemporary

diff --git a/crates/store/src/network.rs b/crates/store/src/network.rs
index ea08989..7de0da3 100644
--- a/crates/store/src/network.rs
+++ b/crates/store/src/network.rs
@@ -1,868 +1,1338 @@
//! 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, RangeStore};
use sled::Transactional;
use std::collections::HashMap;
-use std::net::{Ipv4Addr, Ipv6Addr};
+use std::net::{Ipv4Addr, Ipv6Addr, IpAddr};
/// 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;
+ /// Initialize the assigner for a network during creation
+ fn initialize_for_network(&self, _network_name: &str, _netrange: &NetRange, _range_store: Option<&RangeStore>) -> Result<()> {
+ // Default implementation does nothing
+ Ok(())
+ }
+
+ /// Initialize the assigner for a network within a transaction
+ fn initialize_for_network_in_transaction(
+ &self,
+ _network_name: &str,
+ _netrange: &NetRange,
+ _range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
+ ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
+ // Default implementation does nothing
+ Ok(())
+ }
+
+ /// Get an IP assignment for the given identifier
+ fn get_assignment(&self, _network: &Network, _identifier: &str, _range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
+ // Default implementation returns None (no assignment tracking)
+ Ok(None)
+ }
}
/// 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")
+/// // Create a RangeAssigner for a network (uses network name as range name)
+/// let assigner = RangeAssigner::new()
/// .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")?;
+/// let range_assigner = RangeAssigner::new();
+/// let ip_bit = range_assigner.assign_ip("my-network", &ranges, "device1")?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RangeAssigner {
- pub range_name: String,
+ pub range_name: Option<String>,
pub config: HashMap<String, String>,
}
impl RangeAssigner {
- /// Create a new RangeAssigner with the specified range name
+ /// Create a new RangeAssigner with default settings
///
- /// 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.
+ /// The range name will default to the network name when the network is created.
+ /// Use `with_range_name()` to specify a custom range name if needed.
+ pub fn new() -> Self {
+ Self {
+ range_name: None,
+ config: HashMap::new(),
+ }
+ }
+
+ /// Create a new RangeAssigner with a specific range name
///
/// # Arguments
///
- /// * `range_name` - Unique identifier for the IP range
- pub fn new(range_name: &str) -> Self {
+ /// * `range_name` - Custom identifier for the IP range
+ pub fn with_range_name(range_name: &str) -> Self {
Self {
- range_name: range_name.to_string(),
+ range_name: Some(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_name` - Name to use for the range
/// * `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<()> {
+ pub fn initialize_range(&self, range_name: &str, 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)
+ range_store.define(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_name` - Name of the range to assign from
/// * `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)
+ pub fn assign_ip(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<u64> {
+ range_store.assign(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_name` - Name of the range to query
/// * `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)
+ pub fn get_assignment_by_bit(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<Option<String>> {
+ range_store.get(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_name` - Name of the range to unassign from
/// * `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)
+ pub fn unassign_ip(&self, range_name: &str, range_store: &RangeStore, bit_position: u64) -> Result<bool> {
+ range_store.unassign_bit(range_name, bit_position)
+ }
+
+ /// Get the range name for this assigner, using the network name as default
+ ///
+ /// # Arguments
+ ///
+ /// * `network_name` - Network name to use as default if no custom range name is set
+ ///
+ /// # Returns
+ ///
+ /// Returns the range name to use for operations
+ pub fn get_range_name(&self, network_name: &str) -> String {
+ self.range_name.clone().unwrap_or_else(|| network_name.to_string())
+ }
+
+ /// Convert a bit position to an IP address within the network range
+ ///
+ /// # Arguments
+ ///
+ /// * `bit_position` - The bit position within the range
+ /// * `netrange` - The network range to calculate the IP within
+ ///
+ /// # Returns
+ ///
+ /// Returns the IP address corresponding to the bit position
+ pub fn bit_position_to_ip(&self, bit_position: u64, netrange: &NetRange) -> Result<IpAddr> {
+ match netrange {
+ NetRange::V4 { addr, prefix: _ } => {
+ let network_addr = u32::from(*addr);
+ let host_addr = network_addr + bit_position as u32;
+ Ok(IpAddr::V4(Ipv4Addr::from(host_addr)))
+ }
+ NetRange::V6 { addr, prefix: _ } => {
+ let network_addr = u128::from(*addr);
+ let host_addr = network_addr + bit_position as u128;
+ Ok(IpAddr::V6(Ipv6Addr::from(host_addr)))
+ }
+ }
+ }
+
+ /// Find the bit position for a given identifier in the range
+ ///
+ /// # Arguments
+ ///
+ /// * `range_name` - Name of the range to search
+ /// * `range_store` - Reference to the RangeStore
+ /// * `identifier` - The identifier to search for
+ ///
+ /// # Returns
+ ///
+ /// Returns the bit position if found, None otherwise
+ pub fn find_identifier_bit_position(&self, range_name: &str, range_store: &RangeStore, identifier: &str) -> Result<Option<u64>> {
+ // Get range ID first
+ let range_id = match range_store.names.get(range_name)? {
+ Some(data) => {
+ if data.len() != 16 {
+ return Ok(None);
+ }
+ let id_bytes: [u8; 8] = data[0..8].try_into()
+ .map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid range data".to_string())))?;
+ u64::from_be_bytes(id_bytes)
+ }
+ None => return Ok(None),
+ };
+
+ // Search for the assignment with this identifier
+ let prefix = range_id.to_be_bytes();
+
+ for result in range_store.assign.scan_prefix(&prefix) {
+ let (key, stored_value) = result?;
+ if key.len() == 16 {
+ let stored_value_str = String::from_utf8(stored_value.to_vec())?;
+ if stored_value_str == identifier {
+ let bit_position_bytes: [u8; 8] = key[8..16].try_into()
+ .map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid key format".to_string())))?;
+ return Ok(Some(u64::from_be_bytes(bit_position_bytes)));
+ }
+ }
+ }
+
+ Ok(None)
}
}
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)))
})
}
+
+ fn initialize_for_network(&self, network_name: &str, netrange: &NetRange, range_store: Option<&RangeStore>) -> Result<()> {
+ if let Some(store) = range_store {
+ let range_name = self.get_range_name(network_name);
+ self.initialize_range(&range_name, store, netrange)?;
+ }
+ Ok(())
+ }
+
+ fn initialize_for_network_in_transaction(
+ &self,
+ network_name: &str,
+ netrange: &NetRange,
+ range_trees: Option<(&sled::transaction::TransactionalTree, &sled::transaction::TransactionalTree)>
+ ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
+ if let Some((range_names, range_map)) = range_trees {
+ let range_name = self.get_range_name(network_name);
+ let size = match netrange {
+ NetRange::V4 { prefix, .. } => {
+ if *prefix >= 32 {
+ return Err(sled::transaction::ConflictableTransactionError::Abort(()));
+ }
+ 1u64 << (32 - prefix)
+ }
+ NetRange::V6 { prefix, .. } => {
+ if *prefix >= 128 {
+ return Err(sled::transaction::ConflictableTransactionError::Abort(()));
+ }
+ // 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
+ }
+ }
+ };
+
+ // Define the range within the transaction
+ match range_names.get(range_name.as_bytes())? {
+ Some(_) => Ok(()), // Range already exists
+ None => {
+ let id = range_names.generate_id()?;
+
+ // Store name -> (id, size)
+ let mut value = Vec::new();
+ value.extend_from_slice(&id.to_be_bytes());
+ value.extend_from_slice(&size.to_be_bytes());
+ range_names.insert(range_name.as_bytes(), value)?;
+
+ // Initialize bitmap for the range (all zeros)
+ let bitmap_size = (size + 7) / 8; // Round up to nearest byte
+ let bitmap = vec![0u8; bitmap_size as usize];
+ range_map.insert(&id.to_be_bytes(), bitmap)?;
+
+ Ok(())
+ }
+ }
+ } else {
+ Ok(())
+ }
+ }
+
+ fn get_assignment(&self, network: &Network, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
+ if let Some(store) = range_store {
+ let range_name = self.get_range_name(&network.name);
+
+ // Find the bit position for this identifier
+ if let Some(bit_position) = self.find_identifier_bit_position(&range_name, store, identifier)? {
+ // Convert bit position to IP address
+ let ip = self.bit_position_to_ip(bit_position, &network.netrange)?;
+ Ok(Some(ip))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(None)
+ }
+ }
}
/// 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>,
}
+impl Network {
+ /// Get an IP assignment for the given identifier
+ ///
+ /// This method calls the appropriate assigner implementation to retrieve
+ /// the assigned IP address for a given identifier. Different assigners
+ /// may return the IP in different formats (bit position or full IP).
+ ///
+ /// # Arguments
+ ///
+ /// * `identifier` - The identifier to look up (e.g., device name, container ID)
+ /// * `range_store` - Optional reference to RangeStore (required for RangeAssigner)
+ ///
+ /// # Returns
+ ///
+ /// Returns the assigned IP address if found, None if no assignment exists
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use store::network::{Network, NetRange, RangeAssigner};
+ /// # 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 = store::network::NetworkStore::open_with_ranges(&db, ranges.clone())?;
+ /// # let netrange = NetRange::from_cidr("192.168.1.0/24")?;
+ /// # let provider = store::network::BasicProvider::new();
+ /// # let assigner = RangeAssigner::new();
+ /// # store.create("test", netrange, provider, Some(assigner))?;
+ /// # let network = store.get("test")?.unwrap();
+ ///
+ /// let ip = network.get_assignment("device1", Some(&ranges))?;
+ /// if let Some(addr) = ip {
+ /// println!("Device1 is assigned IP: {}", addr);
+ /// }
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn get_assignment(&self, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>> {
+ if let (Some(assigner_type), Some(assigner_config)) = (&self.assigner_type, &self.assigner_config) {
+ match assigner_type.as_str() {
+ "range" => {
+ let range_assigner: RangeAssigner = serde_json::from_str(assigner_config)
+ .map_err(|e| Error::StoreError(sled::Error::Unsupported(
+ format!("Failed to deserialize RangeAssigner: {}", e)
+ )))?;
+ range_assigner.get_assignment(self, identifier, range_store)
+ }
+ "basic" => {
+ let basic_assigner: BasicAssigner = serde_json::from_str(assigner_config)
+ .map_err(|e| Error::StoreError(sled::Error::Unsupported(
+ format!("Failed to deserialize BasicAssigner: {}", e)
+ )))?;
+ basic_assigner.get_assignment(self, identifier, range_store)
+ }
+ _ => {
+ // Unknown assigner type
+ Ok(None)
+ }
+ }
+ } else {
+ // No assigner configured
+ Ok(None)
+ }
+ }
+
+ /// Get an IP address from a bit position in the network range
+ ///
+ /// This method converts a bit position to the corresponding IP address
+ /// within this network's range. Useful when you already know the bit
+ /// position from range operations.
+ ///
+ /// # Arguments
+ ///
+ /// * `bit_position` - The bit position within the network range
+ ///
+ /// # Returns
+ ///
+ /// Returns the IP address corresponding to the bit position
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// # use store::network::{Network, NetRange};
+ /// # fn example() -> store::Result<()> {
+ /// # let netrange = NetRange::from_cidr("192.168.1.0/24")?;
+ /// let network = Network {
+ /// name: "test".to_string(),
+ /// netrange: netrange,
+ /// provider_type: "basic".to_string(),
+ /// provider_config: "{}".to_string(),
+ /// assigner_type: None,
+ /// assigner_config: None,
+ /// };
+ ///
+ /// let ip = network.get_assignment_by_bit_position(5)?;
+ /// println!("Bit position 5 corresponds to IP: {}", ip);
+ /// # Ok(())
+ /// # }
+ /// ```
+ pub fn get_assignment_by_bit_position(&self, bit_position: u64) -> Result<IpAddr> {
+ match &self.netrange {
+ NetRange::V4 { addr, prefix: _ } => {
+ let network_addr = u32::from(*addr);
+ let host_addr = network_addr + bit_position as u32;
+ Ok(IpAddr::V4(Ipv4Addr::from(host_addr)))
+ }
+ NetRange::V6 { addr, prefix: _ } => {
+ let network_addr = u128::from(*addr);
+ let host_addr = network_addr + bit_position as u128;
+ Ok(IpAddr::V6(Ipv6Addr::from(host_addr)))
+ }
+ }
+ }
+}
+
/// 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,
+ netrange: netrange.clone(),
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())
+ // Include range store trees in transaction if needed
+ if let (Some(assigner_ref), Some(range_store)) = (&assigner, &self.ranges) {
+ if assigner_ref.assigner_type() == "range" {
+ // Transaction with both network and range stores
+ (&self.namespaces.names, &self.namespaces.spaces, &self.networks, &range_store.names, &range_store.map).transaction(
+ |(names, spaces, networks, range_names, range_map)| {
+ // 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(()));
+ }
+
+ // Initialize the assigner within the transaction
+ assigner_ref.initialize_for_network_in_transaction(name, &netrange, Some((range_names, range_map)))?;
+
+ // 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),
+ })?;
+ } else {
+ // Transaction without range store
+ (&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),
+ })?;
}
- sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
- })?;
+ } else {
+ // Transaction without assigner or range store
+ (&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 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")
+ let assigner = RangeAssigner::new()
.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");
+ let range_assigner = RangeAssigner::new();
+ let range_name = "test-network"; // Uses network name as range name
// Assign some IPs
- let bit1 = range_assigner.assign_ip(range_store, "device1")?;
- let bit2 = range_assigner.assign_ip(range_store, "device2")?;
+ let bit1 = range_assigner.assign_ip(range_name, range_store, "device1")?;
+ let bit2 = range_assigner.assign_ip(range_name, 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();
+ let assignment1 = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?.unwrap();
+ let assignment2 = range_assigner.get_assignment_by_bit(range_name, range_store, bit2)?.unwrap();
assert_eq!(assignment1, "device1");
assert_eq!(assignment2, "device2");
// Test unassignment
- let unassigned = range_assigner.unassign_ip(range_store, bit1)?;
+ let unassigned = range_assigner.unassign_ip(range_name, range_store, bit1)?;
assert!(unassigned);
- let assignment1_after = range_assigner.get_assignment(range_store, bit1)?;
+ let assignment1_after = range_assigner.get_assignment_by_bit(range_name, range_store, bit1)?;
assert!(assignment1_after.is_none());
}
Ok(())
}
#[test]
fn test_range_assigner_serialization() -> Result<()> {
- let assigner = RangeAssigner::new("test-range")
+ let assigner = RangeAssigner::with_range_name("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.range_name, Some("test-range".to_string()));
assert_eq!(deserialized.config.get("strategy"), Some(&"random".to_string()));
assert_eq!(deserialized.config.get("pool_size"), Some(&"100".to_string()));
Ok(())
}
+
+ #[test]
+ fn test_network_get_assignment() -> 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()
+ .with_config("strategy", "sequential");
+
+ store.create("test-network", netrange, provider, Some(assigner))?;
+
+ // Get the network
+ let network = store.get("test-network")?.unwrap();
+
+ // Test that no assignment exists initially
+ let ip = network.get_assignment("device1", store.ranges.as_ref())?;
+ assert!(ip.is_none());
+
+ // Assign an IP using the range assigner directly
+ if let Some(ref range_store) = store.ranges {
+ let range_assigner = RangeAssigner::new();
+ let range_name = "test-network"; // Uses network name as range name
+
+ // Assign an IP to device1
+ let bit_position = range_assigner.assign_ip(range_name, range_store, "device1")?;
+ assert_eq!(bit_position, 0);
+
+ // Now test get_assignment on the network
+ let ip = network.get_assignment("device1", store.ranges.as_ref())?;
+ assert!(ip.is_some());
+
+ let assigned_ip = ip.unwrap();
+ match assigned_ip {
+ std::net::IpAddr::V4(ipv4) => {
+ // Should be 192.168.1.0 + bit_position
+ let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position as u8);
+ assert_eq!(ipv4, expected);
+ }
+ _ => panic!("Expected IPv4 address"),
+ }
+
+ // Test assignment for non-existent device
+ let no_ip = network.get_assignment("nonexistent", store.ranges.as_ref())?;
+ assert!(no_ip.is_none());
+
+ // Assign another device
+ let bit_position2 = range_assigner.assign_ip(range_name, range_store, "device2")?;
+ assert_eq!(bit_position2, 1);
+
+ let ip2 = network.get_assignment("device2", store.ranges.as_ref())?;
+ assert!(ip2.is_some());
+
+ let assigned_ip2 = ip2.unwrap();
+ match assigned_ip2 {
+ std::net::IpAddr::V4(ipv4) => {
+ let expected = std::net::Ipv4Addr::new(192, 168, 1, bit_position2 as u8);
+ assert_eq!(ipv4, expected);
+ }
+ _ => panic!("Expected IPv4 address"),
+ }
+ }
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_network_get_assignment_no_assigner() -> Result<()> {
+ let (store, _temp_dir) = create_test_store()?;
+
+ // Create a network without an assigner
+ let netrange = NetRange::from_cidr("192.168.1.0/24")?;
+ let provider = BasicProvider::new()
+ .with_config("type", "test");
+
+ store.create("no-assigner-network", netrange, provider, None::<RangeAssigner>)?;
+
+ // Get the network
+ let network = store.get("no-assigner-network")?.unwrap();
+
+ // Test that get_assignment returns None when no assigner is configured
+ let ip = network.get_assignment("device1", store.ranges.as_ref())?;
+ assert!(ip.is_none());
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_network_get_assignment_ipv6() -> Result<()> {
+ let (store, _temp_dir) = create_test_store()?;
+
+ // Create an IPv6 network with RangeAssigner
+ let netrange = NetRange::from_cidr("2001:db8::/64")?;
+ let provider = BasicProvider::new()
+ .with_config("type", "test");
+ let assigner = RangeAssigner::new()
+ .with_config("strategy", "sequential");
+
+ store.create("ipv6-network", netrange, provider, Some(assigner))?;
+
+ // Get the network
+ let network = store.get("ipv6-network")?.unwrap();
+
+ // Assign an IP using the range assigner directly
+ if let Some(ref range_store) = store.ranges {
+ let range_assigner = RangeAssigner::new();
+ let range_name = "ipv6-network";
+
+ // Assign an IP to device1
+ let bit_position = range_assigner.assign_ip(range_name, range_store, "ipv6-device1")?;
+ assert_eq!(bit_position, 0);
+
+ // Test get_assignment on the network
+ let ip = network.get_assignment("ipv6-device1", store.ranges.as_ref())?;
+ assert!(ip.is_some());
+
+ let assigned_ip = ip.unwrap();
+ match assigned_ip {
+ std::net::IpAddr::V6(ipv6) => {
+ // Should be 2001:db8:: + bit_position
+ let expected = std::net::Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, bit_position as u16);
+ assert_eq!(ipv6, expected);
+ }
+ _ => panic!("Expected IPv6 address"),
+ }
+ }
+
+ Ok(())
+ }
}
\ No newline at end of file
diff --git a/crates/store/src/range.rs b/crates/store/src/range.rs
index 6e8f114..c599615 100644
--- a/crates/store/src/range.rs
+++ b/crates/store/src/range.rs
@@ -1,915 +1,929 @@
//! # Range Store Module
//!
//! This module implements "ranges" which are buckets of unique range assignments
//! that map values to specific bit positions within fixed-size ranges. This is useful
//! for allocating unique identifiers, session tokens, or any scenario where you need
//! to assign sequential positions to values.
//!
//! ## Storage Design
//!
//! The RangeStore uses three sled trees:
//!
//! - **`names` tree**: Maps range names to their metadata
//! - Key: `name` (string)
//! - Value: `id || size` where `id` is a u64 from `generate_id()` and `size` is a u64
//!
//! - **`map` tree**: Stores bitmaps for each range to track allocated positions
//! - Key: `id` (u64 from the `names` tree)
//! - Value: `bitmap` (binary data representing which bits are allocated)
//!
//! - **`assign` tree**: Maps range positions to their assigned values
//! - Key: `id || bit_position` where `id` is the range ID and `bit_position` is a u64
//! - Value: `value` (the string value assigned to this position)
//!
//! ## API Operations
//!
//! - `define(name, size)`: Create a new range with the given name and maximum size
//! - `assign(name, value)`: Assign a value to the next available position in the range
//! - `get(name, position)`: Retrieve the value at a specific position
//! - `list_range(name)`: List all assignments in a range
//! - `list_ranges()`: List all defined ranges
//! - `unassign(name, value)`: Remove a value from the range and free its position
//!
//! ## Example Usage
//!
//! ```rust
//! use store::{open, Result};
//!
//! fn example() -> Result<()> {
//! let store = open()?;
//! let ranges = store.ranges();
//!
//! // Define a range for user IDs
//! ranges.define("user_ids", 1000)?;
//!
//! // Assign some users
//! let alice_id = ranges.assign("user_ids", "alice@example.com")?; // Returns 0
//! let bob_id = ranges.assign("user_ids", "bob@example.com")?; // Returns 1
//!
//! // Get a user by position
//! let email = ranges.get("user_ids", alice_id)?; // Some("alice@example.com")
//!
//! // Remove a user (frees the position for reuse)
//! ranges.unassign("user_ids", "bob@example.com")?;
//!
//! // Next assignment will reuse Bob's position
//! let charlie_id = ranges.assign("user_ids", "charlie@example.com")?; // Returns 1
//!
//! Ok(())
//! }
//! ```
use crate::Result;
use crate::Error;
use sled::Transactional;
#[derive(Debug, Clone)]
pub struct RangeStore {
pub(crate) names: sled::Tree,
pub(crate) map: sled::Tree,
pub(crate) assign: sled::Tree,
}
impl RangeStore {
pub fn open(db: &sled::Db) -> Result<Self> {
Ok(RangeStore {
names: db.open_tree("ranges/1/names")?,
map: db.open_tree("ranges/1/map")?,
assign: db.open_tree("ranges/1/assign")?,
})
}
/// Assign a value to a range within an existing transaction context
pub fn assign_in_transaction(
&self,
names: &sled::transaction::TransactionalTree,
map: &sled::transaction::TransactionalTree,
assign: &sled::transaction::TransactionalTree,
range_name: &str,
value: &str,
) -> sled::transaction::ConflictableTransactionResult<u64, ()> {
// Get range info
let (range_id, range_size) = match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
(u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))
}
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Get current bitmap
let mut bitmap = match map.get(&range_id.to_be_bytes())? {
Some(data) => data.to_vec(),
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Find first free bit
let mut bit_position = None;
for bit in 0..range_size {
let byte_index = (bit / 8) as usize;
let bit_index = bit % 8;
if byte_index < bitmap.len() {
let mask = 1u8 << bit_index;
if (bitmap[byte_index] & mask) == 0 {
// This bit is free
bitmap[byte_index] |= mask;
bit_position = Some(bit);
break;
}
}
}
let bit_pos = match bit_position {
Some(pos) => pos,
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Update bitmap
map.insert(&range_id.to_be_bytes(), bitmap)?;
// Store assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_pos.to_be_bytes());
assign.insert(assign_key, value.as_bytes())?;
Ok(bit_pos)
}
/// Get a value from a specific bit position within an existing transaction context
/// Get range assignment details within an existing transaction context
pub fn get_in_transaction(
&self,
names: &sled::transaction::TransactionalTree,
assign: &sled::transaction::TransactionalTree,
range_name: &str,
bit_position: u64,
) -> sled::transaction::ConflictableTransactionResult<Option<String>, ()> {
// Get range info
let (range_id, range_size) = match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(None);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
(u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))
}
None => return Ok(None),
};
if bit_position >= range_size {
return Ok(None);
}
// Get assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_position.to_be_bytes());
match assign.get(&assign_key)? {
Some(value_bytes) => {
let value = String::from_utf8(value_bytes.to_vec())
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
Ok(Some(value))
}
None => Ok(None),
}
}
/// Unassign a specific bit position within an existing transaction context
pub fn unassign_bit_in_transaction(
&self,
names: &sled::transaction::TransactionalTree,
map: &sled::transaction::TransactionalTree,
assign: &sled::transaction::TransactionalTree,
range_name: &str,
bit_position: u64,
) -> sled::transaction::ConflictableTransactionResult<bool, ()> {
// Get range info
let (range_id, range_size) = match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(false);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
(u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))
}
None => return Ok(false),
};
if bit_position >= range_size {
return Ok(false);
}
// Remove assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_position.to_be_bytes());
let removed = assign.remove(assign_key)?.is_some();
if removed {
// Update bitmap
let mut bitmap = match map.get(&range_id.to_be_bytes())? {
Some(data) => data.to_vec(),
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
let byte_index = (bit_position / 8) as usize;
let bit_index = bit_position % 8;
if byte_index < bitmap.len() {
let mask = 1u8 << bit_index;
bitmap[byte_index] &= !mask; // Clear the bit
map.insert(&range_id.to_be_bytes(), bitmap)?;
}
}
Ok(removed)
}
/// Check if a range exists within an existing transaction context
pub fn exists_in_transaction(
&self,
names: &sled::transaction::TransactionalTree,
range_name: &str,
) -> sled::transaction::ConflictableTransactionResult<bool, ()> {
Ok(names.get(range_name)?.is_some())
}
/// Get range info (id and size) within an existing transaction context
pub fn info_in_transaction(
&self,
names: &sled::transaction::TransactionalTree,
range_name: &str,
) -> sled::transaction::ConflictableTransactionResult<Option<(u64, u64)>, ()> {
match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(None);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
Ok(Some((u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))))
}
None => Ok(None),
}
}
// Define a new range with a given name and size
pub fn define(&self, name: &str, size: u64) -> Result<()> {
(&self.names, &self.map).transaction(|(names, map)| {
- match names.get(name)? {
- Some(_) => Ok(()), // Range already exists
- None => {
- let id = names.generate_id()?;
-
- // Store name -> (id, size)
- let mut value = Vec::new();
- value.extend_from_slice(&id.to_be_bytes());
- value.extend_from_slice(&size.to_be_bytes());
- names.insert(name, value)?;
-
- // Initialize bitmap for the range (all zeros)
- let bitmap_size = (size + 7) / 8; // Round up to nearest byte
- let bitmap = vec![0u8; bitmap_size as usize];
- map.insert(&id.to_be_bytes(), bitmap)?;
-
- Ok(())
- }
- }
+ self.define_in_transaction(names, map, name, size)
+ }).map_err(|e| match e {
+ sled::transaction::TransactionError::Abort(()) => Error::StoreError(sled::Error::Unsupported("Range definition failed".to_string())),
+ sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(())
}
+ /// Define a new range within an existing transaction
+ pub fn define_in_transaction(
+ &self,
+ names: &sled::transaction::TransactionalTree,
+ map: &sled::transaction::TransactionalTree,
+ name: &str,
+ size: u64,
+ ) -> Result<(), sled::transaction::ConflictableTransactionError<()>> {
+ match names.get(name)? {
+ Some(_) => Ok(()), // Range already exists
+ None => {
+ let id = names.generate_id()?;
+
+ // Store name -> (id, size)
+ let mut value = Vec::new();
+ value.extend_from_slice(&id.to_be_bytes());
+ value.extend_from_slice(&size.to_be_bytes());
+ names.insert(name, value)?;
+
+ // Initialize bitmap for the range (all zeros)
+ let bitmap_size = (size + 7) / 8; // Round up to nearest byte
+ let bitmap = vec![0u8; bitmap_size as usize];
+ map.insert(&id.to_be_bytes(), bitmap)?;
+
+ Ok(())
+ }
+ }
+ }
+
// Assign the next free bit in the range to a value
pub fn assign(&self, range_name: &str, value: &str) -> Result<u64> {
let result = (&self.names, &self.map, &self.assign).transaction(|(names, map, assign)| {
// Get range info
let (range_id, range_size) = match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
(u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))
}
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Get current bitmap
let mut bitmap = match map.get(&range_id.to_be_bytes())? {
Some(data) => data.to_vec(),
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Find first free bit
let mut bit_position = None;
for bit in 0..range_size {
let byte_index = (bit / 8) as usize;
let bit_index = bit % 8;
if byte_index < bitmap.len() {
let mask = 1u8 << bit_index;
if (bitmap[byte_index] & mask) == 0 {
// This bit is free
bitmap[byte_index] |= mask;
bit_position = Some(bit);
break;
}
}
}
let bit_pos = match bit_position {
Some(pos) => pos,
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
// Update bitmap
map.insert(&range_id.to_be_bytes(), bitmap)?;
// Store assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_pos.to_be_bytes());
assign.insert(assign_key, value.as_bytes())?;
Ok(bit_pos)
}).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => Error::RangeFull(range_name.to_string()),
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(result)
}
// Get a value from a specific bit position in the range
pub fn get(&self, range_name: &str, bit_position: u64) -> Result<Option<String>> {
let result = (&self.names, &self.assign).transaction(|(names, assign)| {
// Get range info
let (range_id, range_size) = match names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(None);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
let size_bytes: [u8; 8] = data[8..16].try_into()
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
(u64::from_be_bytes(id_bytes), u64::from_be_bytes(size_bytes))
}
None => return Ok(None),
};
if bit_position >= range_size {
return Err(sled::transaction::ConflictableTransactionError::Abort(()));
}
// Get assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_position.to_be_bytes());
match assign.get(assign_key)? {
Some(value_bytes) => {
let value = String::from_utf8(value_bytes.to_vec())
.map_err(|_| sled::transaction::ConflictableTransactionError::Abort(()))?;
Ok(Some(value))
}
None => Ok(None),
}
}).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => Error::BitOutOfRange(range_name.to_string(), bit_position),
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(result)
}
// List all assignments in a range
pub fn list_range(&self, range_name: &str) -> Result<Vec<(u64, String)>> {
// Get range info first
let range_id = match self.names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(Vec::new());
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid range data".to_string())))?;
u64::from_be_bytes(id_bytes)
}
None => return Ok(Vec::new()),
};
let mut assignments = Vec::new();
let prefix = range_id.to_be_bytes();
for result in self.assign.scan_prefix(&prefix) {
let (key, value) = result?;
if key.len() == 16 { // 8 bytes for range_id + 8 bytes for bit_position
let bit_position_bytes: [u8; 8] = key[8..16].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid key format".to_string())))?;
let bit_position = u64::from_be_bytes(bit_position_bytes);
let value_str = String::from_utf8(value.to_vec())?;
assignments.push((bit_position, value_str));
}
}
assignments.sort_by_key(|&(bit_pos, _)| bit_pos);
Ok(assignments)
}
// List all ranges
pub fn list_ranges(&self) -> Result<Vec<(String, u64)>> {
let mut ranges = Vec::new();
for result in self.names.iter() {
let (key, value) = result?;
if value.len() == 16 {
let name = String::from_utf8(key.to_vec())?;
let size_bytes: [u8; 8] = value[8..16].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid size data".to_string())))?;
let size = u64::from_be_bytes(size_bytes);
ranges.push((name, size));
}
}
ranges.sort_by(|a, b| a.0.cmp(&b.0));
Ok(ranges)
}
// Unassign a specific bit position from the range
pub fn unassign_bit(&self, range_name: &str, bit_position: u64) -> Result<bool> {
let result = (&self.names, &self.map, &self.assign).transaction(|(names, map, assign)| {
self.unassign_bit_in_transaction(names, map, assign, range_name, bit_position)
}).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => Error::BitOutOfRange(range_name.to_string(), bit_position),
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(result)
}
// Unassign a value from the range (find by value and remove)
pub fn unassign(&self, range_name: &str, value: &str) -> Result<bool> {
// First find the assignment outside of transaction
let range_id = match self.names.get(range_name)? {
Some(data) => {
if data.len() != 16 {
return Ok(false);
}
let id_bytes: [u8; 8] = data[0..8].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid range data".to_string())))?;
u64::from_be_bytes(id_bytes)
}
None => return Ok(false),
};
// Find the assignment with this value
let prefix = range_id.to_be_bytes();
let mut found_bit = None;
for result in self.assign.scan_prefix(&prefix) {
let (key, stored_value) = result?;
if key.len() == 16 {
let stored_value_str = String::from_utf8(stored_value.to_vec())?;
if stored_value_str == value {
let bit_position_bytes: [u8; 8] = key[8..16].try_into()
.map_err(|_| Error::StoreError(sled::Error::Unsupported("Invalid key format".to_string())))?;
found_bit = Some(u64::from_be_bytes(bit_position_bytes));
break;
}
}
}
let bit_position = match found_bit {
Some(pos) => pos,
None => return Ok(false),
};
// Now perform the removal in a transaction
let result = (&self.map, &self.assign).transaction(|(map, assign)| {
// Remove assignment
let mut assign_key = Vec::new();
assign_key.extend_from_slice(&range_id.to_be_bytes());
assign_key.extend_from_slice(&bit_position.to_be_bytes());
assign.remove(assign_key)?;
// Update bitmap
let mut bitmap = match map.get(&range_id.to_be_bytes())? {
Some(data) => data.to_vec(),
None => return Err(sled::transaction::ConflictableTransactionError::Abort(())),
};
let byte_index = (bit_position / 8) as usize;
let bit_index = bit_position % 8;
if byte_index < bitmap.len() {
let mask = 1u8 << bit_index;
bitmap[byte_index] &= !mask; // Clear the bit
map.insert(&range_id.to_be_bytes(), bitmap)?;
}
Ok(true)
}).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => Error::ValueNotInRange(range_name.to_string(), value.to_string()),
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
})?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn create_test_store() -> Result<RangeStore> {
let temp_dir = tempdir().unwrap();
let db = sled::open(temp_dir.path())?;
RangeStore::open(&db)
}
#[test]
fn test_transaction_methods() -> Result<()> {
let store = create_test_store()?;
// Define a range
store.define("test_range", 100)?;
// Test transaction methods
let result = (&store.names, &store.map, &store.assign).transaction(|(names, map, assign)| {
// Assign in transaction
let bit_pos = store.assign_in_transaction(names, map, assign, "test_range", "tx_value")?;
assert!(bit_pos < 100);
// Get in transaction
let value = store.get_in_transaction(names, assign, "test_range", bit_pos)?;
assert_eq!(value, Some("tx_value".to_string()));
// Check if range exists in transaction
let exists = store.exists_in_transaction(names, "test_range")?;
assert_eq!(exists, true);
// Get range info in transaction
let info = store.info_in_transaction(names, "test_range")?;
assert!(info.is_some());
let (_, size) = info.unwrap();
assert_eq!(size, 100);
// Verify assignment exists
let value_check = store.get_in_transaction(names, assign, "test_range", bit_pos)?;
assert_eq!(value_check, Some("tx_value".to_string()));
// Unassign bit in transaction
let unassigned = store.unassign_bit_in_transaction(names, map, assign, "test_range", bit_pos)?;
assert_eq!(unassigned, true);
// Verify it's gone
let value_after = store.get_in_transaction(names, assign, "test_range", bit_pos)?;
assert_eq!(value_after, None);
Ok(())
}).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)
}
#[test]
fn test_transaction_rollback() -> Result<()> {
let store = create_test_store()?;
// Define a range and assign initial value
store.define("test_range", 5)?; // Small range to easily fill
let initial_bit = store.assign("test_range", "initial_value")?;
// Fill up the range
for i in 1..5 {
store.assign("test_range", &format!("value_{}", i))?;
}
// Attempt a transaction that should fail (range full)
let result = (&store.names, &store.map, &store.assign).transaction(|(names, map, assign)| {
// This should succeed
let _unassigned = store.unassign_bit_in_transaction(names, map, assign, "test_range", initial_bit)?;
// This should fail (range is still full after we just freed one bit)
// Actually, this should succeed since we freed a bit, so let's try to assign to a full range differently
// Let's assign multiple values to ensure failure
store.assign_in_transaction(names, map, assign, "test_range", "temp1")?;
store.assign_in_transaction(names, map, assign, "test_range", "temp2")?; // This should fail
Ok(())
}).map_err(|e| match e {
sled::transaction::TransactionError::Abort(()) => Error::RangeFull("test_range".to_string()),
sled::transaction::TransactionError::Storage(storage_err) => Error::StoreError(storage_err),
});
// Transaction should have failed
assert!(result.is_err());
// Verify rollback - initial value should still be there
let value = store.get("test_range", initial_bit)?;
assert_eq!(value, Some("initial_value".to_string()));
Ok(())
}
#[test]
fn test_bit_out_of_range() -> Result<()> {
let store = create_test_store()?;
// Define a small range
store.define("small_range", 10)?;
// Try to get bit beyond range
let result = store.get("small_range", 15);
assert!(result.is_err());
// Try to unassign bit beyond range
let result = store.unassign_bit("small_range", 15);
assert!(result.is_ok());
assert_eq!(result?, false);
Ok(())
}
#[test]
fn test_multiple_ranges() -> Result<()> {
let store = create_test_store()?;
// Define multiple ranges
store.define("range_a", 50)?;
store.define("range_b", 100)?;
store.define("range_c", 25)?;
// Assign values to each range
let bit_a = store.assign("range_a", "value_a")?;
let bit_b = store.assign("range_b", "value_b")?;
let bit_c = store.assign("range_c", "value_c")?;
// Verify isolation between ranges
assert_eq!(store.get("range_a", bit_a)?, Some("value_a".to_string()));
assert_eq!(store.get("range_b", bit_b)?, Some("value_b".to_string()));
assert_eq!(store.get("range_c", bit_c)?, Some("value_c".to_string()));
// Cross-range access: accessing bit positions that exist in one range but not another
// Since all ranges start at bit 0, we need to test with positions beyond smaller ranges
// Test accessing valid bit positions across ranges
// All ranges have their first assignment at bit 0, so this tests basic isolation
assert_eq!(store.get("range_a", bit_a)?, Some("value_a".to_string()));
// Test accessing bit positions that are out of range
// range_c has only 25 bits (0-24), so bit 25 and above should give an error
let result = store.get("range_c", 25);
assert!(result.is_err());
// range_a has 50 bits (0-49), so bit 50 and above should give an error
let result = store.get("range_a", 50);
assert!(result.is_err());
// List all ranges
let ranges = store.list_ranges()?;
assert_eq!(ranges.len(), 3);
assert!(ranges.iter().any(|(name, size)| name == "range_a" && *size == 50));
assert!(ranges.iter().any(|(name, size)| name == "range_b" && *size == 100));
assert!(ranges.iter().any(|(name, size)| name == "range_c" && *size == 25));
// List assignments in each range
let assignments_a = store.list_range("range_a")?;
assert_eq!(assignments_a.len(), 1);
assert_eq!(assignments_a[0], (bit_a, "value_a".to_string()));
let assignments_b = store.list_range("range_b")?;
assert_eq!(assignments_b.len(), 1);
assert_eq!(assignments_b[0], (bit_b, "value_b".to_string()));
let assignments_c = store.list_range("range_c")?;
assert_eq!(assignments_c.len(), 1);
assert_eq!(assignments_c[0], (bit_c, "value_c".to_string()));
Ok(())
}
#[test]
fn test_sequential_bit_assignment() -> Result<()> {
let store = create_test_store()?;
// Define a range
store.define("sequential", 10)?;
// Assign values and verify they get sequential bits
let mut bits = Vec::new();
for i in 0..5 {
let bit = store.assign("sequential", &format!("seq_{}", i))?;
bits.push(bit);
}
// Should get bits 0, 1, 2, 3, 4 in that order
for (i, &bit) in bits.iter().enumerate() {
assert_eq!(bit, i as u64);
}
// Unassign bit 2
store.unassign("sequential", "seq_2")?;
// Next assignment should reuse bit 2
let next_bit = store.assign("sequential", "reused")?;
assert_eq!(next_bit, 2);
Ok(())
}
#[test]
fn test_define_range() -> Result<()> {
let store = create_test_store()?;
// Define a range
store.define("test_range", 100)?;
// Defining the same range again should not error
store.define("test_range", 100)?;
// Check that ranges are listed
let ranges = store.list_ranges()?;
assert!(ranges.iter().any(|(name, size)| name == "test_range" && *size == 100));
Ok(())
}
#[test]
fn test_assign_and_get() -> Result<()> {
let store = create_test_store()?;
// Define a range first
store.define("test_range", 100)?;
// Assign a value
let bit_pos = store.assign("test_range", "test_value")?;
assert!(bit_pos < 100);
// Get the value
let value = store.get("test_range", bit_pos)?;
assert_eq!(value, Some("test_value".to_string()));
// Test getting non-existent bit
let empty = store.get("test_range", 99)?;
assert_eq!(empty, None);
Ok(())
}
#[test]
fn test_multiple_assignments() -> Result<()> {
let store = create_test_store()?;
// Define range
store.define("test_range", 64)?;
// Assign multiple values
let pos1 = store.assign("test_range", "value1")?;
let pos2 = store.assign("test_range", "value2")?;
let pos3 = store.assign("test_range", "value3")?;
assert_eq!(pos1, 0);
assert_eq!(pos2, 1);
assert_eq!(pos3, 2);
// Verify all values
assert_eq!(store.get("test_range", 0)?, Some("value1".to_string()));
assert_eq!(store.get("test_range", 1)?, Some("value2".to_string()));
assert_eq!(store.get("test_range", 2)?, Some("value3".to_string()));
Ok(())
}
#[test]
fn test_list_range() -> Result<()> {
let store = create_test_store()?;
// Define range
store.define("test_range", 64)?;
// Assign some values
store.assign("test_range", "alpha")?;
store.assign("test_range", "beta")?;
store.assign("test_range", "gamma")?;
// List assignments
let assignments = store.list_range("test_range")?;
assert_eq!(assignments.len(), 3);
assert_eq!(assignments[0], (0, "alpha".to_string()));
assert_eq!(assignments[1], (1, "beta".to_string()));
assert_eq!(assignments[2], (2, "gamma".to_string()));
Ok(())
}
#[test]
fn test_list_ranges() -> Result<()> {
let store = create_test_store()?;
// Define multiple ranges
store.define("range_a", 32)?;
store.define("range_b", 64)?;
store.define("range_c", 128)?;
// List all ranges
let ranges = store.list_ranges()?;
assert_eq!(ranges.len(), 3);
assert_eq!(ranges[0], ("range_a".to_string(), 32));
assert_eq!(ranges[1], ("range_b".to_string(), 64));
assert_eq!(ranges[2], ("range_c".to_string(), 128));
Ok(())
}
#[test]
fn test_unassign() -> Result<()> {
let store = create_test_store()?;
// Define range
store.define("test_range", 64)?;
// Assign values
store.assign("test_range", "value1")?;
store.assign("test_range", "value2")?;
store.assign("test_range", "value3")?;
// Unassign middle value
let removed = store.unassign("test_range", "value2")?;
assert!(removed);
// Verify it's gone
assert_eq!(store.get("test_range", 1)?, None);
// Other values should still be there
assert_eq!(store.get("test_range", 0)?, Some("value1".to_string()));
assert_eq!(store.get("test_range", 2)?, Some("value3".to_string()));
// Assign a new value - should reuse the freed slot
let new_pos = store.assign("test_range", "new_value")?;
assert_eq!(new_pos, 1); // Should reuse position 1
Ok(())
}
#[test]
fn test_range_full() -> Result<()> {
let store = create_test_store()?;
// Define small range
store.define("small_range", 2)?;
// Fill it up
store.assign("small_range", "value1")?;
store.assign("small_range", "value2")?;
// Try to assign one more - should fail
let result = store.assign("small_range", "value3");
assert!(result.is_err());
Ok(())
}
#[test]
fn test_undefined_range() -> Result<()> {
let store = create_test_store()?;
// Try operations on undefined range
let result = store.assign("undefined_range", "value");
assert!(result.is_err());
let value = store.get("undefined_range", 0)?;
assert_eq!(value, None);
let assignments = store.list_range("undefined_range")?;
assert_eq!(assignments.len(), 0);
let removed = store.unassign("undefined_range", "value")?;
assert!(!removed);
Ok(())
}
}
diff --git a/docs/range_assigner.md b/docs/range_assigner.md
index f6dfe89..5d6b764 100644
--- a/docs/range_assigner.md
+++ b/docs/range_assigner.md
@@ -1,163 +1,226 @@
# 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
+- Unified IP assignment querying through `Network.get_assignment()`
## 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")
+// Create a RangeAssigner (uses network name as range name by default)
+let assigner = RangeAssigner::new()
.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
+// Range name will be "production" (same as network name)
store.create("production", netrange, provider, Some(assigner))?;
```
### Assigning IP Addresses
```rust
// Create assigner instance for operations
-let range_assigner = RangeAssigner::new("production-network-pool");
+let range_assigner = RangeAssigner::new();
-// 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")?;
+// Assign IPs to devices using the network name as range name
+let web_server_ip = range_assigner.assign_ip("production", store.ranges(), "web-server-01")?;
+let db_server_ip = range_assigner.assign_ip("production", store.ranges(), "database-01")?;
+let cache_server_ip = range_assigner.assign_ip("production", 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)?;
+let assignment = range_assigner.get_assignment_by_bit("production", 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)?;
+let unassigned = range_assigner.unassign_ip("production", 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:
+When a network is created with a `RangeAssigner`, the range is automatically initialized within the same transaction 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)
+- **Range Name**: Defaults to the network name unless overridden with `with_range_name()`
+
+This ensures atomicity - either both the network and its range are created, or neither is created if the transaction fails.
### 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
+## IP Address Assignment and Retrieval
+
+### Using Network.get_assignment() (Recommended)
-The `assign_ip()` method returns a bit position within the range. To convert this to an actual IP address:
+The easiest way to get IP assignments is through the `Network.get_assignment()` method:
```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")?;
+// Get the network instance
+let network = store.networks().get("production")?.unwrap();
+
+// Query IP assignment by identifier
+let ip = network.get_assignment("web-server-01", store.ranges())?;
+match ip {
+ Some(addr) => println!("web-server-01 has IP: {}", addr),
+ None => println!("web-server-01 has no IP assignment"),
+}
+```
-// 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
+### Converting Bit Positions to IP Addresses
+
+The `assign_ip()` method returns a bit position within the range. You can convert this to an actual IP address:
+
+```rust
+// Method 1: Using Network.get_assignment_by_bit_position()
+let network = store.networks().get("my-network")?.unwrap();
+let bit_position = range_assigner.assign_ip("my-network", store.ranges(), "device-1")?;
+let actual_ip = network.get_assignment_by_bit_position(bit_position)?;
+println!("Device assigned IP: {}", actual_ip);
+
+// Method 2: Manual calculation (for IPv4 network 192.168.1.0/24)
+let network_base = Ipv4Addr::new(192, 168, 1, 0);
+let actual_ip = Ipv4Addr::from(u32::from(network_base) + bit_position as u32);
+println!("Device assigned IP: {}", actual_ip);
```
## 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") {
+// Using direct assignment
+match range_assigner.assign_ip("my-network", 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),
}
+
+// Using Network.get_assignment to query existing assignments
+let network = store.networks().get("my-network")?.unwrap();
+match network.get_assignment("device-id", store.ranges()) {
+ Ok(Some(ip)) => println!("Device has IP: {}", ip),
+ Ok(None) => println!("Device has no IP assignment"),
+ Err(e) => println!("Query 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
+4. **Querying**: Networks provide unified access to assignments via `get_assignment()`
+
+## Network.get_assignment() API
+
+The `Network` struct provides a unified interface for IP assignment queries:
+
+```rust
+impl Network {
+ /// Get an IP assignment for the given identifier
+ pub fn get_assignment(&self, identifier: &str, range_store: Option<&RangeStore>) -> Result<Option<IpAddr>>;
+
+ /// Get an IP address from a bit position in the network range
+ pub fn get_assignment_by_bit_position(&self, bit_position: u64) -> Result<IpAddr>;
+}
+```
+
+### Features:
+- **Assigner-agnostic**: Works with any `NetworkAssigner` implementation
+- **Type safety**: Returns `IpAddr` (IPv4 or IPv6) instead of raw bit positions
+- **Unified interface**: Same API regardless of underlying assigner type
+- **Error handling**: Returns `None` for networks without assigners
+- **Performance**: Efficient lookup using range scanning
## Best Practices
### Range Naming
-- Use descriptive, unique names for ranges
+- By default, ranges use the network name (recommended for simplicity)
+- Use `with_range_name()` only when you need multiple ranges per network
- Consider including network name: `"production-web-tier-range"`
- Avoid special characters that might cause issues
### IP Management
+- Use `Network.get_assignment()` for unified IP querying
- Always check return values from assignment operations
- Implement proper cleanup when devices are removed
- Monitor range utilization to prevent exhaustion
+- Use `get_assignment_by_bit_position()` for direct bit-to-IP conversion
### 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.
+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. Range initialization is atomic with network creation, preventing phantom ranges.
## 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
+- Bitmap storage is memory-efficient even for large ranges
+- `Network.get_assignment()` uses range scanning - O(n) for identifier lookups
+- `get_assignment_by_bit_position()` is O(1) for direct bit-to-IP conversion
+- Consider caching frequently accessed assignments for better performance
\ No newline at end of file
diff --git a/examples/network_get_assignment_example.rs b/examples/network_get_assignment_example.rs
new file mode 100644
index 0000000..cc5a544
--- /dev/null
+++ b/examples/network_get_assignment_example.rs
@@ -0,0 +1,158 @@
+use store::{open, Result};
+use store::network::{NetRange, BasicProvider, RangeAssigner};
+use std::net::IpAddr;
+
+fn main() -> Result<()> {
+ println!("=== Network Assignment Example ===\n");
+
+ // Open the store
+ let store = open()?;
+
+ // Create an IPv4 network with RangeAssigner
+ let ipv4_netrange = NetRange::from_cidr("192.168.100.0/24")?;
+ let provider = BasicProvider::new()
+ .with_config("type", "docker")
+ .with_config("driver", "bridge");
+
+ let assigner = RangeAssigner::new()
+ .with_config("strategy", "sequential")
+ .with_config("reserve_gateway", "true");
+
+ store.networks().create("docker-bridge", ipv4_netrange, provider, Some(assigner))?;
+ println!("✓ Created IPv4 network: docker-bridge (192.168.100.0/24)");
+
+ // Create an IPv6 network
+ let ipv6_netrange = NetRange::from_cidr("fd00:db8::/64")?;
+ let provider6 = BasicProvider::new()
+ .with_config("type", "kubernetes")
+ .with_config("driver", "calico");
+
+ let assigner6 = RangeAssigner::new()
+ .with_config("strategy", "sequential");
+
+ store.networks().create("k8s-pods", ipv6_netrange, provider6, Some(assigner6))?;
+ println!("✓ Created IPv6 network: k8s-pods (fd00:db8::/64)");
+
+ // Create a network without an assigner
+ let no_assigner_net = NetRange::from_cidr("10.50.0.0/16")?;
+ let static_provider = BasicProvider::new()
+ .with_config("type", "static")
+ .with_config("manual", "true");
+
+ store.networks().create("static-network", no_assigner_net, static_provider, None::<RangeAssigner>)?;
+ println!("✓ Created static network: static-network (10.50.0.0/16) - no assigner\n");
+
+ // Get network instances
+ let docker_network = store.networks().get("docker-bridge")?.unwrap();
+ let k8s_network = store.networks().get("k8s-pods")?.unwrap();
+ let static_network = store.networks().get("static-network")?.unwrap();
+
+ // Assign some IPs manually using RangeAssigner
+ let range_assigner = RangeAssigner::new();
+
+ // Docker network assignments
+ println!("--- Docker Bridge Network Assignments ---");
+ if let Some(ref ranges) = store.ranges() {
+ // Assign IPs to containers
+ let web_bit = range_assigner.assign_ip("docker-bridge", ranges, "web-container-1")?;
+ let db_bit = range_assigner.assign_ip("docker-bridge", ranges, "database-container")?;
+ let cache_bit = range_assigner.assign_ip("docker-bridge", ranges, "redis-cache")?;
+
+ println!("Assigned bit positions:");
+ println!(" web-container-1: bit {}", web_bit);
+ println!(" database-container: bit {}", db_bit);
+ println!(" redis-cache: bit {}", cache_bit);
+ }
+
+ // Now use Network.get_assignment to retrieve IP addresses
+ println!("\nUsing Network.get_assignment() to resolve IPs:");
+
+ let containers = ["web-container-1", "database-container", "redis-cache", "nonexistent-container"];
+ for container in &containers {
+ match docker_network.get_assignment(container, store.ranges())? {
+ Some(ip) => println!(" {} -> {}", container, ip),
+ None => println!(" {} -> No assignment found", container),
+ }
+ }
+
+ // K8s network assignments
+ println!("\n--- Kubernetes Pods Network Assignments ---");
+ if let Some(ref ranges) = store.ranges() {
+ range_assigner.assign_ip("k8s-pods", ranges, "nginx-pod-abc123")?;
+ range_assigner.assign_ip("k8s-pods", ranges, "postgres-pod-def456")?;
+ range_assigner.assign_ip("k8s-pods", ranges, "api-pod-ghi789")?;
+ }
+
+ let pods = ["nginx-pod-abc123", "postgres-pod-def456", "api-pod-ghi789"];
+ for pod in &pods {
+ if let Some(ip) = k8s_network.get_assignment(pod, store.ranges())? {
+ println!(" {} -> {}", pod, ip);
+ }
+ }
+
+ // Test static network (should return None since no assigner)
+ println!("\n--- Static Network (No Assigner) ---");
+ match static_network.get_assignment("some-device", store.ranges())? {
+ Some(ip) => println!(" Unexpected IP found: {}", ip),
+ None => println!(" ✓ No assignment found (expected for static network)"),
+ }
+
+ // Demonstrate bit position to IP conversion
+ println!("\n--- Direct Bit Position to IP Conversion ---");
+ for bit_pos in 0..5 {
+ let ipv4_addr = docker_network.get_assignment_by_bit_position(bit_pos)?;
+ let ipv6_addr = k8s_network.get_assignment_by_bit_position(bit_pos)?;
+ println!(" Bit {}: IPv4={}, IPv6={}", bit_pos, ipv4_addr, ipv6_addr);
+ }
+
+ // Show practical usage: find all assigned IPs in a network
+ println!("\n--- All Docker Network Assignments ---");
+ if let Some(ref ranges) = store.ranges() {
+ let assignments = ranges.list_range("docker-bridge")?;
+ for (bit_pos, identifier) in assignments {
+ let ip = docker_network.get_assignment_by_bit_position(bit_pos)?;
+ println!(" {}: {} (bit {})", identifier, ip, bit_pos);
+ }
+ }
+
+ // Demonstrate unassignment and re-query
+ println!("\n--- Assignment Lifecycle ---");
+ let test_container = "temporary-container";
+
+ // Assign
+ if let Some(ref ranges) = store.ranges() {
+ let bit_pos = range_assigner.assign_ip("docker-bridge", ranges, test_container)?;
+ let ip_before = docker_network.get_assignment(test_container, store.ranges())?.unwrap();
+ println!(" Assigned {} -> {} (bit {})", test_container, ip_before, bit_pos);
+
+ // Unassign
+ let unassigned = range_assigner.unassign_ip("docker-bridge", ranges, bit_pos)?;
+ println!(" Unassigned {}: {}", test_container, unassigned);
+
+ // Query after unassignment
+ let ip_after = docker_network.get_assignment(test_container, store.ranges())?;
+ println!(" Query after unassignment: {:?}", ip_after);
+ }
+
+ // Summary
+ println!("\n=== Summary ===");
+ let networks = store.networks().list()?;
+ println!("Created {} networks:", networks.len());
+ for network_name in networks {
+ let net = store.networks().get(&network_name)?.unwrap();
+ let assigner_info = match &net.assigner_type {
+ Some(atype) => format!("with {} assigner", atype),
+ None => "no assigner".to_string(),
+ };
+ println!(" • {} ({}) - {}", network_name, net.netrange.to_cidr(), assigner_info);
+ }
+
+ println!("\nNetwork.get_assignment() provides a unified interface to:");
+ println!(" ✓ Query IP assignments by identifier");
+ println!(" ✓ Support different assigner implementations");
+ println!(" ✓ Handle both IPv4 and IPv6 networks");
+ println!(" ✓ Return None for networks without assigners");
+ println!(" ✓ Convert bit positions to actual IP addresses");
+
+ Ok(())
+}
\ No newline at end of file
diff --git a/examples/range_assigner_example.rs b/examples/range_assigner_example.rs
index 59ea2ce..c90fdc8 100644
--- a/examples/range_assigner_example.rs
+++ b/examples/range_assigner_example.rs
@@ -1,65 +1,66 @@
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")
+ let assigner = RangeAssigner::new()
.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");
+ let range_assigner = RangeAssigner::new();
+ let range_name = "docker-network"; // Uses network name as range name
// 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")?;
+ let container1_ip = range_assigner.assign_ip(range_name, store.ranges(), "container-web-1")?;
+ let container2_ip = range_assigner.assign_ip(range_name, store.ranges(), "container-db-1")?;
+ let container3_ip = range_assigner.assign_ip(range_name, 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)?;
+ let web_assignment = range_assigner.get_assignment_by_bit(range_name, store.ranges(), container1_ip)?;
+ let db_assignment = range_assigner.get_assignment_by_bit(range_name, 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)?;
+ let unassigned = range_assigner.unassign_ip(range_name, store.ranges(), container2_ip)?;
println!("\nUnassigned container-db-1: {}", unassigned);
// Verify unassignment
- let db_assignment_after = range_assigner.get_assignment(store.ranges(), container2_ip)?;
+ let db_assignment_after = range_assigner.get_assignment_by_bit(range_name, 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")?;
+ let new_container_ip = range_assigner.assign_ip(range_name, 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
diff --git a/examples/updated_range_assigner_example.rs b/examples/updated_range_assigner_example.rs
new file mode 100644
index 0000000..4f8bfa8
--- /dev/null
+++ b/examples/updated_range_assigner_example.rs
@@ -0,0 +1,88 @@
+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");
+
+ // RangeAssigner now defaults to using the network name as the range name
+ let assigner = RangeAssigner::new()
+ .with_config("strategy", "sequential")
+ .with_config("reserve_gateway", "true");
+
+ // Create the network - range is automatically initialized within the same transaction
+ store.networks().create("docker-network", netrange, provider, Some(assigner))?;
+
+ println!("Created network 'docker-network' with range 10.0.0.0/24");
+ println!("Range name automatically set to 'docker-network'");
+
+ // Create a RangeAssigner instance for IP operations
+ let range_assigner = RangeAssigner::new();
+ let network_name = "docker-network"; // This is also the range name
+
+ // Assign IP addresses to devices
+ let container1_ip = range_assigner.assign_ip(network_name, store.ranges(), "container-web-1")?;
+ let container2_ip = range_assigner.assign_ip(network_name, store.ranges(), "container-db-1")?;
+ let container3_ip = range_assigner.assign_ip(network_name, store.ranges(), "container-cache-1")?;
+
+ println!("\nAssigned 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_by_bit(network_name, store.ranges(), container1_ip)?;
+ let db_assignment = range_assigner.get_assignment_by_bit(network_name, 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(network_name, store.ranges(), container2_ip)?;
+ println!("\nUnassigned container-db-1: {}", unassigned);
+
+ // Verify unassignment
+ let db_assignment_after = range_assigner.get_assignment_by_bit(network_name, 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(network_name, store.ranges(), "container-api-1")?;
+ println!("New container assigned to bit: {}", new_container_ip);
+
+ // Example with custom range name
+ println!("\n--- Example with custom range name ---");
+
+ let custom_assigner = RangeAssigner::with_range_name("custom-pool")
+ .with_config("strategy", "sequential");
+
+ let netrange2 = NetRange::from_cidr("172.16.0.0/16")?;
+ let provider2 = BasicProvider::new().with_config("type", "kubernetes");
+
+ store.networks().create("k8s-network", netrange2, provider2, Some(custom_assigner))?;
+ println!("Created 'k8s-network' with custom range name 'custom-pool'");
+
+ // Assign from the custom range
+ let k8s_assigner = RangeAssigner::new();
+ let pod_ip = k8s_assigner.assign_ip("custom-pool", store.ranges(), "pod-nginx-1")?;
+ println!("Assigned pod IP bit position: {}", pod_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!("\nDocker network details:");
+ println!(" Name: {}", network.name);
+ println!(" Range: {}", network.netrange.to_cidr());
+ println!(" Assigner type: {:?}", network.assigner_type);
+
+ Ok(())
+}
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jun 8, 11:31 AM (1 d, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
47602
Default Alt Text
(117 KB)

Event Timeline