446 lines
15 KiB
Plaintext
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"
|
||
|
}
|
||
|
}
|
||
|
}
|