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
+948
@@ -0,0 +1,948 @@
|
||||
"""Admin API routes for AnchorHost."""
|
||||
|
||||
|
||||
import bcrypt
|
||||
from database_service import database_connection, dict_from_row
|
||||
from domain_runtime import domain_manager
|
||||
from flask import Blueprint, jsonify, request
|
||||
import json
|
||||
from kubernetes import client
|
||||
from kubernetes.client.rest import ApiException
|
||||
from kubernetes_service import custom_objects_api, k8s_connected, k8s_manager, v1
|
||||
from runtime_config import (
|
||||
DATABASE_URL,
|
||||
logger,
|
||||
)
|
||||
from server_billing import charge_monthly_fees, get_user_credit_history, get_user_monthly_fees
|
||||
from server_capacity import parse_resource_to_mebibytes, parse_resource_to_millicores, parse_storage_to_gb
|
||||
from server_lifecycle import delete_server, restart_server, start_server, stop_server
|
||||
from server_records import get_server_by_id, get_user_servers
|
||||
from showcase_service import _is_valid_http_url, _normalize_showcase_url, get_homepage_showcase_links, save_homepage_showcase_links
|
||||
from user_service import authenticate_user, get_bearer_token, get_panel_settings, get_pricing, require_admin, set_site_setting
|
||||
import uuid
|
||||
|
||||
__all__ = ['admin_ban_user', 'admin_delete_server', 'admin_delete_user', 'admin_disable_user_2fa', 'admin_get_all_servers', 'admin_get_cluster_nodes', 'admin_get_stats', 'admin_get_user_billing_history', 'admin_get_user_credit_history', 'admin_get_user_servers', 'admin_get_users', 'admin_homepage_showcase', 'admin_panel_settings', 'admin_pricing', 'admin_reset_user_password', 'admin_restart_server', 'admin_start_server', 'admin_stop_server', 'admin_unban_user', 'admin_update_credits', 'create_announcement', 'debug_domains', 'debug_k8s', 'debug_servers', 'debug_users', 'delete_announcement', 'list_namespaces', 'run_manual_billing', 'update_announcement']
|
||||
|
||||
|
||||
|
||||
bp = Blueprint("admin", __name__)
|
||||
|
||||
|
||||
@bp.route('/api/admin/billing/run-manual', methods=['POST'])
|
||||
def run_manual_billing():
|
||||
"""Run manual billing collection (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
result = charge_monthly_fees()
|
||||
|
||||
return jsonify(result), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running manual billing: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users', methods=['GET'])
|
||||
def admin_get_users():
|
||||
"""Get all users (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get all users
|
||||
cursor.execute('''
|
||||
SELECT id, username, email, role, credits, created_at, email_verified, last_login, login_count
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
''')
|
||||
|
||||
users = []
|
||||
for row in cursor.fetchall():
|
||||
user_dict = dict_from_row(row)
|
||||
users.append({
|
||||
'id': user_dict['id'],
|
||||
'username': user_dict['username'],
|
||||
'email': user_dict['email'],
|
||||
'role': user_dict['role'],
|
||||
'credits': user_dict['credits'],
|
||||
'created_at': user_dict['created_at'],
|
||||
'email_verified': bool(user_dict['email_verified']),
|
||||
'last_login': user_dict['last_login'],
|
||||
'login_count': user_dict['login_count']
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'users': users}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting users: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/ban', methods=['POST'])
|
||||
def admin_ban_user(user_id):
|
||||
"""Ban a user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
if user['id'] == user_id:
|
||||
return jsonify({'success': False, 'error': 'Cannot ban yourself'}), 400
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET role = ? WHERE id = ?', ('banned', user_id))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"User {user_id} banned by admin {user['username']}")
|
||||
return jsonify({'success': True, 'message': 'User banned'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error banning user: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/unban', methods=['POST'])
|
||||
def admin_unban_user(user_id):
|
||||
"""Unban a user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET role = ? WHERE id = ?', ('user', user_id))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"User {user_id} unbanned by admin {user['username']}")
|
||||
return jsonify({'success': True, 'message': 'User unbanned'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error unbanning user: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/credits', methods=['POST'])
|
||||
def admin_update_credits(user_id):
|
||||
"""Update user credits (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
data = request.get_json()
|
||||
amount = data.get('amount')
|
||||
operation = data.get('operation', 'add') # add, set, subtract
|
||||
|
||||
if amount is None:
|
||||
return jsonify({'success': False, 'error': 'Amount required'}), 400
|
||||
|
||||
try:
|
||||
amount = int(amount)
|
||||
except ValueError:
|
||||
return jsonify({'success': False, 'error': 'Amount must be an integer'}), 400
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT credits, username FROM users WHERE id = ?', (user_id,))
|
||||
current_user = dict_from_row(cursor.fetchone())
|
||||
|
||||
if not current_user:
|
||||
return jsonify({'success': False, 'error': 'User not found'}), 404
|
||||
|
||||
current_credits = current_user['credits']
|
||||
new_credits = current_credits
|
||||
|
||||
if operation == 'add':
|
||||
new_credits += amount
|
||||
elif operation == 'subtract':
|
||||
new_credits -= amount
|
||||
elif operation == 'set':
|
||||
new_credits = amount
|
||||
|
||||
cursor.execute('UPDATE users SET credits = ? WHERE id = ?', (new_credits, user_id))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"Credits updated for user {current_user['username']} by admin {user['username']}: {current_credits} -> {new_credits}")
|
||||
return jsonify({'success': True, 'message': 'Credits updated', 'new_credits': new_credits}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating credits: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/servers', methods=['GET'])
|
||||
def admin_get_user_servers(user_id):
|
||||
"""Get servers for a specific user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
servers = get_user_servers(user_id)
|
||||
return jsonify({'success': True, 'servers': servers}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user servers: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/credits/history', methods=['GET'])
|
||||
def admin_get_user_credit_history(user_id):
|
||||
"""Get credit transaction history for a specific user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
history = get_user_credit_history(user_id, limit=50) # Use existing function
|
||||
return jsonify({'success': True, 'history': history}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user credit history: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/billing/history', methods=['GET'])
|
||||
def admin_get_user_billing_history(user_id):
|
||||
"""Get billing history for a specific user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
history = get_user_monthly_fees(user_id, limit=50) # Use existing function
|
||||
return jsonify({'success': True, 'history': history}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user billing history: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/servers/<server_id>/start', methods=['POST'])
|
||||
def admin_start_server(server_id):
|
||||
"""Start a server (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
# Get server to find owner
|
||||
server = get_server_by_id(server_id)
|
||||
if not server:
|
||||
return jsonify({'success': False, 'error': 'Server not found'}), 404
|
||||
|
||||
if start_server(server_id, server['owner_id']):
|
||||
return jsonify({'success': True, 'message': 'Server started'}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to start server'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/servers/<server_id>/stop', methods=['POST'])
|
||||
def admin_stop_server(server_id):
|
||||
"""Stop a server (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
# Get server to find owner
|
||||
server = get_server_by_id(server_id)
|
||||
if not server:
|
||||
return jsonify({'success': False, 'error': 'Server not found'}), 404
|
||||
|
||||
if stop_server(server_id, server['owner_id']):
|
||||
return jsonify({'success': True, 'message': 'Server stopped'}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to stop server'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/servers/<server_id>/restart', methods=['POST'])
|
||||
def admin_restart_server(server_id):
|
||||
"""Restart a server (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
# Get server to find owner
|
||||
server = get_server_by_id(server_id)
|
||||
if not server:
|
||||
return jsonify({'success': False, 'error': 'Server not found'}), 404
|
||||
|
||||
if restart_server(server_id, server['owner_id']):
|
||||
return jsonify({'success': True, 'message': 'Server restarted'}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to restart server'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/servers/<server_id>', methods=['DELETE'])
|
||||
def admin_delete_server(server_id):
|
||||
"""Delete a server (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
# Get server to find owner
|
||||
server = get_server_by_id(server_id)
|
||||
if not server:
|
||||
return jsonify({'success': False, 'error': 'Server not found'}), 404
|
||||
|
||||
if delete_server(server_id, server['owner_id']):
|
||||
return jsonify({'success': True, 'message': 'Server deleted'}), 200
|
||||
return jsonify({'success': False, 'error': 'Failed to delete server'}), 500
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/servers', methods=['GET'])
|
||||
def admin_get_all_servers():
|
||||
"""Get all servers with owner details (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT s.id, s.name, s.cpu, s.ram, s.storage, s.image, s.type, s.version, s.loader, s.java_version,
|
||||
s.status, s.ports, s.created_at, u.username as owner, u.id as owner_id,
|
||||
s.sftp_username, s.sftp_password, s.sftp_port
|
||||
FROM servers s
|
||||
JOIN users u ON s.owner_id = u.id
|
||||
ORDER BY u.username, s.name
|
||||
''')
|
||||
servers = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
return jsonify({'success': True, 'servers': servers}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting all servers: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/stats', methods=['GET'])
|
||||
def admin_get_stats():
|
||||
"""Get platform stats (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as count FROM users')
|
||||
res = dict_from_row(cursor.fetchone())
|
||||
total_users = res['count'] if res else 0
|
||||
|
||||
cursor.execute('SELECT COUNT(*) as count FROM servers')
|
||||
res = dict_from_row(cursor.fetchone())
|
||||
total_servers = res['count'] if res else 0
|
||||
|
||||
cursor.execute("SELECT COUNT(*) as count FROM servers WHERE status = 'Running'")
|
||||
res = dict_from_row(cursor.fetchone())
|
||||
active_servers = res['count'] if res else 0
|
||||
|
||||
cursor.execute('SELECT SUM(credits) as total FROM users')
|
||||
res = dict_from_row(cursor.fetchone())
|
||||
total_credits = res['total'] if res and res['total'] is not None else 0
|
||||
|
||||
stats = {
|
||||
'total_users': total_users,
|
||||
'total_servers': total_servers,
|
||||
'active_servers': active_servers,
|
||||
'total_credits': total_credits
|
||||
}
|
||||
|
||||
return jsonify({'success': True, 'stats': stats}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting stats: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>', methods=['DELETE'])
|
||||
def admin_delete_user(user_id):
|
||||
"""Delete a user (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if user exists
|
||||
cursor.execute('SELECT username FROM users WHERE id = ?', (user_id,))
|
||||
target_user = cursor.fetchone()
|
||||
|
||||
if not target_user:
|
||||
return jsonify({'success': False, 'error': 'User not found'}), 404
|
||||
|
||||
# Delete user
|
||||
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"User {user_id} deleted by admin {user['username']}")
|
||||
return jsonify({'success': True}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting user: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/password', methods=['POST'])
|
||||
def admin_reset_user_password(user_id):
|
||||
"""Reset user password (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
data = request.json
|
||||
new_password = data.get('password')
|
||||
|
||||
if not new_password or len(new_password) < 6:
|
||||
return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400
|
||||
|
||||
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET password_hash = ?, password_changed_at = CURRENT_TIMESTAMP WHERE id = ?', (hashed_password, user_id))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"Password reset for user {user_id} by admin {user['username']}")
|
||||
return jsonify({'success': True}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resetting password: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/users/<user_id>/2fa/disable', methods=['POST'])
|
||||
def admin_disable_user_2fa(user_id):
|
||||
"""Disable user 2FA (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET two_factor_enabled = FALSE, two_factor_secret = NULL WHERE id = ?', (user_id,))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"2FA disabled for user {user_id} by admin {user['username']}")
|
||||
return jsonify({'success': True}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error disabling 2FA: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/cluster/nodes', methods=['GET'])
|
||||
def admin_get_cluster_nodes():
|
||||
"""Get detailed cluster node stats (admin only)"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
|
||||
if not user or user.get('role') != 'admin':
|
||||
return jsonify({'success': False, 'error': 'Admin access required'}), 403
|
||||
|
||||
if not k8s_connected or not v1:
|
||||
return jsonify({'success': False, 'error': 'Kubernetes not connected'}), 503
|
||||
|
||||
nodes = v1.list_node()
|
||||
node_stats = []
|
||||
|
||||
# Try to get metrics if available
|
||||
metrics = {}
|
||||
try:
|
||||
# Use global custom_objects_api if available, else try to instantiate
|
||||
api_instance = custom_objects_api if custom_objects_api else client.CustomObjectsApi()
|
||||
metrics_data = api_instance.list_cluster_custom_object(
|
||||
group="metrics.k8s.io",
|
||||
version="v1beta1",
|
||||
plural="nodes"
|
||||
)
|
||||
for item in metrics_data.get('items', []):
|
||||
metrics[item['metadata']['name']] = item['usage']
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not fetch node metrics: {e}")
|
||||
|
||||
# Get pod allocations for allocated stats
|
||||
all_pods = v1.list_pod_for_all_namespaces()
|
||||
pod_allocations = {}
|
||||
for pod in all_pods.items:
|
||||
if pod.spec.node_name and pod.status.phase in ['Running', 'Pending']:
|
||||
node_name = pod.spec.node_name
|
||||
if node_name not in pod_allocations:
|
||||
pod_allocations[node_name] = {'cpu': 0, 'memory': 0, 'storage': 0.0}
|
||||
|
||||
for container in pod.spec.containers:
|
||||
if container.resources and container.resources.requests:
|
||||
if 'cpu' in container.resources.requests:
|
||||
pod_allocations[node_name]['cpu'] += parse_resource_to_millicores(container.resources.requests['cpu'])
|
||||
if 'memory' in container.resources.requests:
|
||||
pod_allocations[node_name]['memory'] += parse_resource_to_mebibytes(container.resources.requests['memory'])
|
||||
if 'ephemeral-storage' in container.resources.requests:
|
||||
pod_allocations[node_name]['storage'] += parse_storage_to_gb(container.resources.requests['ephemeral-storage'])
|
||||
|
||||
for node in nodes.items:
|
||||
name = node.metadata.name
|
||||
|
||||
# Specs
|
||||
allocatable = node.status.allocatable or {}
|
||||
capacity = node.status.capacity or {}
|
||||
node_info = node.status.node_info
|
||||
|
||||
cpu_allocatable = parse_resource_to_millicores(allocatable.get('cpu', '0'))
|
||||
memory_allocatable = parse_resource_to_mebibytes(allocatable.get('memory', '0'))
|
||||
storage_capacity = parse_storage_to_gb(capacity.get('ephemeral-storage', '0Gi'))
|
||||
storage_allocatable = parse_storage_to_gb(allocatable.get('ephemeral-storage', capacity.get('ephemeral-storage', '0Gi')))
|
||||
|
||||
# Roles
|
||||
labels = node.metadata.labels or {}
|
||||
roles = []
|
||||
if 'node-role.kubernetes.io/control-plane' in labels or 'node-role.kubernetes.io/master' in labels:
|
||||
roles.append('Control Plane')
|
||||
else:
|
||||
roles.append('Worker')
|
||||
|
||||
# OS Info
|
||||
os_image = node_info.os_image
|
||||
kernel_version = node_info.kernel_version
|
||||
|
||||
# Extract CPU Model (if available from labels)
|
||||
cpu_model = "Unknown CPU"
|
||||
if 'cpu-model.node.kubevirt.io/Haswell-noTSX' in labels:
|
||||
cpu_model = "Intel Haswell"
|
||||
elif 'cpu-model.node.kubevirt.io/Nehalem' in labels:
|
||||
cpu_model = "Intel Nehalem"
|
||||
elif any(k.startswith('cpu-model.node.kubevirt.io/') for k in labels):
|
||||
cpu_model = [k.split('/')[-1] for k in labels if k.startswith('cpu-model.node.kubevirt.io/')][0]
|
||||
|
||||
# Fallback if vendor is known
|
||||
if cpu_model == "Unknown CPU":
|
||||
if 'cpu-vendor.node.kubevirt.io/Intel' in labels:
|
||||
cpu_model = "Intel CPU"
|
||||
elif 'cpu-vendor.node.kubevirt.io/AMD' in labels:
|
||||
cpu_model = "AMD CPU"
|
||||
|
||||
# Allocated
|
||||
allocated = pod_allocations.get(name, {'cpu': 0, 'memory': 0, 'storage': 0.0})
|
||||
storage_allocated = allocated.get('storage', 0.0)
|
||||
disk_alloc_percent = round((storage_allocated / storage_allocatable * 100), 1) if storage_allocatable > 0 else 0.0
|
||||
|
||||
# Usage (from metrics if available)
|
||||
usage = {'cpu': 0, 'memory': 0}
|
||||
if name in metrics:
|
||||
usage['cpu'] = parse_resource_to_millicores(metrics[name].get('cpu', '0'))
|
||||
usage['memory'] = parse_resource_to_mebibytes(metrics[name].get('memory', '0'))
|
||||
|
||||
node_stats.append({
|
||||
'name': name,
|
||||
'status': 'Ready' if any(c.type == 'Ready' and c.status == 'True' for c in node.status.conditions) else 'NotReady',
|
||||
'role': roles[0],
|
||||
'os': {
|
||||
'name': node_info.operating_system,
|
||||
'image': os_image,
|
||||
'kernel': kernel_version,
|
||||
'arch': node_info.architecture,
|
||||
'icon': 'linux' if node_info.operating_system.lower() == 'linux' else 'windows'
|
||||
},
|
||||
'specs': {
|
||||
'cpu': f"{int(cpu_allocatable/1000)} Cores",
|
||||
'cpu_model': cpu_model,
|
||||
'ram': f"{memory_allocatable/1024:.1f} GB",
|
||||
'disk': f"{storage_capacity:.1f} GB"
|
||||
},
|
||||
'usage': {
|
||||
'cpu': f"{usage['cpu']/1000:.2f} Cores",
|
||||
'cpu_display': f"{usage['cpu']/1000:.2f} Cores" if usage['cpu'] > 0 else "0.00 Cores",
|
||||
'cpu_percent': round(usage['cpu'] / cpu_allocatable * 100, 1) if cpu_allocatable > 0 else 0,
|
||||
'ram': f"{usage['memory']/1024:.2f} GB",
|
||||
'ram_display': f"{usage['memory']/1024:.2f} GB" if usage['memory'] > 0 else "0.00 GB",
|
||||
'ram_percent': round(usage['memory'] / memory_allocatable * 100, 1) if memory_allocatable > 0 else 0
|
||||
},
|
||||
'allocated': {
|
||||
'cpu': f"{allocated['cpu']/1000:.2f} Cores",
|
||||
'cpu_display': f"{allocated['cpu']/1000:.2f} Cores",
|
||||
'cpu_percent': round(allocated['cpu'] / cpu_allocatable * 100, 1) if cpu_allocatable > 0 else 0,
|
||||
'ram': f"{allocated['memory']/1024:.2f} GB",
|
||||
'ram_display': f"{allocated['memory']/1024:.2f} GB",
|
||||
'ram_percent': round(allocated['memory'] / memory_allocatable * 100, 1) if memory_allocatable > 0 else 0,
|
||||
'disk': f"{storage_allocated:.2f} GB",
|
||||
'disk_display': f"{storage_allocated:.2f} GB / {storage_capacity:.1f} GB",
|
||||
'disk_percent': disk_alloc_percent
|
||||
}
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'nodes': node_stats}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting cluster nodes: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/debug/users', methods=['GET'])
|
||||
def debug_users():
|
||||
if not require_admin():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, username, email, role, credits, created_at FROM users')
|
||||
users_list = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
return jsonify({
|
||||
'users': users_list,
|
||||
'count': len(users_list),
|
||||
'kubernetes': {
|
||||
'connected': k8s_manager.connected,
|
||||
'lastError': str(k8s_manager.last_error) if k8s_manager.last_error else None
|
||||
}
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/debug/servers', methods=['GET'])
|
||||
def debug_servers():
|
||||
if not require_admin():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT id, name, type, status, cpu, ram, storage, ip, ports,
|
||||
owner_id, namespace, k8s_name, is_demo, demo_expires_at,
|
||||
created_at
|
||||
FROM servers
|
||||
''')
|
||||
servers_list = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
for server in servers_list:
|
||||
if server and 'ports' in server:
|
||||
server['ports'] = json.loads(server['ports'])
|
||||
|
||||
return jsonify({
|
||||
'servers': servers_list,
|
||||
'count': len(servers_list)
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/debug/domains', methods=['GET'])
|
||||
def debug_domains():
|
||||
if not require_admin():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM user_domains')
|
||||
domains_list = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
return jsonify({
|
||||
'domains': domains_list,
|
||||
'count': len(domains_list),
|
||||
'domain_manager': 'available' if domain_manager else 'unavailable'
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@bp.route('/debug/k8s', methods=['GET'])
|
||||
def debug_k8s():
|
||||
if not require_admin():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
return jsonify({
|
||||
'connected': k8s_manager.connected,
|
||||
'lastError': str(k8s_manager.last_error) if k8s_manager.last_error else None
|
||||
}), 200
|
||||
|
||||
@bp.route('/namespaces', methods=['GET'])
|
||||
def list_namespaces():
|
||||
if not require_admin():
|
||||
return jsonify({'error': 'Unauthorized'}), 403
|
||||
try:
|
||||
if k8s_manager.connected:
|
||||
namespaces = v1.list_namespace()
|
||||
namespace_list = [ns.metadata.name for ns in namespaces.items]
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'namespaces': namespace_list,
|
||||
'count': len(namespace_list)
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'namespaces': ['user-testuser'],
|
||||
'count': 1,
|
||||
'message': 'Kubernetes not connected'
|
||||
}), 200
|
||||
except ApiException as e:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'Failed to list namespaces: {e.reason}',
|
||||
'status': e.status
|
||||
}), e.status
|
||||
|
||||
@bp.route('/api/admin/announcements', methods=['POST'])
|
||||
def create_announcement():
|
||||
"""Create a new announcement (Admin only)"""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.json
|
||||
if not data or not data.get('title') or not data.get('content'):
|
||||
return jsonify({'success': False, 'error': 'Title and content are required'}), 400
|
||||
|
||||
title = data['title']
|
||||
content = data['content']
|
||||
tags = data.get('tags', [])
|
||||
|
||||
# Ensure tags is a list
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
|
||||
# Serialize tags to JSON for storage
|
||||
tags_json = json.dumps(tags)
|
||||
|
||||
announcement_id = f"ann-{str(uuid.uuid4())[:8]}"
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
INSERT INTO announcements (id, title, content, tags, author_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (announcement_id, title, content, tags_json, user['id']))
|
||||
conn.commit()
|
||||
|
||||
# Fetch the created announcement to return it
|
||||
cursor.execute('''
|
||||
SELECT a.*, u.username as author_name
|
||||
FROM announcements a
|
||||
JOIN users u ON a.author_id = u.id
|
||||
WHERE a.id = ?
|
||||
''', (announcement_id,))
|
||||
|
||||
new_announcement = dict_from_row(cursor.fetchone())
|
||||
if new_announcement and new_announcement.get('tags'):
|
||||
try:
|
||||
new_announcement['tags'] = json.loads(new_announcement['tags'])
|
||||
except:
|
||||
new_announcement['tags'] = []
|
||||
|
||||
return jsonify({'success': True, 'announcement': new_announcement})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating announcement: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/announcements/<announcement_id>', methods=['PUT'])
|
||||
def update_announcement(announcement_id):
|
||||
"""Update an announcement (Admin only)"""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
data = request.json
|
||||
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if 'title' in data:
|
||||
updates.append("title = ?")
|
||||
params.append(data['title'])
|
||||
|
||||
if 'content' in data:
|
||||
updates.append("content = ?")
|
||||
params.append(data['content'])
|
||||
|
||||
if 'tags' in data:
|
||||
tags = data['tags']
|
||||
if isinstance(tags, str):
|
||||
tags = [tags]
|
||||
updates.append("tags = ?")
|
||||
params.append(json.dumps(tags))
|
||||
|
||||
if not updates:
|
||||
return jsonify({'success': False, 'error': 'No fields to update'}), 400
|
||||
|
||||
updates.append("updated_at = CURRENT_TIMESTAMP")
|
||||
params.append(announcement_id)
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(f'''
|
||||
UPDATE announcements
|
||||
SET {', '.join(updates)}
|
||||
WHERE id = ?
|
||||
''', params)
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
return jsonify({'success': False, 'error': 'Announcement not found'}), 404
|
||||
|
||||
return jsonify({'success': True, 'message': 'Announcement updated successfully'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating announcement: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/announcements/<announcement_id>', methods=['DELETE'])
|
||||
def delete_announcement(announcement_id):
|
||||
"""Delete an announcement (Admin only)"""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM announcements WHERE id = ?', (announcement_id,))
|
||||
conn.commit()
|
||||
|
||||
if cursor.rowcount == 0:
|
||||
return jsonify({'success': False, 'error': 'Announcement not found'}), 404
|
||||
|
||||
return jsonify({'success': True, 'message': 'Announcement deleted successfully'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting announcement: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/homepage-showcase', methods=['GET', 'PUT'])
|
||||
def admin_homepage_showcase():
|
||||
"""Read or update homepage showcase links (admin only)."""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
if request.method == 'GET':
|
||||
return jsonify({'success': True, 'links': get_homepage_showcase_links()})
|
||||
|
||||
data = request.json or {}
|
||||
raw_links = data.get('links')
|
||||
if not isinstance(raw_links, list):
|
||||
return jsonify({'success': False, 'error': 'links must be an array'}), 400
|
||||
|
||||
normalized = []
|
||||
for i in range(3):
|
||||
entry = raw_links[i] if i < len(raw_links) and isinstance(raw_links[i], dict) else {}
|
||||
label = (entry.get('label') or f'Site {i + 1}').strip()[:160]
|
||||
url = _normalize_showcase_url(entry.get('url', ''))
|
||||
if not _is_valid_http_url(url):
|
||||
return jsonify({'success': False, 'error': f'Invalid URL for slot {i + 1}'}), 400
|
||||
normalized.append({"label": label, "url": url})
|
||||
|
||||
save_homepage_showcase_links(normalized)
|
||||
return jsonify({'success': True, 'links': normalized})
|
||||
except Exception as e:
|
||||
logger.error(f"admin_homepage_showcase: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/pricing', methods=['GET', 'POST'])
|
||||
def admin_pricing():
|
||||
"""Read or update service pricing (admin only)."""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
if request.method == 'GET':
|
||||
return jsonify({'success': True, 'pricing': get_pricing()})
|
||||
|
||||
data = request.json or {}
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
for key, value in data.items():
|
||||
if key.startswith('price_') or key.startswith('multiplier_'):
|
||||
# Skip None values (JSON NaN becomes None)
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
if 'postgres' in DATABASE_URL:
|
||||
cursor.execute(
|
||||
"INSERT INTO site_settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = CURRENT_TIMESTAMP",
|
||||
(key, str(value))
|
||||
)
|
||||
else:
|
||||
# Check if key exists
|
||||
cursor.execute("SELECT 1 FROM site_settings WHERE key = ?", (key,))
|
||||
if cursor.fetchone():
|
||||
cursor.execute(
|
||||
"UPDATE site_settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?",
|
||||
(str(value), key)
|
||||
)
|
||||
else:
|
||||
cursor.execute(
|
||||
"INSERT INTO site_settings (key, value) VALUES (?, ?)",
|
||||
(key, str(value))
|
||||
)
|
||||
conn.commit()
|
||||
return jsonify({'success': True, 'pricing': get_pricing()})
|
||||
except Exception as e:
|
||||
logger.error(f"admin_pricing: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/admin/settings', methods=['GET', 'POST', 'PUT'])
|
||||
@bp.route('/api/admin/panel-settings', methods=['GET', 'POST', 'PUT'])
|
||||
def admin_panel_settings():
|
||||
"""Read or update panel settings (admin only)."""
|
||||
try:
|
||||
user = require_admin()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 403
|
||||
|
||||
if request.method == 'GET':
|
||||
return jsonify({'success': True, 'settings': get_panel_settings()})
|
||||
|
||||
data = request.json or {}
|
||||
if 'demo_servers_enabled' in data:
|
||||
demo_val = 'true' if data['demo_servers_enabled'] in (True, 'true', '1', 1) else 'false'
|
||||
set_site_setting('demo_servers_enabled', demo_val)
|
||||
|
||||
if 'new_user_credits' in data:
|
||||
try:
|
||||
credits_val = max(0, int(float(data['new_user_credits'])))
|
||||
set_site_setting('new_user_credits', str(credits_val))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
return jsonify({'success': True, 'settings': get_panel_settings()})
|
||||
except Exception as e:
|
||||
logger.error(f"admin_panel_settings: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
Reference in new issue
Block a user