Files
anchorhost-backend/server_capacity.py
T
wfalkowski c177d7981d
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Refactor backend and add registry build workflow
2026-10-20 16:44:49 +00:00

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