Files
ServerKit/backend/app/api/system.py
T
Juan Denis c7444a88f5 Add resource tier enforcement and UI for WordPress creation
Introduces backend logic to determine server resource tier and block WordPress site creation on insufficient resources (lite tier). Adds ResourceTierContext and ResourceGate component to the frontend, displaying a UI warning and disabling creation actions when requirements are not met. Updates WordPress page and styles to integrate the new resource gating mechanism.
2026-01-25 20:01:07 -05:00

329 lines
9.6 KiB
Python

import os
import time
import urllib.request
import json
from flask import Blueprint, jsonify, request, current_app
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.models import User
from app.services.system_service import SystemService
from app.services.resource_tier_service import ResourceTierService
# Cache for update check (to avoid hitting GitHub API too often)
_update_cache = {
'last_check': 0,
'result': None,
'ttl': 3600 # 1 hour cache
}
system_bp = Blueprint('system', __name__)
@system_bp.route('/version', methods=['GET'])
def get_version():
"""Get ServerKit version - no auth required."""
version = '1.0.0' # Default version
# Try to read from VERSION file
version_paths = [
'/opt/serverkit/VERSION',
os.path.join(current_app.root_path, '..', 'VERSION'),
os.path.join(current_app.root_path, '..', '..', 'VERSION'),
]
for path in version_paths:
try:
if os.path.exists(path):
with open(path, 'r') as f:
version = f.read().strip()
break
except Exception:
pass
return jsonify({
'version': version,
'name': 'ServerKit'
}), 200
@system_bp.route('/check-update', methods=['GET'])
def check_update():
"""Check for updates from GitHub releases - no auth required."""
global _update_cache
# Return cached result if still valid
if _update_cache['result'] and (time.time() - _update_cache['last_check']) < _update_cache['ttl']:
return jsonify(_update_cache['result']), 200
# Get current version
current_version = '1.0.0'
version_paths = [
'/opt/serverkit/VERSION',
os.path.join(current_app.root_path, '..', 'VERSION'),
os.path.join(current_app.root_path, '..', '..', 'VERSION'),
]
for path in version_paths:
try:
if os.path.exists(path):
with open(path, 'r') as f:
current_version = f.read().strip()
break
except Exception:
pass
result = {
'current_version': current_version,
'latest_version': None,
'update_available': False,
'release_url': None,
'release_notes': None,
'error': None
}
try:
# Fetch latest release from GitHub API
req = urllib.request.Request(
'https://api.github.com/repos/jhd3197/ServerKit/releases/latest',
headers={'User-Agent': 'ServerKit-UpdateChecker'}
)
with urllib.request.urlopen(req, timeout=10) as response:
data = json.loads(response.read().decode('utf-8'))
latest_version = data.get('tag_name', '').lstrip('v')
result['latest_version'] = latest_version
result['release_url'] = data.get('html_url')
result['release_notes'] = data.get('body', '')[:500] # Truncate release notes
# Simple version comparison (works for semver-like versions)
def parse_version(v):
try:
return tuple(int(x) for x in v.split('.'))
except:
return (0, 0, 0)
if parse_version(latest_version) > parse_version(current_version):
result['update_available'] = True
except urllib.error.HTTPError as e:
if e.code == 404:
result['error'] = 'No releases found'
else:
result['error'] = f'GitHub API error: {e.code}'
except Exception as e:
result['error'] = f'Failed to check for updates: {str(e)}'
# Cache the result
_update_cache['last_check'] = time.time()
_update_cache['result'] = result
return jsonify(result), 200
@system_bp.route('/metrics', methods=['GET'])
@jwt_required()
def get_metrics():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
metrics = SystemService.get_all_metrics()
return jsonify(metrics), 200
@system_bp.route('/info', methods=['GET'])
@jwt_required()
def get_system_info():
"""Get system information (hostname, IP, kernel, CPU info)."""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify(SystemService.get_system_info()), 200
@system_bp.route('/cpu', methods=['GET'])
@jwt_required()
def get_cpu_metrics():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify(SystemService.get_cpu_metrics()), 200
@system_bp.route('/memory', methods=['GET'])
@jwt_required()
def get_memory_metrics():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify(SystemService.get_memory_metrics()), 200
@system_bp.route('/disk', methods=['GET'])
@jwt_required()
def get_disk_metrics():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify(SystemService.get_disk_metrics()), 200
@system_bp.route('/network', methods=['GET'])
@jwt_required()
def get_network_metrics():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
return jsonify(SystemService.get_network_metrics()), 200
@system_bp.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint - no auth required"""
return jsonify({
'status': 'healthy',
'service': 'serverkit-api'
}), 200
@system_bp.route('/resource-tier', methods=['GET'])
@jwt_required()
def get_resource_tier():
"""
Get server resource tier information.
Returns tier, specs, and feature permissions based on server resources.
Results are cached for 1 hour unless refresh=true is passed.
"""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
force_refresh = request.args.get('refresh', 'false').lower() == 'true'
tier_info = ResourceTierService.get_tier_info(force_refresh=force_refresh)
return jsonify(tier_info), 200
@system_bp.route('/time', methods=['GET'])
@jwt_required()
def get_server_time():
"""Get current server time and timezone."""
return jsonify(SystemService.get_server_time()), 200
@system_bp.route('/timezones', methods=['GET'])
@jwt_required()
def get_timezones():
"""Get list of available timezones."""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
timezones = SystemService.get_available_timezones()
return jsonify({'timezones': timezones}), 200
@system_bp.route('/timezone', methods=['PUT'])
@jwt_required()
def set_timezone():
"""Set server timezone (admin only)."""
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
data = request.get_json()
if not data or 'timezone' not in data:
return jsonify({'error': 'Timezone is required'}), 400
result = SystemService.set_timezone(data['timezone'])
if result['success']:
return jsonify(result), 200
else:
return jsonify(result), 400
@system_bp.route('/processes', methods=['GET'])
@jwt_required()
def get_processes():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
try:
pinfo = proc.info
if pinfo['cpu_percent'] > 0 or pinfo['memory_percent'] > 0.1:
processes.append({
'pid': pinfo['pid'],
'name': pinfo['name'],
'cpu_percent': pinfo['cpu_percent'],
'memory_percent': round(pinfo['memory_percent'], 2),
'status': pinfo['status']
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
# Sort by CPU usage
processes.sort(key=lambda x: x['cpu_percent'], reverse=True)
return jsonify({
'processes': processes[:50] # Top 50 processes
}), 200
@system_bp.route('/services', methods=['GET'])
@jwt_required()
def get_services():
current_user_id = get_jwt_identity()
user = User.query.get(current_user_id)
if user.role != 'admin':
return jsonify({'error': 'Admin access required'}), 403
# Common services to check
service_names = ['nginx', 'mysql', 'postgresql', 'redis', 'docker', 'php-fpm']
services = []
for name in service_names:
running = False
for proc in psutil.process_iter(['name']):
try:
if name.lower() in proc.info['name'].lower():
running = True
break
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
services.append({
'name': name,
'status': 'running' if running else 'stopped'
})
return jsonify({'services': services}), 200