796 lines
31 KiB
Python
796 lines
31 KiB
Python
"""Server capacity operations."""
|
|
|
|
|
|
from datetime import datetime
|
|
from kubernetes_service import custom_objects_api, k8s_manager, v1
|
|
from runtime_config import (
|
|
logger,
|
|
)
|
|
from typing import Dict, Tuple
|
|
from user_service import get_pricing
|
|
|
|
__all__ = ['calculate_monthly_server_cost', 'calculate_server_cost', 'check_longhorn_storage_capacity', 'get_cluster_capacity', 'parse_kubernetes_resource', 'parse_resource_to_mebibytes', 'parse_resource_to_millicores', 'parse_storage_to_gb', 'validate_server_capacity']
|
|
|
|
|
|
|
|
def check_longhorn_storage_capacity(required_storage_bytes: int, replicas: int = 3) -> Tuple[bool, str]:
|
|
"""Check if Longhorn cluster has enough capacity for a new volume"""
|
|
try:
|
|
# Get Longhorn nodes
|
|
lh_nodes = custom_objects_api.list_namespaced_custom_object(
|
|
group="longhorn.io",
|
|
version="v1beta2",
|
|
namespace="longhorn-system",
|
|
plural="nodes"
|
|
)
|
|
|
|
schedulable_nodes = 0
|
|
nodes_with_space = []
|
|
|
|
for item in lh_nodes.get('items', []):
|
|
node_name = item['metadata']['name']
|
|
spec = item.get('spec', {})
|
|
status = item.get('status', {})
|
|
disk_status = status.get('diskStatus', {})
|
|
|
|
# Check if node allows scheduling
|
|
if not spec.get('allowScheduling', True):
|
|
continue
|
|
|
|
# Check available storage on disks
|
|
node_has_space = False
|
|
for disk_uuid, disk_info in disk_status.items():
|
|
# Check individual disk schedulable status if available in conditions
|
|
conditions = disk_info.get('conditions', [])
|
|
|
|
# Check if disk is schedulable
|
|
schedulable_status = "True"
|
|
if isinstance(conditions, list):
|
|
schedulable_cond = next((c for c in conditions if c.get('type') == 'Schedulable'), {})
|
|
schedulable_status = schedulable_cond.get('status', 'True')
|
|
elif isinstance(conditions, dict):
|
|
schedulable_status = conditions.get('Schedulable', {}).get('status', 'True')
|
|
|
|
if schedulable_status == 'False':
|
|
continue
|
|
|
|
# storageScheduled is the space available for scheduling new replicas (after over-provisioning)
|
|
storage_schedulable = int(disk_info.get('storageScheduled', 0))
|
|
|
|
if storage_schedulable >= required_storage_bytes:
|
|
node_has_space = True
|
|
# We only need one suitable disk per node
|
|
break
|
|
|
|
if node_has_space:
|
|
schedulable_nodes += 1
|
|
nodes_with_space.append(node_name)
|
|
|
|
logger.info(f"Longhorn storage check: {schedulable_nodes} nodes have >{required_storage_bytes/1024/1024/1024:.1f}GB schedulable. Nodes: {', '.join(nodes_with_space)}")
|
|
|
|
if schedulable_nodes >= replicas:
|
|
return True, f"Storage available on {schedulable_nodes} nodes"
|
|
else:
|
|
return False, f"Longhorn needs {replicas} nodes with {required_storage_bytes/1024/1024/1024:.1f}GB space, but only found {schedulable_nodes}: {', '.join(nodes_with_space)}"
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Could not check Longhorn storage capacity: {e}")
|
|
# Fallback: assume True if we can't check (e.g. permission error or API down),
|
|
# but log warning.
|
|
return True, f"Skipped storage check: {e}"
|
|
|
|
def validate_server_capacity(server_config: Dict) -> Dict:
|
|
"""
|
|
Validate if there's sufficient capacity for the requested server.
|
|
Uses Kubernetes allocatable resources which already account for system reservations.
|
|
"""
|
|
try:
|
|
# Extract resource requirements
|
|
cpu_cores = int(server_config['cpu'].split()[0])
|
|
ram_gb = int(server_config['ram'].replace('GB', ''))
|
|
storage_gb = int(server_config['storage'].replace('GB', ''))
|
|
|
|
# Convert to Kubernetes units
|
|
# Use 50m (5%) per core for requests and 50% of RAM for requests
|
|
# to allow smarter overcommitment while keeping limits at 100%
|
|
cpu_request_millicores = cpu_cores * 50
|
|
memory_request_mebibytes = (ram_gb * 1024) * 0.5
|
|
storage_request_gb = storage_gb
|
|
|
|
if not k8s_manager.connected:
|
|
# Development mode - assume unlimited capacity
|
|
return {
|
|
'success': True,
|
|
'message': 'Development mode: Capacity validation skipped',
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check Longhorn storage capacity
|
|
storage_request_bytes = storage_gb * 1024 * 1024 * 1024
|
|
storage_ok, storage_msg = check_longhorn_storage_capacity(storage_request_bytes)
|
|
|
|
if not storage_ok:
|
|
logger.warning(f"❌ Storage capacity check failed: {storage_msg}")
|
|
return {
|
|
'success': False,
|
|
'message': f'Insufficient storage capacity: {storage_msg}',
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
}
|
|
}
|
|
}
|
|
|
|
try:
|
|
# Get ALL nodes including control-plane
|
|
nodes = v1.list_node()
|
|
all_nodes = list(nodes.items)
|
|
|
|
if not all_nodes:
|
|
return {
|
|
'success': False,
|
|
'message': 'No nodes available in the cluster',
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
}
|
|
}
|
|
}
|
|
|
|
# Check each node for capacity
|
|
suitable_nodes = []
|
|
all_details = []
|
|
|
|
for node in all_nodes:
|
|
node_name = node.metadata.name
|
|
node_details = {'node': node_name, 'suitable': True, 'reasons': []}
|
|
|
|
# Check node type
|
|
labels = node.metadata.labels or {}
|
|
is_control_plane = 'node-role.kubernetes.io/control-plane' in labels or 'node-role.kubernetes.io/master' in labels
|
|
node_type = 'control-plane' if is_control_plane else 'worker'
|
|
node_details['type'] = node_type
|
|
|
|
# Get node allocatable resources - THIS ALREADY EXCLUDES SYSTEM RESERVATIONS
|
|
allocatable = node.status.allocatable
|
|
|
|
# Parse available resources (allocatable = total - system reservations)
|
|
allocatable_cpu_millicores = parse_resource_to_millicores(allocatable.get('cpu', '0'))
|
|
allocatable_memory_mebibytes = parse_resource_to_mebibytes(allocatable.get('memory', '0'))
|
|
|
|
# For storage, use capacity since ephemeral-storage might not be in allocatable
|
|
capacity = node.status.capacity
|
|
available_storage_gb = parse_storage_to_gb(capacity.get('ephemeral-storage', '0Gi'))
|
|
|
|
# Get currently allocated resources from running pods
|
|
pods = v1.list_pod_for_all_namespaces(field_selector=f"spec.nodeName={node_name}")
|
|
used_cpu_millicores = 0
|
|
used_memory_mebibytes = 0
|
|
|
|
for pod in pods.items:
|
|
if pod.status.phase in ['Running', 'Pending']:
|
|
for container in pod.spec.containers:
|
|
resources = container.resources
|
|
if resources and resources.requests:
|
|
if 'cpu' in resources.requests:
|
|
used_cpu_millicores += parse_resource_to_millicores(resources.requests['cpu'])
|
|
if 'memory' in resources.requests:
|
|
used_memory_mebibytes += parse_resource_to_mebibytes(resources.requests['memory'])
|
|
|
|
# Calculate free resources
|
|
# NO BUFFER NEEDED - allocatable already accounts for system reservations!
|
|
free_cpu_millicores = allocatable_cpu_millicores - used_cpu_millicores
|
|
free_memory_mebibytes = allocatable_memory_mebibytes - used_memory_mebibytes
|
|
|
|
# For storage, assume 20% is used by system/overhead
|
|
free_storage_gb = available_storage_gb * 0.8
|
|
|
|
# Debug logging
|
|
logger.info(f"Node {node_name} ({node_type}):")
|
|
logger.info(f" Allocatable: CPU={allocatable_cpu_millicores/1000:.1f}v, RAM={allocatable_memory_mebibytes/1024:.1f}GB")
|
|
logger.info(f" Used by pods: CPU={used_cpu_millicores/1000:.1f}v, RAM={used_memory_mebibytes/1024:.1f}GB")
|
|
logger.info(f" Free for new pods: CPU={free_cpu_millicores/1000:.1f}v, RAM={free_memory_mebibytes/1024:.1f}GB")
|
|
logger.info(f" Storage: {free_storage_gb:.1f}GB free of {available_storage_gb:.1f}GB")
|
|
logger.info(f" Requested: CPU={cpu_cores}v, RAM={ram_gb}GB, Storage={storage_gb}GB")
|
|
|
|
# Check if node has enough resources
|
|
cpu_passed = free_cpu_millicores >= cpu_request_millicores
|
|
memory_passed = free_memory_mebibytes >= memory_request_mebibytes
|
|
storage_passed = free_storage_gb >= storage_request_gb
|
|
|
|
if not cpu_passed:
|
|
node_details['suitable'] = False
|
|
node_details['reasons'].append(
|
|
f"CPU: {free_cpu_millicores/1000:.1f}v free < {cpu_cores}v needed"
|
|
)
|
|
|
|
if not memory_passed:
|
|
node_details['suitable'] = False
|
|
node_details['reasons'].append(
|
|
f"RAM: {free_memory_mebibytes/1024:.1f}GB free < {ram_gb}GB needed"
|
|
)
|
|
|
|
if not storage_passed:
|
|
node_details['suitable'] = False
|
|
node_details['reasons'].append(
|
|
f"Storage: {free_storage_gb:.1f}GB free < {storage_gb}GB needed"
|
|
)
|
|
|
|
# Add resource details
|
|
node_details.update({
|
|
'free_cpu': free_cpu_millicores,
|
|
'free_memory': free_memory_mebibytes,
|
|
'free_storage': free_storage_gb,
|
|
'requested_cpu': cpu_request_millicores,
|
|
'requested_memory': memory_request_mebibytes,
|
|
'requested_storage': storage_request_gb,
|
|
'is_control_plane': is_control_plane,
|
|
'allocatable_cpu': allocatable_cpu_millicores,
|
|
'allocatable_memory': allocatable_memory_mebibytes,
|
|
'available_storage': available_storage_gb,
|
|
'used_cpu': used_cpu_millicores,
|
|
'used_memory': used_memory_mebibytes
|
|
})
|
|
|
|
all_details.append(node_details)
|
|
|
|
if node_details['suitable']:
|
|
suitable_nodes.append({
|
|
'name': node_name,
|
|
'type': node_type,
|
|
'free_cpu': free_cpu_millicores,
|
|
'free_memory': free_memory_mebibytes,
|
|
'free_storage': free_storage_gb
|
|
})
|
|
logger.info(f"✅ Node {node_name} ({node_type}) has sufficient capacity")
|
|
else:
|
|
logger.info(f"❌ Node {node_name} ({node_type}) insufficient: {node_details['reasons']}")
|
|
|
|
# Check if we found any suitable nodes
|
|
if suitable_nodes:
|
|
suitable_node_names = [n['name'] for n in suitable_nodes]
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f'Sufficient capacity available on {len(suitable_nodes)} node(s)',
|
|
'suitable_nodes': suitable_node_names,
|
|
'node_details': all_details,
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
},
|
|
'suitable_nodes_count': len(suitable_nodes),
|
|
'total_nodes': len(all_nodes)
|
|
}
|
|
}
|
|
else:
|
|
error_details = []
|
|
for detail in all_details:
|
|
if detail['reasons']:
|
|
error_details.append(f"{detail['node']} ({detail['type']}): {', '.join(detail['reasons'])}")
|
|
|
|
return {
|
|
'success': False,
|
|
'message': f'No nodes have sufficient capacity',
|
|
'details': all_details,
|
|
'error_details': error_details,
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
},
|
|
'total_nodes': len(all_nodes),
|
|
'suitable_nodes': 0
|
|
}
|
|
}
|
|
|
|
except Exception as k8s_error:
|
|
logger.error(f"Error checking Kubernetes capacity: {k8s_error}")
|
|
return {
|
|
'success': False,
|
|
'message': f'Error checking cluster capacity: {str(k8s_error)}',
|
|
'summary': {
|
|
'requested': {
|
|
'cpu': f"{cpu_cores} vCPU",
|
|
'ram': f"{ram_gb}GB",
|
|
'storage': f"{storage_gb}GB"
|
|
}
|
|
}
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error in capacity validation: {e}")
|
|
return {
|
|
'success': False,
|
|
'message': f'Capacity validation error: {str(e)}'
|
|
}
|
|
|
|
def parse_resource_to_millicores(resource_str: str) -> float:
|
|
"""Parse CPU resource string to millicores"""
|
|
if not resource_str:
|
|
return 0.0
|
|
|
|
resource_str = str(resource_str).strip()
|
|
|
|
try:
|
|
if resource_str.endswith('m'):
|
|
# Millicores
|
|
return float(resource_str[:-1])
|
|
elif resource_str.replace('.', '', 1).isdigit():
|
|
# Cores (e.g., "1", "0.5", "2.5")
|
|
return float(resource_str) * 1000
|
|
elif resource_str.endswith('n'):
|
|
# Nanocores
|
|
return float(resource_str[:-1]) / 1000000
|
|
else:
|
|
# Try to parse as plain number
|
|
return float(resource_str) * 1000
|
|
except Exception:
|
|
return 0.0
|
|
|
|
def parse_resource_to_mebibytes(resource_str: str) -> float:
|
|
"""Parse memory resource string to MiB (mebibytes)"""
|
|
if not resource_str:
|
|
return 0.0
|
|
|
|
resource_str = str(resource_str).strip()
|
|
|
|
try:
|
|
# Remove any spaces
|
|
resource_str = resource_str.replace(' ', '')
|
|
|
|
if resource_str.endswith('Ki'):
|
|
# Kibibytes
|
|
return float(resource_str[:-2]) / 1024
|
|
elif resource_str.endswith('Mi'):
|
|
# Mebibytes
|
|
return float(resource_str[:-2])
|
|
elif resource_str.endswith('Gi'):
|
|
# Gibibytes
|
|
return float(resource_str[:-2]) * 1024
|
|
elif resource_str.endswith('Ti'):
|
|
# Tebibytes
|
|
return float(resource_str[:-2]) * 1024 * 1024
|
|
elif resource_str.endswith('K'):
|
|
# Kilobytes (SI)
|
|
return float(resource_str[:-1]) / 1000 / 1024 * 1000 # Convert KiB to MiB
|
|
elif resource_str.endswith('M'):
|
|
# Megabytes (SI)
|
|
return float(resource_str[:-1]) / 1024 * 1000 # Convert MB to MiB
|
|
elif resource_str.endswith('G'):
|
|
# Gigabytes (SI)
|
|
return float(resource_str[:-1]) * 1000 # Convert GB to MiB
|
|
else:
|
|
# Assume it's already in bytes
|
|
try:
|
|
bytes_val = float(resource_str)
|
|
return bytes_val / (1024 * 1024) # Convert bytes to MiB
|
|
except:
|
|
return 0.0
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing memory '{resource_str}': {e}")
|
|
return 0.0
|
|
|
|
def parse_storage_to_gb(resource_str: str) -> float:
|
|
"""Parse storage resource string to GB"""
|
|
if not resource_str:
|
|
return 0.0
|
|
|
|
resource_str = str(resource_str).strip()
|
|
|
|
try:
|
|
# Remove any spaces
|
|
resource_str = resource_str.replace(' ', '')
|
|
|
|
if resource_str.endswith('Ki'):
|
|
# Kibibytes
|
|
return float(resource_str[:-2]) / (1024 * 1024)
|
|
elif resource_str.endswith('Mi'):
|
|
# Mebibytes
|
|
return float(resource_str[:-2]) / 1024
|
|
elif resource_str.endswith('Gi'):
|
|
# Gibibytes (1 GiB = 1.07374 GB)
|
|
return float(resource_str[:-2]) * 1.07374
|
|
elif resource_str.endswith('Ti'):
|
|
# Tebibytes
|
|
return float(resource_str[:-2]) * 1024 * 1.07374
|
|
elif resource_str.endswith('K'):
|
|
# Kilobytes (SI)
|
|
return float(resource_str[:-1]) / (1000 * 1000)
|
|
elif resource_str.endswith('M'):
|
|
# Megabytes (SI)
|
|
return float(resource_str[:-1]) / 1000
|
|
elif resource_str.endswith('G'):
|
|
# Gigabytes (SI)
|
|
return float(resource_str[:-1])
|
|
elif resource_str.endswith('T'):
|
|
# Terabytes (SI)
|
|
return float(resource_str[:-1]) * 1000
|
|
else:
|
|
# Assume it's already in bytes
|
|
try:
|
|
bytes_val = float(resource_str)
|
|
return bytes_val / (1000 * 1000 * 1000) # Convert bytes to GB
|
|
except:
|
|
return 0.0
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing storage '{resource_str}': {e}")
|
|
return 0.0
|
|
|
|
def parse_kubernetes_resource(resource_str: str) -> float:
|
|
"""
|
|
Parse Kubernetes resource strings to numeric values.
|
|
|
|
Supports:
|
|
- CPU: "100m" = 100 millicores, "1" = 1 core, "0.5" = 500m
|
|
- Memory: "1Gi" = 1024Mi, "1G" = 1000Mi, "100Mi" = 100Mi, "1M" = 1Mi
|
|
- Storage: "10Gi" = 10GiB, "100G" = 100GB
|
|
|
|
Returns:
|
|
float representing the resource in base units:
|
|
- CPU: millicores
|
|
- Memory: MiB
|
|
- Storage: GB
|
|
"""
|
|
if not resource_str:
|
|
return 0.0
|
|
|
|
resource_str = str(resource_str).strip()
|
|
|
|
try:
|
|
# Handle CPU resources
|
|
if resource_str.endswith('m'):
|
|
# Millicores
|
|
return float(resource_str[:-1])
|
|
elif resource_str.endswith('n'):
|
|
# Nanocores
|
|
return float(resource_str[:-1]) / 1000000
|
|
elif resource_str.replace('.', '', 1).isdigit():
|
|
# Cores (e.g., "1", "0.5", "2.5")
|
|
return float(resource_str) * 1000
|
|
|
|
# Handle memory and storage resources
|
|
# Get numeric value
|
|
import re
|
|
match = re.match(r'^([0-9.]+)([a-zA-Z]*)$', resource_str)
|
|
if not match:
|
|
# Try to parse as plain number
|
|
try:
|
|
return float(resource_str)
|
|
except:
|
|
return 0.0
|
|
|
|
value = float(match.group(1))
|
|
unit = match.group(2).upper()
|
|
|
|
# Memory conversions to MiB
|
|
if unit == 'KI' or unit == 'K':
|
|
return value / 1024 # KiB to MiB
|
|
elif unit == 'MI' or unit == 'M':
|
|
return value # MiB
|
|
elif unit == 'GI' or unit == 'G':
|
|
return value * 1024 # GiB to MiB
|
|
elif unit == 'TI' or unit == 'T':
|
|
return value * 1024 * 1024 # TiB to MiB
|
|
|
|
# If no unit, assume it's already in appropriate units
|
|
return value
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Error parsing resource string '{resource_str}': {e}")
|
|
return 0.0
|
|
|
|
def get_cluster_capacity() -> Dict:
|
|
"""Get cluster capacity using allocatable resources."""
|
|
try:
|
|
if not k8s_manager.connected:
|
|
return {
|
|
'success': False,
|
|
'message': 'Kubernetes not connected'
|
|
}
|
|
|
|
nodes = v1.list_node()
|
|
all_nodes = nodes.items
|
|
|
|
total_allocatable_cpu = 0
|
|
total_allocatable_memory = 0
|
|
total_storage = 0
|
|
|
|
total_used_cpu = 0
|
|
total_used_memory = 0
|
|
|
|
node_details = []
|
|
|
|
# Get pod allocations
|
|
all_pods = v1.list_pod_for_all_namespaces()
|
|
pod_allocations = {}
|
|
|
|
for pod in all_pods.items:
|
|
if pod.spec.node_name and pod.status.phase in ['Running', 'Pending']:
|
|
node_name = pod.spec.node_name
|
|
pod_cpu = 0
|
|
pod_memory = 0
|
|
|
|
for container in pod.spec.containers:
|
|
resources = container.resources
|
|
if resources and resources.requests:
|
|
if 'cpu' in resources.requests:
|
|
pod_cpu += parse_resource_to_millicores(resources.requests['cpu'])
|
|
if 'memory' in resources.requests:
|
|
pod_memory += parse_resource_to_mebibytes(resources.requests['memory'])
|
|
|
|
if node_name not in pod_allocations:
|
|
pod_allocations[node_name] = {'cpu': 0, 'memory': 0}
|
|
pod_allocations[node_name]['cpu'] += pod_cpu
|
|
pod_allocations[node_name]['memory'] += pod_memory
|
|
|
|
for node in all_nodes:
|
|
node_name = node.metadata.name
|
|
|
|
# Node type
|
|
labels = node.metadata.labels or {}
|
|
is_control_plane = 'node-role.kubernetes.io/control-plane' in labels or 'node-role.kubernetes.io/master' in labels
|
|
node_type = 'control-plane' if is_control_plane else 'worker'
|
|
|
|
# Allocatable resources
|
|
allocatable = node.status.allocatable
|
|
cpu = parse_resource_to_millicores(allocatable.get('cpu', '0'))
|
|
memory = parse_resource_to_mebibytes(allocatable.get('memory', '0'))
|
|
|
|
# Storage capacity
|
|
capacity = node.status.capacity
|
|
storage = parse_storage_to_gb(capacity.get('ephemeral-storage', '0Gi'))
|
|
|
|
total_allocatable_cpu += cpu
|
|
total_allocatable_memory += memory
|
|
total_storage += storage
|
|
|
|
# Used resources
|
|
node_alloc = pod_allocations.get(node_name, {'cpu': 0, 'memory': 0})
|
|
total_used_cpu += node_alloc['cpu']
|
|
total_used_memory += node_alloc['memory']
|
|
|
|
# Free resources
|
|
free_cpu = cpu - node_alloc['cpu']
|
|
free_memory = memory - node_alloc['memory']
|
|
free_storage = storage * 0.8 # 80% available
|
|
|
|
# Utilization
|
|
cpu_percent = (node_alloc['cpu'] / cpu * 100) if cpu > 0 else 0
|
|
memory_percent = (node_alloc['memory'] / memory * 100) if memory > 0 else 0
|
|
|
|
node_details.append({
|
|
'name': node_name,
|
|
'type': node_type,
|
|
'allocatable': {
|
|
'cpu': cpu,
|
|
'cpu_cores': cpu / 1000,
|
|
'memory': memory,
|
|
'memory_gb': memory / 1024,
|
|
'storage': storage,
|
|
'storage_gb': storage
|
|
},
|
|
'used': {
|
|
'cpu': node_alloc['cpu'],
|
|
'memory': node_alloc['memory']
|
|
},
|
|
'free': {
|
|
'cpu': free_cpu,
|
|
'memory': free_memory,
|
|
'storage': free_storage
|
|
},
|
|
'utilization': {
|
|
'cpu_percent': round(cpu_percent, 1),
|
|
'memory_percent': round(memory_percent, 1)
|
|
}
|
|
})
|
|
|
|
# Calculate totals
|
|
free_cpu = total_allocatable_cpu - total_used_cpu
|
|
free_memory = total_allocatable_memory - total_used_memory
|
|
free_storage = total_storage * 0.8
|
|
|
|
overall_cpu_percent = (total_used_cpu / total_allocatable_cpu * 100) if total_allocatable_cpu > 0 else 0
|
|
overall_memory_percent = (total_used_memory / total_allocatable_memory * 100) if total_allocatable_memory > 0 else 0
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f'Cluster capacity: {len(all_nodes)} total nodes',
|
|
'total_nodes': len(all_nodes),
|
|
'total_capacity': {
|
|
'cpu': total_allocatable_cpu,
|
|
'cpu_cores': total_allocatable_cpu / 1000,
|
|
'memory': total_allocatable_memory,
|
|
'memory_gb': total_allocatable_memory / 1024,
|
|
'storage': total_storage,
|
|
'storage_gb': total_storage
|
|
},
|
|
'used': {
|
|
'cpu': total_used_cpu,
|
|
'memory': total_used_memory
|
|
},
|
|
'available': {
|
|
'cpu': free_cpu,
|
|
'memory': free_memory,
|
|
'storage': free_storage
|
|
},
|
|
'utilization': {
|
|
'cpu_percent': round(overall_cpu_percent, 1),
|
|
'memory_percent': round(overall_memory_percent, 1)
|
|
},
|
|
'node_details': node_details,
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error getting cluster capacity: {e}")
|
|
return {
|
|
'success': False,
|
|
'message': f'Error getting cluster capacity: {str(e)}',
|
|
'timestamp': datetime.now().isoformat()
|
|
}
|
|
|
|
def calculate_server_cost(config: Dict) -> int:
|
|
"""Calculate server creation cost - UPDATED with new pricing"""
|
|
try:
|
|
cpu_str = config['cpu']
|
|
ram_str = config['ram']
|
|
storage_str = config['storage']
|
|
|
|
# Parsowanie CPU
|
|
cpu_cores = 0
|
|
if 'vCPU' in cpu_str:
|
|
cpu_cores = int(cpu_str.split()[0])
|
|
elif 'GHz' in cpu_str:
|
|
cpu_cores = int(cpu_str.split()[0])
|
|
else:
|
|
import re
|
|
match = re.search(r'\d+', cpu_str)
|
|
if match:
|
|
cpu_cores = int(match.group())
|
|
|
|
# Parsowanie RAM
|
|
ram_gb = 0
|
|
import re
|
|
match = re.search(r'(\d+)\s*GB', ram_str, re.IGNORECASE)
|
|
if match:
|
|
ram_gb = int(match.group(1))
|
|
else:
|
|
match = re.search(r'(\d+)\s*GiB', ram_str, re.IGNORECASE)
|
|
if match:
|
|
ram_gb = int(match.group(1))
|
|
|
|
# Parsowanie Storage
|
|
storage_gb = 0
|
|
match = re.search(r'(\d+)\s*GB', storage_str, re.IGNORECASE)
|
|
if match:
|
|
storage_gb = int(match.group(1))
|
|
else:
|
|
match = re.search(r'(\d+)\s*GiB', storage_str, re.IGNORECASE)
|
|
if match:
|
|
storage_gb = int(match.group(1))
|
|
|
|
# Ceny dopasowane do polskiego rynku (w EURO centach)
|
|
pricing = get_pricing()
|
|
|
|
cpu_cost = cpu_cores * pricing['price_cpu']
|
|
ram_cost = ram_gb * pricing['price_ram']
|
|
storage_cost = storage_gb * pricing['price_storage']
|
|
|
|
server_type = config.get('serverType', 'vps')
|
|
type_multiplier = {
|
|
'vps': pricing.get('multiplier_vps', 1.0),
|
|
'game': pricing.get('multiplier_game', 0.8),
|
|
'website': pricing.get('multiplier_website', 1.5),
|
|
'database': pricing.get('multiplier_database', 1.3)
|
|
}.get(server_type, 1.0)
|
|
|
|
total_cost_cents = int((cpu_cost + ram_cost + storage_cost) * type_multiplier)
|
|
|
|
# Konwersja na kredyty (1 euro = 100 kredytów, 1 cent = 1 kredyt)
|
|
total_credits = total_cost_cents
|
|
|
|
logger.info(f"Server cost calculation for {config['name']}:")
|
|
logger.info(f" CPU: {cpu_cores} vCPU * {pricing['price_cpu']} = {cpu_cost} cents")
|
|
logger.info(f" RAM: {ram_gb}GB * {pricing['price_ram']} = {ram_cost} cents")
|
|
logger.info(f" Storage: {storage_gb}GB * {pricing['price_storage']} = {storage_cost} cents")
|
|
logger.info(f" Type multiplier: {type_multiplier}")
|
|
logger.info(f" Total: {total_cost_cents} cents = {total_credits} credits")
|
|
|
|
return total_credits
|
|
except Exception as e:
|
|
logger.error(f"Error calculating server cost: {e}")
|
|
return 1000 # Domyślny koszt
|
|
|
|
def calculate_monthly_server_cost(server: Dict) -> int:
|
|
"""Oblicz miesięczny koszt serwera"""
|
|
try:
|
|
# Ekstrakcja zasobów z konfiguracji serwera
|
|
cpu_str = server['cpu']
|
|
ram_str = server['ram']
|
|
storage_str = server['storage']
|
|
|
|
# Parsowanie CPU - format może być "2 vCPU" lub podobny
|
|
cpu_cores = 0
|
|
if 'vCPU' in cpu_str:
|
|
cpu_cores = int(cpu_str.split()[0])
|
|
elif 'GHz' in cpu_str:
|
|
cpu_cores = int(cpu_str.split()[0])
|
|
else:
|
|
# Spróbuj wyodrębnić liczbę
|
|
import re
|
|
match = re.search(r'\d+', cpu_str)
|
|
if match:
|
|
cpu_cores = int(match.group())
|
|
|
|
# Parsowanie RAM - format może być "4GB" lub "4 GB"
|
|
ram_gb = 0
|
|
import re
|
|
match = re.search(r'(\d+)\s*GB', ram_str, re.IGNORECASE)
|
|
if match:
|
|
ram_gb = int(match.group(1))
|
|
else:
|
|
# Spróbuj inne formaty
|
|
match = re.search(r'(\d+)\s*GiB', ram_str, re.IGNORECASE)
|
|
if match:
|
|
ram_gb = int(match.group(1))
|
|
|
|
# Parsowanie Storage - format może być "50GB" lub "50 GB"
|
|
storage_gb = 0
|
|
match = re.search(r'(\d+)\s*GB', storage_str, re.IGNORECASE)
|
|
if match:
|
|
storage_gb = int(match.group(1))
|
|
else:
|
|
match = re.search(r'(\d+)\s*GiB', storage_str, re.IGNORECASE)
|
|
if match:
|
|
storage_gb = int(match.group(1))
|
|
|
|
# Kalkulacja kosztów według stawek dopasowanych do polskiego rynku (w EURO centach)
|
|
pricing = get_pricing()
|
|
|
|
cpu_cost = cpu_cores * pricing['price_cpu']
|
|
ram_cost = ram_gb * pricing['price_ram']
|
|
storage_cost = storage_gb * pricing['price_storage']
|
|
|
|
# Koszt bazowy zależny od typu serwera
|
|
base_cost = 0
|
|
server_type = server.get('type', 'vps')
|
|
type_multiplier = {
|
|
'vps': pricing.get('multiplier_vps', 1.0),
|
|
'game': pricing.get('multiplier_game', 0.8),
|
|
'website': pricing.get('multiplier_website', 1.5),
|
|
'database': pricing.get('multiplier_database', 1.3)
|
|
}.get(server_type, 1.0)
|
|
|
|
total_cost = int((base_cost + cpu_cost + ram_cost + storage_cost) * type_multiplier)
|
|
|
|
# Konwersja na kredyty (1 euro = 100 kredytów)
|
|
# Ponieważ koszt jest w centach, dzielimy przez 100 aby dostać euro, a potem *100 aby dostać kredyty
|
|
total_credits = int(total_cost) # ponieważ 1 euro = 100 kredytów, a 1 cent = 1 kredyt
|
|
|
|
logger.info(f"Monthly cost calculation for server {server.get('name', 'unknown')}:")
|
|
logger.info(f" CPU: {cpu_cores} vCPU * {pricing['price_cpu']} = {cpu_cost} cents")
|
|
logger.info(f" RAM: {ram_gb}GB * {pricing['price_ram']} = {ram_cost} cents")
|
|
logger.info(f" Storage: {storage_gb}GB * {pricing['price_storage']} = {storage_cost} cents")
|
|
logger.info(f" Type multiplier: {type_multiplier}")
|
|
logger.info(f" Total: {total_cost} cents = {total_credits} credits")
|
|
|
|
return total_credits
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error calculating monthly cost: {e}")
|
|
return 1000 # Domyślny koszt w przypadku błędu
|