749 lines
29 KiB
Python
749 lines
29 KiB
Python
"""Server lifecycle operations."""
|
|
|
|
|
|
from database_service import database_connection
|
|
from kubernetes import client
|
|
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
|
|
from namespace_utils import user_namespace
|
|
from runtime_config import (
|
|
logger,
|
|
)
|
|
from server_capacity import calculate_monthly_server_cost, calculate_server_cost, validate_server_capacity
|
|
from server_provisioning import wait_and_sync_pod_status, wait_and_sync_vm_status
|
|
from server_records import delete_server_record, get_server_by_id, update_server_status
|
|
import threading
|
|
import time
|
|
from typing import Dict
|
|
from user_service import add_credit_transaction, get_user_by_id, update_user_credits
|
|
|
|
__all__ = ['adjust_server_parameters', 'delete_server', 'restart_server', 'start_server', 'stop_server', 'update_server_resources_in_k8s']
|
|
|
|
|
|
|
|
|
|
def delete_server(server_id: str, user_id: str) -> bool:
|
|
try:
|
|
user = get_user_by_id(user_id)
|
|
if not user:
|
|
raise Exception('User not found')
|
|
|
|
server = get_server_by_id(server_id)
|
|
if not server:
|
|
raise Exception('Server not found')
|
|
|
|
# Verify ownership or admin
|
|
server_owner_id = str(server.get('owner_id') or server.get('ownerId', ''))
|
|
req_id = str(user_id)
|
|
|
|
if server_owner_id != req_id and user.get('role') != 'admin':
|
|
raise Exception("Unauthorized: Access denied")
|
|
|
|
# Get owner for namespace
|
|
if server_owner_id == req_id:
|
|
owner = user
|
|
else:
|
|
owner = get_user_by_id(server_owner_id)
|
|
if not owner:
|
|
raise Exception("Server owner not found")
|
|
|
|
original_cost = calculate_server_cost({
|
|
'serverType': server['type'],
|
|
'cpu': server['cpu'],
|
|
'ram': server['ram'],
|
|
'storage': server['storage']
|
|
})
|
|
refund_amount = int(original_cost * 0.5)
|
|
|
|
if not k8s_manager.connected:
|
|
if delete_server_record(server_id):
|
|
# Removed refund logic
|
|
pass
|
|
return True
|
|
|
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
|
|
|
try:
|
|
custom_objects_api.delete_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
custom_objects_api.delete_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachineinstances",
|
|
name=server_id
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
apps_v1.delete_namespaced_deployment(name=server_id, namespace=namespace)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
v1.delete_namespaced_service(name=f"{server_id}-service", namespace=namespace)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
v1.delete_namespaced_persistent_volume_claim(name=f"{server_id}-data", namespace=namespace)
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
v1.delete_namespaced_persistent_volume_claim(name=f"{server_id}-rootdisk", namespace=namespace)
|
|
except Exception:
|
|
pass
|
|
|
|
if delete_server_record(server_id):
|
|
# Removed refund logic
|
|
pass
|
|
|
|
return True
|
|
except Exception as e:
|
|
raise Exception(f'Failed to delete server: {str(e)}')
|
|
|
|
def start_server(server_id: str, user_id: str) -> bool:
|
|
try:
|
|
# Step 1: Verify server exists
|
|
server = get_server_by_id(server_id)
|
|
if not server:
|
|
raise Exception('Server not found')
|
|
|
|
# Step 2: Get user
|
|
user = get_user_by_id(user_id)
|
|
if not user:
|
|
raise Exception('User not found')
|
|
|
|
# Verify server ownership or admin access
|
|
server_owner_id = str(server.get('owner_id') or server.get('ownerId', ''))
|
|
req_id = str(user_id)
|
|
|
|
if server_owner_id != req_id and user.get('role') != 'admin':
|
|
raise Exception("Unauthorized: You don't own this server")
|
|
|
|
# Get owner for namespace determination
|
|
if server_owner_id == req_id:
|
|
owner = user
|
|
else:
|
|
owner = get_user_by_id(server_owner_id)
|
|
if not owner:
|
|
raise Exception("Server owner not found")
|
|
|
|
# Step 4: Check if user has enough credits (must be non-negative)
|
|
# Use owner's credits check if admin is starting, or just skip?
|
|
# Ideally check owner's credits.
|
|
owner_credits = owner.get('credits', 0)
|
|
if owner_credits < 0:
|
|
raise Exception(f'Insufficient credits to start server. Owner has {owner_credits} credits.')
|
|
|
|
# Step 5: Check capacity if server is not already running
|
|
if server.get('status') != 'Running':
|
|
logger.info(f"🔍 Validating capacity before starting server: {server_id}")
|
|
server_config = {
|
|
'type': server['type'],
|
|
'cpu': server['cpu'],
|
|
'ram': server['ram'],
|
|
'storage': server['storage']
|
|
}
|
|
capacity_check = validate_server_capacity(server_config)
|
|
if not capacity_check['success']:
|
|
error_msg = f"Insufficient capacity to start server: {capacity_check['message']}"
|
|
logger.error(f"❌ Capacity validation failed: {error_msg}")
|
|
raise Exception(error_msg)
|
|
logger.info(f"✅ Capacity validation passed: {capacity_check['message']}")
|
|
|
|
if not k8s_manager.connected:
|
|
update_server_status(server_id, 'Running')
|
|
return True
|
|
|
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
|
|
|
try:
|
|
patch = {'spec': {'runStrategy': 'Always'}}
|
|
custom_objects_api.patch_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id,
|
|
body=patch
|
|
)
|
|
update_server_status(server_id, 'Starting')
|
|
|
|
threading.Thread(
|
|
target=wait_and_sync_vm_status,
|
|
args=(server_id, owner['id'], namespace),
|
|
daemon=True
|
|
).start()
|
|
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
patch = {'spec': {'replicas': 1}}
|
|
apps_v1.patch_namespaced_deployment_scale(name=server_id, namespace=namespace, body=patch)
|
|
update_server_status(server_id, 'Starting')
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
return False
|
|
except Exception as e:
|
|
raise Exception(f'Failed to start server: {str(e)}')
|
|
|
|
def stop_server(server_id: str, user_id: str) -> bool:
|
|
try:
|
|
# Step 1: Verify server exists
|
|
server = get_server_by_id(server_id)
|
|
if not server:
|
|
raise Exception('Server not found')
|
|
|
|
# Step 2: Get user
|
|
user = get_user_by_id(user_id)
|
|
if not user:
|
|
raise Exception('User not found')
|
|
|
|
# Verify server ownership or admin access
|
|
server_owner_id = str(server.get('owner_id') or server.get('ownerId', ''))
|
|
req_id = str(user_id)
|
|
|
|
if server_owner_id != req_id and user.get('role') != 'admin':
|
|
raise Exception("Unauthorized: You don't own this server")
|
|
|
|
# Get owner for namespace determination
|
|
if server_owner_id == req_id:
|
|
owner = user
|
|
else:
|
|
owner = get_user_by_id(server_owner_id)
|
|
if not owner:
|
|
raise Exception("Server owner not found")
|
|
|
|
if not k8s_manager.connected:
|
|
update_server_status(server_id, 'Stopped')
|
|
return True
|
|
|
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
|
|
|
try:
|
|
patch = {'spec': {'runStrategy': 'Halted'}}
|
|
custom_objects_api.patch_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id,
|
|
body=patch
|
|
)
|
|
update_server_status(server_id, 'Stopping')
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
patch = {'spec': {'replicas': 0}}
|
|
apps_v1.patch_namespaced_deployment_scale(name=server_id, namespace=namespace, body=patch)
|
|
update_server_status(server_id, 'Stopping')
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
return False
|
|
except Exception as e:
|
|
raise Exception(f'Failed to stop server: {str(e)}')
|
|
|
|
def restart_server(server_id: str, user_id: str, skip_checks: bool = False) -> bool:
|
|
try:
|
|
# Step 1: Verify server exists
|
|
server = get_server_by_id(server_id)
|
|
if not server:
|
|
raise Exception('Server not found')
|
|
|
|
# Step 2: Get user
|
|
user = get_user_by_id(user_id)
|
|
if not user:
|
|
raise Exception('User not found')
|
|
|
|
# Verify server ownership or admin access
|
|
server_owner_id = str(server.get('owner_id') or server.get('ownerId', ''))
|
|
req_id = str(user_id)
|
|
|
|
if server_owner_id != req_id and user.get('role') != 'admin':
|
|
raise Exception("Unauthorized: You don't own this server")
|
|
|
|
# Get owner for namespace determination
|
|
if server_owner_id == req_id:
|
|
owner = user
|
|
else:
|
|
owner = get_user_by_id(server_owner_id)
|
|
if not owner:
|
|
raise Exception("Server owner not found")
|
|
|
|
# Step 4: Check if user has enough credits (skip for downgrades)
|
|
if not skip_checks:
|
|
# Check owner's credits
|
|
owner_credits = owner.get('credits', 0)
|
|
if owner_credits < 0:
|
|
raise Exception(f'Insufficient credits to restart server. Owner has {owner_credits} credits.')
|
|
|
|
# Step 5: Check capacity before restarting (skip for downgrades)
|
|
if not skip_checks:
|
|
logger.info(f"🔍 Validating capacity before restarting server: {server_id}")
|
|
server_config = {
|
|
'type': server['type'],
|
|
'cpu': server['cpu'],
|
|
'ram': server['ram'],
|
|
'storage': server['storage']
|
|
}
|
|
capacity_check = validate_server_capacity(server_config)
|
|
if not capacity_check['success']:
|
|
error_msg = f"Insufficient capacity to restart server: {capacity_check['message']}"
|
|
logger.error(f"❌ Capacity validation failed: {error_msg}")
|
|
raise Exception(error_msg)
|
|
logger.info(f"✅ Capacity validation passed: {capacity_check['message']}")
|
|
|
|
if not k8s_manager.connected:
|
|
update_server_status(server_id, 'Restarting')
|
|
time.sleep(2)
|
|
update_server_status(server_id, 'Running')
|
|
return True
|
|
|
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
|
update_server_status(server_id, 'Restarting')
|
|
|
|
try:
|
|
stop_patch = {'spec': {'runStrategy': 'Halted'}}
|
|
custom_objects_api.patch_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id,
|
|
body=stop_patch
|
|
)
|
|
|
|
time.sleep(5)
|
|
|
|
start_patch = {'spec': {'runStrategy': 'Always'}}
|
|
custom_objects_api.patch_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id,
|
|
body=start_patch
|
|
)
|
|
|
|
threading.Thread(
|
|
target=wait_and_sync_vm_status,
|
|
args=(server_id, owner['id'], namespace),
|
|
daemon=True
|
|
).start()
|
|
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
pods = k8s_manager.get_pods_for_server(namespace, server_id)
|
|
if pods:
|
|
for pod in pods:
|
|
v1.delete_namespaced_pod(name=pod, namespace=namespace)
|
|
update_server_status(server_id, 'Restarting')
|
|
|
|
# Wait for pods to restart and update status
|
|
threading.Thread(
|
|
target=wait_and_sync_pod_status,
|
|
args=(server_id, user_id, namespace),
|
|
daemon=True
|
|
).start()
|
|
|
|
return True
|
|
except Exception:
|
|
pass
|
|
|
|
return False
|
|
except Exception as e:
|
|
raise Exception(f'Failed to restart server: {str(e)}')
|
|
|
|
def update_server_resources_in_k8s(server: Dict, new_cpu: str, new_ram: str, new_storage: str, namespace: str) -> bool:
|
|
"""Update server resources (CPU, RAM, Storage) in Kubernetes"""
|
|
try:
|
|
server_type = server.get('type')
|
|
server_id = server['id']
|
|
|
|
if server_type == 'vps':
|
|
# Update VirtualMachine resources
|
|
cpu_cores = int(new_cpu.split()[0])
|
|
memory_size = new_ram.replace('GB', 'Gi')
|
|
|
|
# Parse storage size
|
|
import re
|
|
storage_match = re.search(r'(\d+)\s*GB', new_storage, re.IGNORECASE)
|
|
if storage_match:
|
|
storage_gb = int(storage_match.group(1))
|
|
storage_size = f"{storage_gb}Gi"
|
|
else:
|
|
storage_match = re.search(r'(\d+)\s*GiB', new_storage, re.IGNORECASE)
|
|
if storage_match:
|
|
storage_size = f"{storage_match.group(1)}Gi"
|
|
else:
|
|
raise Exception(f"Invalid storage format: {new_storage}")
|
|
|
|
# Get current VM spec to preserve network interfaces and other configurations
|
|
try:
|
|
current_vm = custom_objects_api.get_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id
|
|
)
|
|
|
|
# Preserve existing domain configuration (especially devices/interfaces)
|
|
current_domain = current_vm.get('spec', {}).get('template', {}).get('spec', {}).get('domain', {})
|
|
current_devices = current_domain.get('devices', {})
|
|
current_machine = current_domain.get('machine', {})
|
|
|
|
# Build patch with CPU and resources, explicitly preserving devices (network interfaces)
|
|
domain_patch = {
|
|
'cpu': {
|
|
'cores': cpu_cores,
|
|
'sockets': 1,
|
|
'threads': 1
|
|
},
|
|
'resources': {
|
|
'requests': {
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 100}m'
|
|
},
|
|
'limits': {
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 1000}m'
|
|
}
|
|
}
|
|
}
|
|
|
|
# Preserve devices (network interfaces) if they exist
|
|
if current_devices:
|
|
domain_patch['devices'] = current_devices
|
|
|
|
# Preserve machine type if it exists
|
|
if current_machine:
|
|
domain_patch['machine'] = current_machine
|
|
|
|
vm_patch = {
|
|
'spec': {
|
|
'template': {
|
|
'spec': {
|
|
'domain': domain_patch
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Use JSON merge patch (default) which preserves fields not mentioned
|
|
# But we explicitly include devices to ensure they're preserved
|
|
custom_objects_api.patch_namespaced_custom_object(
|
|
group="kubevirt.io",
|
|
version="v1",
|
|
namespace=namespace,
|
|
plural="virtualmachines",
|
|
name=server_id,
|
|
body=vm_patch
|
|
)
|
|
|
|
logger.info(f"✓ Updated VM {server_id} resources while preserving network interfaces")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating VM resources: {e}")
|
|
raise
|
|
|
|
# Update storage (PVC resize)
|
|
# Find the PVC name - it's usually {server_id}-rootdisk
|
|
pvc_name = f"{server_id}-rootdisk"
|
|
try:
|
|
pvc = v1.read_namespaced_persistent_volume_claim(name=pvc_name, namespace=namespace)
|
|
if pvc:
|
|
# Resize PVC
|
|
pvc_patch = {
|
|
'spec': {
|
|
'resources': {
|
|
'requests': {
|
|
'storage': storage_size
|
|
}
|
|
}
|
|
}
|
|
}
|
|
v1.patch_namespaced_persistent_volume_claim(
|
|
name=pvc_name,
|
|
namespace=namespace,
|
|
body=pvc_patch
|
|
)
|
|
logger.info(f"✓ Updated PVC {pvc_name} to {storage_size}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not resize PVC {pvc_name}: {e}")
|
|
|
|
logger.info(f"✓ Updated VM {server_id} resources: CPU={cpu_cores}, RAM={memory_size}")
|
|
return True
|
|
|
|
elif server_type == 'game':
|
|
# Update Deployment resources
|
|
cpu_cores = int(new_cpu.split()[0])
|
|
ram_gb = int(re.search(r'(\d+)\s*GB', new_ram, re.IGNORECASE).group(1))
|
|
memory_size = f"{ram_gb}Gi"
|
|
|
|
# Parse storage size
|
|
storage_match = re.search(r'(\d+)\s*GB', new_storage, re.IGNORECASE)
|
|
if storage_match:
|
|
storage_gb = int(storage_match.group(1))
|
|
storage_size = f"{storage_gb}Gi"
|
|
else:
|
|
storage_match = re.search(r'(\d+)\s*GiB', new_storage, re.IGNORECASE)
|
|
if storage_match:
|
|
storage_size = f"{storage_match.group(1)}Gi"
|
|
else:
|
|
raise Exception(f"Invalid storage format: {new_storage}")
|
|
|
|
# Get current deployment
|
|
deployment = apps_v1.read_namespaced_deployment(name=server_id, namespace=namespace)
|
|
|
|
# Update container resources and environment variables
|
|
if deployment.spec.template.spec.containers:
|
|
container = deployment.spec.template.spec.containers[0]
|
|
container.resources = client.V1ResourceRequirements(
|
|
requests={
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 100}m'
|
|
},
|
|
limits={
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 1000}m'
|
|
}
|
|
)
|
|
|
|
# Update MEMORY environment variable if it exists
|
|
if container.env:
|
|
for env_var in container.env:
|
|
if env_var.name == 'MEMORY':
|
|
env_var.value = f"{ram_gb}G"
|
|
logger.info(f"✓ Updated MEMORY env var to {ram_gb}G for server {server_id}")
|
|
|
|
# Update deployment
|
|
apps_v1.patch_namespaced_deployment(
|
|
name=server_id,
|
|
namespace=namespace,
|
|
body=deployment
|
|
)
|
|
|
|
# Update storage (PVC resize)
|
|
pvc_name = f"{server_id}-data"
|
|
try:
|
|
pvc = v1.read_namespaced_persistent_volume_claim(name=pvc_name, namespace=namespace)
|
|
if pvc:
|
|
pvc_patch = {
|
|
'spec': {
|
|
'resources': {
|
|
'requests': {
|
|
'storage': storage_size
|
|
}
|
|
}
|
|
}
|
|
}
|
|
v1.patch_namespaced_persistent_volume_claim(
|
|
name=pvc_name,
|
|
namespace=namespace,
|
|
body=pvc_patch
|
|
)
|
|
logger.info(f"✓ Updated PVC {pvc_name} to {storage_size}")
|
|
except Exception as e:
|
|
logger.warning(f"Could not resize PVC {pvc_name}: {e}")
|
|
|
|
logger.info(f"✓ Updated Deployment {server_id} resources: CPU={cpu_cores}, RAM={memory_size}")
|
|
return True
|
|
|
|
else:
|
|
# For other server types (website, database), update deployment similarly
|
|
cpu_cores = int(new_cpu.split()[0])
|
|
ram_gb = int(re.search(r'(\d+)\s*GB', new_ram, re.IGNORECASE).group(1))
|
|
memory_size = f"{ram_gb}Gi"
|
|
|
|
try:
|
|
deployment = apps_v1.read_namespaced_deployment(name=server_id, namespace=namespace)
|
|
if deployment.spec.template.spec.containers:
|
|
container = deployment.spec.template.spec.containers[0]
|
|
container.resources = client.V1ResourceRequirements(
|
|
requests={
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 100}m'
|
|
},
|
|
limits={
|
|
'memory': memory_size,
|
|
'cpu': f'{cpu_cores * 1000}m'
|
|
}
|
|
)
|
|
|
|
# Update MEMORY environment variable if it exists
|
|
if container.env:
|
|
for env_var in container.env:
|
|
if env_var.name == 'MEMORY':
|
|
env_var.value = f"{ram_gb}G"
|
|
logger.info(f"✓ Updated MEMORY env var to {ram_gb}G for server {server_id}")
|
|
|
|
apps_v1.patch_namespaced_deployment(
|
|
name=server_id,
|
|
namespace=namespace,
|
|
body=deployment
|
|
)
|
|
logger.info(f"✓ Updated Deployment {server_id} resources")
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Error updating deployment resources: {e}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error updating server resources in K8s: {e}")
|
|
return False
|
|
|
|
def adjust_server_parameters(server_id: str, user_id: str, new_cpu: str, new_ram: str, new_storage: str) -> Dict:
|
|
"""Adjust server parameters (upgrade/downgrade) with credit calculation"""
|
|
try:
|
|
# Get user first
|
|
user = get_user_by_id(user_id)
|
|
if not user:
|
|
raise Exception("User not found")
|
|
|
|
# Get server
|
|
server = get_server_by_id(server_id)
|
|
if not server:
|
|
raise Exception("Server not found")
|
|
|
|
# Verify server ownership or admin access
|
|
server_owner_id = str(server.get('owner_id') or server.get('ownerId', ''))
|
|
req_id = str(user_id)
|
|
|
|
if server_owner_id != req_id and user.get('role') != 'admin':
|
|
raise Exception("Unauthorized: You don't own this server")
|
|
|
|
# Step 1: Validate capacity for new resources BEFORE proceeding
|
|
new_server_config = {
|
|
'name': server['name'],
|
|
'type': server['type'],
|
|
'cpu': new_cpu,
|
|
'ram': new_ram,
|
|
'storage': new_storage
|
|
}
|
|
|
|
# Check if this is an upgrade (increased resources)
|
|
current_cpu = int(server['cpu'].split()[0])
|
|
current_ram = int(server['ram'].replace('GB', ''))
|
|
current_storage = int(server['storage'].replace('GB', ''))
|
|
new_cpu_val = int(new_cpu.split()[0])
|
|
new_ram_val = int(new_ram.replace('GB', ''))
|
|
new_storage_val = int(new_storage.replace('GB', ''))
|
|
|
|
is_upgrade = (new_cpu_val > current_cpu or new_ram_val > current_ram or new_storage_val > current_storage)
|
|
|
|
# Only check capacity if upgrading (increasing resources)
|
|
if is_upgrade:
|
|
logger.info(f"🔍 Validating capacity for server parameter upgrade: {server_id}")
|
|
capacity_check = validate_server_capacity(new_server_config)
|
|
if not capacity_check['success']:
|
|
error_msg = f"Insufficient capacity: {capacity_check['message']}"
|
|
logger.error(f"❌ Capacity validation failed: {error_msg}")
|
|
raise Exception(error_msg)
|
|
logger.info(f"✅ Capacity validation passed: {capacity_check['message']}")
|
|
|
|
# Calculate current monthly cost
|
|
current_monthly_cost = calculate_monthly_server_cost(server)
|
|
|
|
# Calculate new monthly cost
|
|
new_monthly_cost = calculate_monthly_server_cost(new_server_config)
|
|
|
|
# Calculate credit charge based on upgrade/downgrade
|
|
if is_upgrade:
|
|
# For upgrades: charge the difference between new and current monthly costs
|
|
# Ensure we never charge negative credits (safeguard)
|
|
credits_to_charge = max(0, new_monthly_cost - current_monthly_cost)
|
|
else:
|
|
# For downgrades: no charge
|
|
credits_to_charge = 0
|
|
|
|
logger.info(f"Server parameter adjustment for {server_id}:")
|
|
logger.info(f" Current monthly cost: {current_monthly_cost} credits")
|
|
logger.info(f" New monthly cost: {new_monthly_cost} credits")
|
|
logger.info(f" Credits to charge: {credits_to_charge} credits")
|
|
logger.info(f" Is upgrade: {is_upgrade}")
|
|
|
|
# Only check credits when UPGRADING (increasing resources)
|
|
# When downgrading, we should never check credits regardless of credits_to_charge value
|
|
if is_upgrade and credits_to_charge > 0:
|
|
user_credits = user.get('credits', 0)
|
|
if user_credits < credits_to_charge:
|
|
raise Exception(f'Insufficient credits. Need {credits_to_charge} credits, have {user_credits}')
|
|
|
|
# Deduct credits
|
|
new_credits = user_credits - credits_to_charge
|
|
if not update_user_credits(user_id, new_credits):
|
|
raise Exception('Failed to deduct credits')
|
|
|
|
add_credit_transaction(
|
|
user_id,
|
|
-credits_to_charge,
|
|
'server_upgrade',
|
|
f'Server upgrade: {server["name"]} - {credits_to_charge} credits'
|
|
)
|
|
logger.info(f"✓ Charged {credits_to_charge} credits for server upgrade")
|
|
elif not is_upgrade:
|
|
# For downgrades, never check credits - allow the downgrade regardless
|
|
logger.info(f"✓ Server downgrade: no credit check required (decreasing resources)")
|
|
elif credits_to_charge <= 0:
|
|
# credits_to_charge <= 0, no charge needed (edge case where upgrade doesn't require additional credits)
|
|
logger.info(f"✓ No credit adjustment needed")
|
|
|
|
# Update server in database
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
UPDATE servers
|
|
SET cpu = ?, ram = ?, storage = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''', (new_cpu, new_ram, new_storage, server_id))
|
|
conn.commit()
|
|
|
|
# Update server resources in Kubernetes
|
|
namespace = server.get('namespace') or user_namespace(user['username'])
|
|
if k8s_manager.connected:
|
|
if not update_server_resources_in_k8s(server, new_cpu, new_ram, new_storage, namespace):
|
|
logger.warning(f"Failed to update server resources in K8s, but database was updated")
|
|
|
|
# Restart server to apply changes
|
|
# Skip credit/capacity checks for downgrades (when credits_to_charge <= 0)
|
|
skip_checks = credits_to_charge <= 0
|
|
restart_server(server_id, user_id, skip_checks=skip_checks)
|
|
|
|
logger.info(f"✓ Successfully adjusted server parameters for {server_id}")
|
|
|
|
return {
|
|
'success': True,
|
|
'message': 'Server parameters adjusted successfully',
|
|
'credits_charged': credits_to_charge if credits_to_charge > 0 else 0,
|
|
'new_monthly_cost': new_monthly_cost,
|
|
'current_monthly_cost': current_monthly_cost,
|
|
'is_upgrade': is_upgrade
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error adjusting server parameters: {e}")
|
|
raise Exception(f'Failed to adjust server parameters: {str(e)}')
|