"""Servers API routes for AnchorHost.""" from datetime import datetime from flask import Blueprint, jsonify, request import json from kubernetes_service import k8s_manager import requests from runtime_config import ( MODRINTH_API_BASE, logger, ) from server_access import expose_port, remove_port from server_capacity import get_cluster_capacity, validate_server_capacity from server_lifecycle import adjust_server_parameters from server_provisioning import reinstall_server, sync_server_ports_from_k8s, sync_server_status_from_k8s from server_records import get_server_by_id, get_user_servers from user_service import add_credits, authenticate_user, get_bearer_token, get_user_credits, require_admin __all__ = ['add_credits_api', 'adjust_server_parameters_api', 'check_capacity_api', 'download_plugin_to_server', 'expose_port_api', 'get_cluster_capacity_api', 'get_modrinth_project', 'get_modrinth_tags', 'get_modrinth_version', 'get_modrinth_versions', 'reinstall_server_api', 'remove_port_api', 'search_modrinth_plugins', 'sync_server_status_api'] bp = Blueprint("servers", __name__) @bp.route('/api/capacity/check', methods=['POST']) def check_capacity_api(): """API endpoint to check capacity for a server configuration""" try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'success': False, 'error': 'Unauthorized'}), 401 data = request.get_json() if not data: return jsonify({'success': False, 'error': 'No configuration provided'}), 400 # Validate required fields required_fields = ['cpu', 'ram', 'storage', 'serverType'] for field in required_fields: if field not in data: return jsonify({'success': False, 'error': f'Missing field: {field}'}), 400 # Run capacity validation capacity_result = validate_server_capacity(data) # Also get cluster overview cluster_capacity = get_cluster_capacity() # Extract useful information for frontend response_data = { 'success': True, 'capacity_check': { 'success': capacity_result.get('success', False), 'message': capacity_result.get('message', ''), 'suitable_nodes': capacity_result.get('suitable_nodes', []), 'node_details': capacity_result.get('node_details', []), 'details': capacity_result.get('details', []), 'error_details': capacity_result.get('error_details', []), 'summary': capacity_result.get('summary', {}) }, 'cluster_capacity': cluster_capacity, 'request': { 'cpu': data['cpu'], 'ram': data['ram'], 'storage': data['storage'], 'server_type': data['serverType'] } } return jsonify(response_data), 200 except Exception as e: logger.error(f"Error in capacity check API: {e}") return jsonify({'success': False, 'error': str(e)}), 500 @bp.route('/api/capacity/cluster', methods=['GET']) def get_cluster_capacity_api(): """API endpoint to get cluster capacity overview""" try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'success': False, 'error': 'Unauthorized'}), 401 capacity = get_cluster_capacity() return jsonify(capacity), 200 if capacity['success'] else 500 except Exception as e: logger.error(f"Error getting cluster capacity: {e}") return jsonify({ 'success': False, 'error': str(e), 'timestamp': datetime.now().isoformat() }), 500 @bp.route('/api/servers//sync', methods=['POST']) def sync_server_status_api(server_id): try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Unauthorized'}), 401 sync_server_status_from_k8s(server_id, user['id']) sync_server_ports_from_k8s(server_id, user['id']) server = get_server_by_id(server_id) if not server: return jsonify({'error': 'Server not found'}), 404 return jsonify({ 'success': True, 'server': server }), 200 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/api/servers//reinstall', methods=['POST']) def reinstall_server_api(server_id): try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Unauthorized'}), 401 success = reinstall_server(server_id, user['id']) if success: return jsonify({ 'success': True, 'message': 'Server reinstall started' }), 200 else: return jsonify({'error': 'Failed to reinstall server'}), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/api/users//credits', methods=['POST']) def add_credits_api(user_id): try: if not require_admin(): return jsonify({'error': 'Unauthorized'}), 403 data = request.get_json(silent=True) or {} amount = data.get('amount') if isinstance(amount, bool) or not isinstance(amount, int) or amount <= 0: return jsonify({'error': 'Invalid amount'}), 400 success = add_credits(user_id, amount) if success: return jsonify({ 'success': True, 'message': f'Added {amount} credits', 'new_balance': get_user_credits(user_id) }), 200 else: return jsonify({'error': 'Failed to add credits'}), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/api/modrinth/search', methods=['GET']) def search_modrinth_plugins(): """Search for plugins on Modrinth""" try: query = request.args.get('query', '') limit = int(request.args.get('limit', 20)) offset = int(request.args.get('offset', 0)) project_type = request.args.get('project_type', 'plugin') # plugin, mod, modpack, etc. loaders = request.args.get('loaders', '') # paper, spigot, bukkit, etc. game_versions = request.args.get('game_versions', '') # Build facets array for Modrinth API # Facets format: [['category:value1', 'category:value2'], ['category2:value3']] # Each inner array represents OR conditions, outer array represents AND conditions facets = [['project_type:' + project_type]] if loaders: loader_list = [loader.strip() for loader in loaders.split(',') if loader.strip()] if loader_list: # Create a single array with all loader filters (OR condition) loader_facets = ['loaders:' + loader for loader in loader_list] facets.append(loader_facets) if game_versions: version_list = [version.strip() for version in game_versions.split(',') if version.strip()] if version_list: # Create a single array with all version filters (OR condition) version_facets = ['versions:' + version for version in version_list] facets.append(version_facets) # Build query parameters params = { 'query': query, 'limit': limit, 'offset': offset, 'facets': json.dumps(facets) } response = requests.get(f'{MODRINTH_API_BASE}/search', params=params, headers={ 'User-Agent': 'AnchorHost/1.0 (contact@anchorhost.eu)' }) if response.status_code == 200: return jsonify(response.json()), 200 else: return jsonify({'error': 'Failed to search Modrinth', 'status': response.status_code}), response.status_code except Exception as e: logger.error(f"Error searching Modrinth: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/modrinth/project/', methods=['GET']) def get_modrinth_project(project_id): """Get project details from Modrinth""" try: response = requests.get( f'{MODRINTH_API_BASE}/project/{project_id}', headers={'User-Agent': 'AnchorHost/1.0 (contact@anchorhost.eu)'} ) if response.status_code == 200: return jsonify(response.json()), 200 else: return jsonify({'error': 'Project not found', 'status': response.status_code}), response.status_code except Exception as e: logger.error(f"Error fetching Modrinth project: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/modrinth/project//versions', methods=['GET']) def get_modrinth_versions(project_id): """Get versions for a Modrinth project""" try: game_versions = request.args.get('game_versions', '') loaders = request.args.get('loaders', '') params = {} if game_versions: params['game_versions'] = game_versions.split(',') if loaders: params['loaders'] = loaders.split(',') response = requests.get( f'{MODRINTH_API_BASE}/project/{project_id}/version', params=params, headers={'User-Agent': 'AnchorHost/1.0 (contact@anchorhost.eu)'} ) if response.status_code == 200: return jsonify(response.json()), 200 else: return jsonify({'error': 'Failed to fetch versions', 'status': response.status_code}), response.status_code except Exception as e: logger.error(f"Error fetching Modrinth versions: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/modrinth/project//version/', methods=['GET']) def get_modrinth_version(project_id, version_id): """Get specific version details""" try: response = requests.get( f'{MODRINTH_API_BASE}/version/{version_id}', headers={'User-Agent': 'AnchorHost/1.0 (contact@anchorhost.eu)'} ) if response.status_code == 200: return jsonify(response.json()), 200 else: return jsonify({'error': 'Version not found', 'status': response.status_code}), response.status_code except Exception as e: logger.error(f"Error fetching Modrinth version: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/modrinth/tags', methods=['GET']) def get_modrinth_tags(): """Get available tags/categories from Modrinth""" try: response = requests.get( f'{MODRINTH_API_BASE}/tag', headers={'User-Agent': 'AnchorHost/1.0 (contact@anchorhost.eu)'} ) if response.status_code == 200: return jsonify(response.json()), 200 else: return jsonify({'error': 'Failed to fetch tags', 'status': response.status_code}), response.status_code except Exception as e: logger.error(f"Error fetching Modrinth tags: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/plugins/download-to-server', methods=['POST']) def download_plugin_to_server(): """Download a plugin from URL and upload it to a server's plugins directory""" try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Not authenticated'}), 401 data = request.get_json() server_id = data.get('server_id') plugin_url = data.get('plugin_url') plugin_name = data.get('plugin_name', 'plugin') file_name = data.get('file_name', 'plugin.jar') if not server_id or not plugin_url: return jsonify({'error': 'Server ID and plugin URL are required'}), 400 # Verify server belongs to user user_servers = get_user_servers(user['id']) server = next((s for s in user_servers if s['id'] == server_id), None) if not server: return jsonify({'error': 'Server not found or access denied'}), 404 # Check if server is Minecraft server if server.get('image') != 'minecraft' or server.get('type') != 'game': return jsonify({'error': 'Plugin downloads are only available for Minecraft game servers'}), 400 # Download plugin file logger.info(f"Downloading plugin from {plugin_url} for server {server_id}") plugin_response = requests.get(plugin_url, stream=True, timeout=30) if plugin_response.status_code != 200: return jsonify({'error': f'Failed to download plugin: HTTP {plugin_response.status_code}'}), 500 # Determine plugins directory based on server type # For Minecraft servers, plugins go in /data/plugins directory plugins_dir = '/data/plugins' file_path = f'{plugins_dir}/{file_name}' # Save plugin to temporary file first import tempfile import os with tempfile.NamedTemporaryFile(delete=False, suffix='.jar') as temp_file: temp_file_path = temp_file.name for chunk in plugin_response.iter_content(chunk_size=8192): if chunk: temp_file.write(chunk) try: # Upload to server namespace = f"user-{user['username']}".lower() pods = k8s_manager.get_pods_for_server(namespace, server_id) if not pods: return jsonify({'error': 'Server pod not found'}), 404 # Ensure plugins directory exists k8s_manager.exec_command(namespace, pods[0], ['mkdir', '-p', plugins_dir]) # Upload file using write_file_binary_from_disk success = k8s_manager.write_file_binary_from_disk(namespace, pods[0], file_path, temp_file_path) if success: logger.info(f"Plugin {file_name} successfully uploaded to server {server_id}") return jsonify({ 'success': True, 'message': f'Plugin {plugin_name} downloaded and installed successfully', 'file_path': file_path }), 200 else: return jsonify({'error': 'Failed to upload plugin to server'}), 500 finally: # Clean up temporary file try: if os.path.exists(temp_file_path): os.remove(temp_file_path) except Exception as e: logger.warning(f"Failed to remove temporary file: {e}") except requests.exceptions.RequestException as e: logger.error(f"Error downloading plugin: {e}") return jsonify({'error': f'Failed to download plugin: {str(e)}'}), 500 except Exception as e: logger.error(f"Error downloading plugin to server: {e}") return jsonify({'error': str(e)}), 500 @bp.route('/api/servers//ports', methods=['POST']) def expose_port_api(server_id): try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Unauthorized'}), 401 data = request.get_json() internal_port = data.get('internalPort') protocol = data.get('protocol', 'TCP') if not internal_port: return jsonify({'error': 'Internal port required'}), 400 success = expose_port(server_id, user['id'], internal_port, protocol) if success: return jsonify({ 'success': True, 'message': f'Port {internal_port} exposed successfully' }), 200 else: return jsonify({'error': 'Failed to expose port'}), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/api/servers//ports/', methods=['DELETE']) def remove_port_api(server_id, internal_port): try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Unauthorized'}), 401 data = request.get_json() or {} protocol = data.get('protocol', 'TCP') success = remove_port(server_id, user['id'], internal_port, protocol) if success: return jsonify({ 'success': True, 'message': f'Port {internal_port}/{protocol} removed successfully' }), 200 else: return jsonify({'error': 'Failed to remove port'}), 500 except Exception as e: return jsonify({'error': str(e)}), 500 @bp.route('/api/servers//adjust-parameters', methods=['POST']) def adjust_server_parameters_api(server_id): """API endpoint to adjust server parameters (upgrade/downgrade)""" try: token = get_bearer_token() user = authenticate_user(token) if not user: return jsonify({'error': 'Unauthorized'}), 401 data = request.get_json() if not data: return jsonify({'error': 'Request body required'}), 400 new_cpu = data.get('cpu') new_ram = data.get('ram') new_storage = data.get('storage') if not new_cpu or not new_ram or not new_storage: return jsonify({'error': 'cpu, ram, and storage are required'}), 400 result = adjust_server_parameters(server_id, user['id'], new_cpu, new_ram, new_storage) return jsonify(result), 200 except Exception as e: return jsonify({'error': str(e)}), 500