"""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)}')