Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
This commit is contained in:
commit
c177d7981d
43 files changed
+18124
No files matched your search
@@ -0,0 +1,747 @@
|
||||
"""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 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 = f"user-{owner['username']}".lower()
|
||||
|
||||
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 = f"user-{owner['username']}".lower()
|
||||
|
||||
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 = f"user-{owner['username']}".lower()
|
||||
|
||||
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 = f"user-{owner['username']}".lower()
|
||||
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 = f"user-{user['username']}".lower()
|
||||
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)}')
|
||||
Reference in new issue
Block a user