406 lines
14 KiB
Rust
406 lines
14 KiB
Rust
//! Slice calculator service for automatic slice calculation from node capacity
|
|
//! Follows the established builder pattern for consistent API design
|
|
|
|
use crate::models::user::{NodeCapacity, FarmNode};
|
|
use rust_decimal::Decimal;
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
/// Base slice unit definition (1 vCPU, 4GB RAM, 200GB storage)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SliceUnit {
|
|
pub cpu_cores: u32, // 1
|
|
pub memory_gb: u32, // 4
|
|
pub storage_gb: u32, // 200
|
|
}
|
|
|
|
impl Default for SliceUnit {
|
|
fn default() -> Self {
|
|
Self {
|
|
cpu_cores: 1,
|
|
memory_gb: 4,
|
|
storage_gb: 200,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Calculated slice combination from node capacity
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SliceCombination {
|
|
pub id: String,
|
|
pub multiplier: u32, // How many base slices this uses
|
|
pub cpu_cores: u32, // Slice-specific resource
|
|
pub memory_gb: u32, // Slice-specific resource
|
|
pub storage_gb: u32, // Slice-specific resource
|
|
pub quantity_available: u32, // How many of this combination available
|
|
pub price_per_hour: Decimal,
|
|
pub base_slices_required: u32,
|
|
|
|
// Inherited from parent node
|
|
pub node_uptime_percentage: f64,
|
|
pub node_bandwidth_mbps: u32,
|
|
pub node_location: String,
|
|
pub node_certification_type: String,
|
|
pub node_id: String,
|
|
pub resource_provider_email: String,
|
|
}
|
|
|
|
/// Track individual slice rentals
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SliceAllocation {
|
|
pub allocation_id: String,
|
|
pub slice_combination_id: String,
|
|
pub renter_email: String,
|
|
pub base_slices_used: u32,
|
|
pub rental_start: DateTime<Utc>,
|
|
pub rental_end: Option<DateTime<Utc>>,
|
|
pub status: AllocationStatus,
|
|
pub monthly_cost: Decimal,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub enum AllocationStatus {
|
|
Active,
|
|
Expired,
|
|
Cancelled,
|
|
}
|
|
|
|
/// Pricing configuration for node slices
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SlicePricing {
|
|
pub base_price_per_hour: Decimal, // Price for 1 base slice per hour
|
|
pub currency: String,
|
|
pub pricing_multiplier: Decimal, // ResourceProvider can adjust pricing (0.5x - 2.0x)
|
|
}
|
|
|
|
impl Default for SlicePricing {
|
|
fn default() -> Self {
|
|
Self {
|
|
base_price_per_hour: Decimal::from(1), // $1 per hour for base slice
|
|
currency: "USD".to_string(),
|
|
pricing_multiplier: Decimal::from(1),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Service for slice calculations following builder pattern
|
|
#[derive(Clone)]
|
|
pub struct SliceCalculatorService {
|
|
base_slice: SliceUnit,
|
|
pricing_limits: PricingLimits,
|
|
}
|
|
|
|
/// Platform-enforced pricing limits
|
|
#[derive(Debug, Clone)]
|
|
pub struct PricingLimits {
|
|
pub min_price_per_hour: Decimal, // e.g., $0.10
|
|
pub max_price_per_hour: Decimal, // e.g., $10.00
|
|
}
|
|
|
|
impl Default for PricingLimits {
|
|
fn default() -> Self {
|
|
Self {
|
|
min_price_per_hour: Decimal::from_str_exact("0.10").unwrap(),
|
|
max_price_per_hour: Decimal::from_str_exact("10.00").unwrap(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Builder for SliceCalculatorService
|
|
#[derive(Default)]
|
|
pub struct SliceCalculatorServiceBuilder {
|
|
base_slice: Option<SliceUnit>,
|
|
pricing_limits: Option<PricingLimits>,
|
|
}
|
|
|
|
impl SliceCalculatorServiceBuilder {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn base_slice(mut self, base_slice: SliceUnit) -> Self {
|
|
self.base_slice = Some(base_slice);
|
|
self
|
|
}
|
|
|
|
pub fn pricing_limits(mut self, limits: PricingLimits) -> Self {
|
|
self.pricing_limits = Some(limits);
|
|
self
|
|
}
|
|
|
|
pub fn build(self) -> Result<SliceCalculatorService, String> {
|
|
Ok(SliceCalculatorService {
|
|
base_slice: self.base_slice.unwrap_or_default(),
|
|
pricing_limits: self.pricing_limits.unwrap_or_default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl SliceCalculatorService {
|
|
pub fn builder() -> SliceCalculatorServiceBuilder {
|
|
SliceCalculatorServiceBuilder::new()
|
|
}
|
|
|
|
/// Calculate maximum base slices from node capacity
|
|
pub fn calculate_max_base_slices(&self, capacity: &NodeCapacity) -> u32 {
|
|
let cpu_slices = capacity.cpu_cores as u32 / self.base_slice.cpu_cores;
|
|
let memory_slices = capacity.memory_gb as u32 / self.base_slice.memory_gb;
|
|
let storage_slices = capacity.storage_gb as u32 / self.base_slice.storage_gb;
|
|
|
|
// Return the limiting factor
|
|
std::cmp::min(std::cmp::min(cpu_slices, memory_slices), storage_slices)
|
|
}
|
|
|
|
/// Generate all possible slice combinations from available base slices
|
|
pub fn generate_slice_combinations(
|
|
&self,
|
|
max_base_slices: u32,
|
|
allocated_slices: u32,
|
|
node: &FarmNode,
|
|
resource_provider_email: &str
|
|
) -> Vec<SliceCombination> {
|
|
let available_base_slices = max_base_slices.saturating_sub(allocated_slices);
|
|
let mut combinations = Vec::new();
|
|
|
|
if available_base_slices == 0 {
|
|
return combinations;
|
|
}
|
|
|
|
// Generate practical slice combinations up to a reasonable limit
|
|
// We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x
|
|
let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32];
|
|
|
|
for multiplier in practical_multipliers {
|
|
// Skip if multiplier is larger than available slices
|
|
if multiplier > available_base_slices {
|
|
continue;
|
|
}
|
|
|
|
// Calculate how many complete units of this multiplier we can create
|
|
let quantity = available_base_slices / multiplier;
|
|
|
|
// Skip if we can't create at least one complete unit
|
|
if quantity == 0 {
|
|
continue;
|
|
}
|
|
|
|
let combination = SliceCombination {
|
|
id: format!("{}x{}", quantity, multiplier),
|
|
multiplier,
|
|
cpu_cores: self.base_slice.cpu_cores * multiplier,
|
|
memory_gb: self.base_slice.memory_gb * multiplier,
|
|
storage_gb: self.base_slice.storage_gb * multiplier,
|
|
quantity_available: quantity,
|
|
price_per_hour: self.calculate_combination_price(multiplier, node.slice_pricing.as_ref()
|
|
.and_then(|sp| serde_json::from_value(sp.clone()).ok())
|
|
.as_ref()
|
|
.unwrap_or(&crate::services::slice_calculator::SlicePricing::default())),
|
|
base_slices_required: multiplier,
|
|
|
|
// Inherited from parent node
|
|
node_uptime_percentage: node.uptime_percentage as f64,
|
|
node_bandwidth_mbps: node.capacity.bandwidth_mbps as u32,
|
|
node_location: node.location.clone(),
|
|
node_certification_type: node.grid_data.as_ref()
|
|
.map(|g| g.get("certification_type")
|
|
.and_then(|cert| cert.as_str())
|
|
.unwrap_or("DIY")
|
|
.to_string())
|
|
.unwrap_or_else(|| "DIY".to_string()),
|
|
node_id: node.id.clone(),
|
|
resource_provider_email: resource_provider_email.to_string(),
|
|
};
|
|
|
|
combinations.push(combination);
|
|
}
|
|
|
|
// Sort by multiplier (smallest slices first)
|
|
combinations.sort_by_key(|c| c.multiplier);
|
|
combinations
|
|
}
|
|
|
|
/// Generate slice combinations with explicit SLA values (for user-defined SLAs)
|
|
pub fn generate_slice_combinations_with_sla(
|
|
&self,
|
|
max_base_slices: u32,
|
|
allocated_slices: u32,
|
|
node: &FarmNode,
|
|
resource_provider_email: &str,
|
|
uptime_percentage: f64,
|
|
bandwidth_mbps: u32,
|
|
base_price_per_hour: Decimal
|
|
) -> Vec<SliceCombination> {
|
|
let available_base_slices = max_base_slices.saturating_sub(allocated_slices);
|
|
let mut combinations = Vec::new();
|
|
|
|
if available_base_slices == 0 {
|
|
return combinations;
|
|
}
|
|
|
|
// Generate practical slice combinations up to a reasonable limit
|
|
// We'll generate combinations for multipliers 1x, 2x, 3x, 4x, 5x, 6x, 8x, 10x, 12x, 16x, 20x, 24x, 32x
|
|
let practical_multipliers = vec![1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32];
|
|
|
|
// Create custom pricing with user's base price
|
|
let custom_pricing = SlicePricing {
|
|
base_price_per_hour,
|
|
currency: "USD".to_string(),
|
|
pricing_multiplier: Decimal::from(1),
|
|
};
|
|
|
|
for multiplier in practical_multipliers {
|
|
// Skip if multiplier is larger than available slices
|
|
if multiplier > available_base_slices {
|
|
continue;
|
|
}
|
|
|
|
// Calculate how many complete units of this multiplier we can create
|
|
let quantity = available_base_slices / multiplier;
|
|
|
|
// Skip if we can't create at least one complete unit
|
|
if quantity == 0 {
|
|
continue;
|
|
}
|
|
|
|
let combination = SliceCombination {
|
|
id: format!("{}x{}", quantity, multiplier),
|
|
multiplier,
|
|
cpu_cores: self.base_slice.cpu_cores * multiplier,
|
|
memory_gb: self.base_slice.memory_gb * multiplier,
|
|
storage_gb: self.base_slice.storage_gb * multiplier,
|
|
quantity_available: quantity,
|
|
price_per_hour: self.calculate_combination_price(multiplier, &custom_pricing),
|
|
base_slices_required: multiplier,
|
|
|
|
// Use explicit SLA values instead of inheriting from node
|
|
node_uptime_percentage: uptime_percentage,
|
|
node_bandwidth_mbps: bandwidth_mbps,
|
|
node_location: node.location.clone(),
|
|
node_certification_type: node.grid_data.as_ref()
|
|
.map(|g| g.get("certification_type")
|
|
.and_then(|cert| cert.as_str())
|
|
.unwrap_or("DIY")
|
|
.to_string())
|
|
.unwrap_or_else(|| "DIY".to_string()),
|
|
node_id: node.id.clone(),
|
|
resource_provider_email: resource_provider_email.to_string(),
|
|
};
|
|
|
|
combinations.push(combination);
|
|
}
|
|
|
|
// Sort by multiplier (smallest slices first)
|
|
combinations.sort_by_key(|c| c.multiplier);
|
|
combinations
|
|
}
|
|
|
|
/// Calculate price for a slice combination
|
|
fn calculate_combination_price(&self, multiplier: u32, pricing: &SlicePricing) -> Decimal {
|
|
pricing.base_price_per_hour * pricing.pricing_multiplier * Decimal::from(multiplier)
|
|
}
|
|
|
|
/// Update availability after rental
|
|
pub fn update_availability_after_rental(
|
|
&self,
|
|
node: &mut FarmNode,
|
|
rented_base_slices: u32,
|
|
resource_provider_email: &str
|
|
) -> Result<(), String> {
|
|
// Update allocated count
|
|
node.allocated_base_slices += rented_base_slices as i32;
|
|
|
|
// Recalculate available combinations
|
|
let combinations = self.generate_slice_combinations(
|
|
node.total_base_slices as u32,
|
|
node.allocated_base_slices as u32,
|
|
node,
|
|
resource_provider_email
|
|
);
|
|
node.available_combinations = combinations.iter()
|
|
.map(|c| serde_json::to_value(c).unwrap_or_default())
|
|
.collect();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Update availability after rental expiry
|
|
pub fn update_availability_after_release(
|
|
&self,
|
|
node: &mut FarmNode,
|
|
released_base_slices: u32,
|
|
resource_provider_email: &str
|
|
) -> Result<(), String> {
|
|
// Update allocated count
|
|
node.allocated_base_slices = node.allocated_base_slices.saturating_sub(released_base_slices as i32);
|
|
|
|
// Recalculate available combinations
|
|
node.available_combinations = self.generate_slice_combinations(
|
|
node.total_base_slices as u32,
|
|
node.allocated_base_slices as u32,
|
|
node,
|
|
resource_provider_email
|
|
).iter()
|
|
.map(|c| serde_json::to_value(c).unwrap_or_default())
|
|
.collect();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Validate slice price within platform limits
|
|
pub fn validate_slice_price(&self, price: Decimal) -> Result<(), String> {
|
|
if price < self.pricing_limits.min_price_per_hour {
|
|
return Err(format!("Price too low. Minimum: ${}/hour", self.pricing_limits.min_price_per_hour));
|
|
}
|
|
if price > self.pricing_limits.max_price_per_hour {
|
|
return Err(format!("Price too high. Maximum: ${}/hour", self.pricing_limits.max_price_per_hour));
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Slice rental record for users with deployment options
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SliceRental {
|
|
pub rental_id: String,
|
|
pub slice_combination_id: String,
|
|
pub node_id: String,
|
|
pub resource_provider_email: String,
|
|
pub slice_allocation: SliceAllocation,
|
|
pub total_cost: Decimal,
|
|
pub payment_status: PaymentStatus,
|
|
#[serde(default)]
|
|
pub slice_format: String,
|
|
#[serde(default)]
|
|
pub user_email: String,
|
|
#[serde(default)]
|
|
pub status: String,
|
|
#[serde(default)]
|
|
pub start_date: Option<chrono::DateTime<chrono::Utc>>,
|
|
#[serde(default)]
|
|
pub rental_duration_days: Option<u32>,
|
|
#[serde(default)]
|
|
pub monthly_cost: Option<Decimal>,
|
|
#[serde(default)]
|
|
pub id: String,
|
|
// NEW: Deployment information
|
|
#[serde(default)]
|
|
pub deployment_type: Option<String>, // "vm" or "kubernetes"
|
|
#[serde(default)]
|
|
pub deployment_name: Option<String>,
|
|
#[serde(default)]
|
|
pub deployment_config: Option<serde_json::Value>,
|
|
#[serde(default)]
|
|
pub deployment_status: Option<String>, // "Provisioning", "Active", "Stopped", "Failed"
|
|
#[serde(default)]
|
|
pub deployment_endpoint: Option<String>, // Access URL/IP for the deployment
|
|
#[serde(default)]
|
|
pub deployment_metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum PaymentStatus {
|
|
Pending,
|
|
Paid,
|
|
Failed,
|
|
Refunded,
|
|
} |