provisioning/core/nulib/api/server.nu
2025-09-22 23:11:41 +01:00

446 lines
15 KiB
Plaintext

#!/usr/bin/env nu
# API Server for Provisioning System
# Provides HTTP REST API endpoints for infrastructure queries and management
use ../lib_provisioning/utils/settings.nu *
use ../main_provisioning/query.nu *
use ../lib_provisioning/ai/lib.nu *
export def start_api_server [
--port: int = 8080
--host: string = "localhost"
--enable-websocket
--enable-cors
--debug
]: nothing -> nothing {
print $"🚀 Starting Provisioning API Server on ($host):($port)"
if $debug {
$env.PROVISIONING_API_DEBUG = "true"
print "Debug mode enabled"
}
# Check if port is available
let port_check = (check_port_available $port)
if not $port_check {
error make {
msg: $"Port ($port) is already in use"
help: "Try a different port with --port flag"
}
}
# Setup server configuration
let server_config = {
host: $host
port: $port
enable_websocket: $enable_websocket
enable_cors: $enable_cors
debug: $debug
routes: (get_api_routes)
}
print $"📡 Server configuration: ($server_config | to json)"
print "Available endpoints:"
print " GET /api/v1/health - Health check"
print " GET /api/v1/query - Infrastructure queries"
print " POST /api/v1/query - Complex queries with body"
print " GET /api/v1/metrics - System metrics"
print " GET /api/v1/logs - System logs"
print " GET /api/v1/dashboard - Dashboard data"
if $enable_websocket {
print " WS /ws/stream - WebSocket real-time updates"
}
# Start HTTP server
start_http_server $server_config
}
def check_port_available [port: int]: nothing -> bool {
# Try to bind to the port to check if it's available
let result = (do -i {
http listen $port --host "127.0.0.1" --timeout 1 | ignore
})
match $result {
null => false, # Port is busy
_ => true # Port is available
}
}
def get_api_routes []: nothing -> list {
[
{ method: "GET", path: "/api/v1/health", handler: "handle_health" }
{ method: "GET", path: "/api/v1/query", handler: "handle_query_get" }
{ method: "POST", path: "/api/v1/query", handler: "handle_query_post" }
{ method: "GET", path: "/api/v1/metrics", handler: "handle_metrics" }
{ method: "GET", path: "/api/v1/logs", handler: "handle_logs" }
{ method: "GET", path: "/api/v1/dashboard", handler: "handle_dashboard" }
{ method: "GET", path: "/api/v1/servers", handler: "handle_servers" }
{ method: "GET", path: "/api/v1/servers/{id}/status", handler: "handle_server_status" }
]
}
def start_http_server [config: record]: nothing -> nothing {
print $"🌐 Starting HTTP server on ($config.host):($config.port)..."
# Use a Python-based HTTP server for better compatibility
let server_script = create_python_server $config
# Save server script to temporary file
let temp_server = $"/tmp/provisioning_api_server.py"
$server_script | save --force $temp_server
print $"📝 Server script saved to: ($temp_server)"
print "🎯 Starting server... (Press Ctrl+C to stop)"
# Start the Python server
python3 $temp_server
}
def create_python_server [config: record]: nothing -> str {
let cors_headers = if $config.enable_cors {
'''
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
'''
} else { "" }
let websocket_import = if $config.enable_websocket {
"import websockets"
} else { "" }
$"#!/usr/bin/env python3
import http.server
import socketserver
import json
import subprocess
import urllib.parse
import os
from pathlib import Path
($websocket_import)
class ProvisioningAPIHandler(http.server.BaseHTTPRequestHandler):
def do_OPTIONS(self):
self.send_response(200)
($cors_headers)
self.end_headers()
def do_GET(self):
self.handle_request('GET')
def do_POST(self):
self.handle_request('POST')
def handle_request(self, method):
try:
path_parts = urllib.parse.urlparse(self.path)
path = path_parts.path
query_params = urllib.parse.parse_qs(path_parts.query)
# Route handling
if path == '/api/v1/health':
self.handle_health()
elif path == '/api/v1/query':
if method == 'GET':
self.handle_query_get(query_params)
else:
self.handle_query_post()
elif path == '/api/v1/metrics':
self.handle_metrics(query_params)
elif path == '/api/v1/logs':
self.handle_logs(query_params)
elif path == '/api/v1/dashboard':
self.handle_dashboard(query_params)
elif path == '/api/v1/servers':
self.handle_servers(query_params)
elif path.startswith('/api/v1/servers/') and path.endswith('/status'):
server_id = path.split('/')[-2]
self.handle_server_status(server_id, query_params)
else:
self.send_error(404, 'Not Found')
except Exception as e:
self.send_error(500, f'Internal Server Error: {{str(e)}}')
def handle_health(self):
response = {{
'status': 'healthy',
'service': 'provisioning-api',
'version': '1.0.0',
'timestamp': self.get_timestamp()
}}
self.send_json_response(response)
def handle_query_get(self, params):
# Convert query parameters to nushell command
target = params.get('target', ['servers'])[0]
infra = params.get('infra', [None])[0]
find = params.get('find', [None])[0]
cols = params.get('cols', [None])[0]
out_format = params.get('format', ['json'])[0]
cmd_args = ['nu', '-c', f'use ($env.PROVISIONING_PATH)/core/nulib/main_provisioning/query.nu; main query {{target}} --out {{out_format}}']
if infra:
cmd_args[-1] = cmd_args[-1].replace('{{target}}', f'{{target}} --infra {{infra}}')
result = self.run_provisioning_command(cmd_args)
self.send_json_response(result)
def handle_query_post(self):
content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
post_data = self.rfile.read(content_length)
try:
query_data = json.loads(post_data.decode('utf-8'))
# Process complex query
result = self.process_complex_query(query_data)
self.send_json_response(result)
except json.JSONDecodeError:
self.send_error(400, 'Invalid JSON')
else:
self.send_error(400, 'No data provided')
def handle_metrics(self, params):
timerange = params.get('timerange', ['1h'])[0]
metric_type = params.get('type', ['all'])[0]
# Mock metrics data - replace with actual metrics collection
metrics = {{
'cpu_usage': {{
'current': 45.2,
'average': 38.7,
'max': 89.1,
'unit': 'percentage'
}},
'memory_usage': {{
'current': 2.3,
'total': 8.0,
'unit': 'GB'
}},
'disk_usage': {{
'used': 120.5,
'total': 500.0,
'unit': 'GB'
}},
'network_io': {{
'in': 1024,
'out': 2048,
'unit': 'MB/s'
}},
'timestamp': self.get_timestamp(),
'timerange': timerange
}}
self.send_json_response(metrics)
def handle_logs(self, params):
level = params.get('level', ['info'])[0]
limit = int(params.get('limit', ['100'])[0])
since = params.get('since', ['1h'])[0]
# Mock log data - replace with actual log collection
logs = {{
'entries': [
{{
'timestamp': '2024-01-16T10:30:00Z',
'level': 'info',
'service': 'provisioning-core',
'message': 'Server created successfully: web-01'
}},
{{
'timestamp': '2024-01-16T10:29:45Z',
'level': 'debug',
'service': 'aws-provider',
'message': 'EC2 instance launched: i-1234567890abcdef0'
}}
],
'total': 2,
'filters': {{
'level': level,
'limit': limit,
'since': since
}}
}}
self.send_json_response(logs)
def handle_dashboard(self, params):
view = params.get('view', ['overview'])[0]
dashboard_data = {{
'overview': {{
'total_servers': 25,
'active_servers': 23,
'failed_servers': 2,
'total_cost_monthly': 3250.75,
'cost_trend': '+5.2%',
'uptime': 99.7
}},
'recent_activities': [
{{
'type': 'deployment',
'message': 'Deployed application to production',
'timestamp': '2024-01-16T10:30:00Z',
'status': 'success'
}},
{{
'type': 'scaling',
'message': 'Auto-scaled web servers: 3 → 5',
'timestamp': '2024-01-16T10:25:00Z',
'status': 'success'
}}
],
'alerts': [
{{
'severity': 'warning',
'message': 'High CPU usage on web-01',
'timestamp': '2024-01-16T10:28:00Z'
}}
]
}}
self.send_json_response(dashboard_data)
def handle_servers(self, params):
status_filter = params.get('status', [None])[0]
provider = params.get('provider', [None])[0]
# Use actual provisioning query command
cmd_args = ['nu', '-c', f'use ($env.PROVISIONING_PATH)/core/nulib/main_provisioning/query.nu; main query servers --out json']
result = self.run_provisioning_command(cmd_args)
self.send_json_response(result)
def handle_server_status(self, server_id, params):
# Mock server status - replace with actual server status check
server_status = {{
'server_id': server_id,
'status': 'running',
'uptime': '5d 12h 30m',
'cpu_usage': 34.2,
'memory_usage': 68.5,
'disk_usage': 45.1,
'network_in': 125.6,
'network_out': 89.3,
'last_check': self.get_timestamp()
}}
self.send_json_response(server_status)
def run_provisioning_command(self, cmd_args):
try:
result = subprocess.run(
cmd_args,
capture_output=True,
text=True,
env={{**os.environ, 'PROVISIONING_OUT': 'json'}}
)
if result.returncode == 0:
try:
return json.loads(result.stdout)
except json.JSONDecodeError:
return {{'output': result.stdout, 'raw': True}}
else:
return {{'error': result.stderr, 'returncode': result.returncode}}
except Exception as e:
return {{'error': str(e), 'type': 'execution_error'}}
def process_complex_query(self, query_data):
# Process complex queries with AI if available
if 'ai_query' in query_data:
# Use AI processing
ai_result = self.process_ai_query(query_data['ai_query'])
return ai_result
else:
# Standard complex query processing
return {{'result': 'Complex query processed', 'data': query_data}}
def process_ai_query(self, ai_query):
try:
cmd_args = [
'nu', '-c',
f'use ($env.PROVISIONING_PATH)/core/nulib/main_provisioning/query.nu; main query --ai-query \"{{ai_query}}\" --out json'
]
result = self.run_provisioning_command(cmd_args)
return result
except Exception as e:
return {{'error': f'AI query failed: {{str(e)}}'}}
def send_json_response(self, data):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
($cors_headers)
self.end_headers()
json_data = json.dumps(data, indent=2, ensure_ascii=False)
self.wfile.write(json_data.encode('utf-8'))
def get_timestamp(self):
from datetime import datetime
return datetime.utcnow().isoformat() + 'Z'
def log_message(self, format, *args):
if os.getenv('PROVISIONING_API_DEBUG') == 'true':
super().log_message(format, *args)
if __name__ == '__main__':
HOST = '($config.host)'
PORT = ($config.port)
# Set environment variables
os.environ['PROVISIONING_PATH'] = '($env.PROVISIONING_PATH | default "/usr/local/provisioning")'
with socketserver.TCPServer((HOST, PORT), ProvisioningAPIHandler) as httpd:
print(f'🌐 Provisioning API Server running on http://{{HOST}}:{{PORT}}')
print('📋 Available endpoints:')
print(' GET /api/v1/health')
print(' GET /api/v1/query')
print(' POST /api/v1/query')
print(' GET /api/v1/metrics')
print(' GET /api/v1/logs')
print(' GET /api/v1/dashboard')
print(' GET /api/v1/servers')
print(' GET /api/v1/servers/{{id}}/status')
print('\\n🎯 Server ready! Press Ctrl+C to stop')
try:
httpd.serve_forever()
except KeyboardInterrupt:
print('\\n🛑 Server shutting down...')
httpd.shutdown()
print('✅ Server stopped')
"
}
# WebSocket server for real-time updates (if enabled)
export def start_websocket_server [
--port: int = 8081
--host: string = "localhost"
]: nothing -> nothing {
print $"🔗 Starting WebSocket server on ($host):($port) for real-time updates"
print "This feature requires additional WebSocket implementation"
print "Consider using a Rust-based WebSocket server for production use"
}
# Health check for the API server
export def check_api_health [
--host: string = "localhost"
--port: int = 8080
]: nothing -> record {
try {
let response = http get $"http://($host):($port)/api/v1/health"
{
status: "healthy",
api_server: true,
response: $response
}
} catch {
{
status: "unhealthy",
api_server: false,
error: "Cannot connect to API server"
}
}
}