pve-state/main.py
2025-02-12 16:49:46 +01:00

272 lines
9.1 KiB
Python

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse, JSONResponse
import requests
import os
import subprocess
import sqlite3
from wakeonlan import send_magic_packet
from pydantic import BaseModel
from fastapi.openapi.utils import get_openapi
from ping3 import ping
# Database setup
def init_db():
conn = sqlite3.connect("servers.db")
cursor = conn.cursor()
cursor.execute('''CREATE TABLE IF NOT EXISTS clusters (
name TEXT PRIMARY KEY,
api_token TEXT NOT NULL)''')
cursor.execute('''CREATE TABLE IF NOT EXISTS servers (
name TEXT PRIMARY KEY,
ip TEXT NOT NULL,
mac TEXT NOT NULL,
cluster_name TEXT NOT NULL,
FOREIGN KEY(cluster_name) REFERENCES clusters(name))''')
conn.commit()
conn.close()
init_db()
def execute_query(query, params=()):
try:
conn = sqlite3.connect("servers.db")
cursor = conn.cursor()
cursor.execute(query, params)
conn.commit()
conn.close()
except sqlite3.Error as e:
raise DatabaseError(str(e))
def fetch_query(query, params=()):
try:
conn = sqlite3.connect("servers.db")
cursor = conn.cursor()
cursor.execute(query, params)
result = cursor.fetchall()
conn.close()
return result
except sqlite3.Error as e:
raise DatabaseError(str(e))
def get_server_info(server_name):
result = fetch_query(
"SELECT servers.ip, servers.mac, clusters.api_token FROM servers JOIN clusters ON servers.cluster_name = clusters.name WHERE servers.name = ?",
(server_name,)
)
if result:
return {"ip": result[0][0], "mac": result[0][1], "api_token": result[0][2]}
return None
def list_servers():
servers = fetch_query(
"SELECT servers.name, servers.ip, servers.mac, servers.cluster_name FROM servers"
)
if servers:
return [{"name": s[0], "ip": s[1], "mac": s[2], "cluster_name": s[3]} for s in servers]
return None
def list_clusters():
clusters = fetch_query("SELECT name FROM clusters")
if clusters:
return [c[0] for c in clusters]
return None
def add_or_update_cluster(name: str, api_token: str):
execute_query("REPLACE INTO clusters (name, api_token) VALUES (?, ?)", (name, api_token))
def delete_cluster(name: str):
execute_query("DELETE FROM clusters WHERE name = ?", (name,))
def add_or_update_server(name: str, ip: str, mac: str, cluster_name: str):
execute_query("REPLACE INTO servers (name, ip, mac, cluster_name) VALUES (?, ?, ?, ?)", (name, ip, mac, cluster_name))
def delete_server(name: str):
execute_query("DELETE FROM servers WHERE name = ?", (name,))
app = FastAPI()
class DatabaseError(Exception):
"""Custom exception for database errors."""
def __init__(self, message: str):
self.message = message
class ProxmoxAPIError(Exception):
"""Custom exception for Proxmox API errors."""
def __init__(self, message: str):
self.message = message
@app.exception_handler(DatabaseError)
def database_error_handler(request: Request, exc: DatabaseError):
return JSONResponse(
status_code=500,
content={"error": "Database Error", "message": exc.message},
)
@app.exception_handler(ProxmoxAPIError)
def proxmox_api_error_handler(request: Request, exc: ProxmoxAPIError):
return JSONResponse(
status_code=500,
content={"error": "Proxmox API Error", "message": exc.message},
)
@app.exception_handler(Exception)
def generic_exception_handler(request: Request, exc: Exception):
return JSONResponse(
status_code=500,
content={"error": "Internal Server Error", "message": str(exc)},
)
class ServerModel(BaseModel):
name: str
ip: str
mac: str
cluster_name: str
class ClusterModel(BaseModel):
name: str
api_token: str
def get_proxmox_status(server_ip, api_token):
"""Fetch Proxmox server status using API token."""
headers = {"Authorization": f"PVEAPIToken={api_token}"}
try:
response = requests.get(f"https://{server_ip}:8006/api2/json/nodes", headers=headers, verify=False)
response.raise_for_status()
data = response.json()
return {"status": data["data"][0]["status"]} # Extract node status
except requests.exceptions.Timeout:
raise ProxmoxAPIError("Connection to Proxmox server timed out")
except requests.exceptions.ConnectionError:
raise ProxmoxAPIError("Failed to connect to Proxmox server")
except requests.exceptions.RequestException as e:
raise ProxmoxAPIError(str(e))
@app.get("/openapi.json")
def get_openapi_spec():
"""Returns the OpenAPI specification."""
return get_openapi(title=app.title, version="1.0.0", routes=app.routes)
@app.get("/apidoc", response_class=HTMLResponse)
def api_docs():
"""Returns the API documentation using RapiDoc."""
return HTMLResponse(content="""
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
</head>
<body>
<rapi-doc spec-url="/openapi.json" theme="light"></rapi-doc>
</body>
</html>
""", status_code=200)
@app.get("/statuses/{server_name}")
def status(server_name: str):
"""Returns the Proxmox server status for a specific server."""
server = get_server_info(server_name)
if not server:
raise HTTPException(status_code=404, detail="Server not found")
return get_proxmox_status(server["ip"], server["api_token"])
@app.get("/statuses")
def list_all_statuses():
"""Returns the statuses of all servers."""
servers = list_servers()
statuses = {}
for server in servers:
try:
cluster_token = get_server_info(server["name"])["api_token"]
statuses[server["name"]] = get_proxmox_status(server["ip"], cluster_token)
except ProxmoxAPIError as e:
statuses[server["name"]] = {"status": "unknown", "error": str(e)}
return statuses
@app.get("/clusters")
def get_clusters():
"""Returns a list of all cluster names."""
return list_clusters()
@app.post("/clusters")
@app.put("/clusters")
def add_cluster(cluster: ClusterModel):
"""Adds or updates a cluster."""
add_or_update_cluster(cluster.name, cluster.api_token)
return {"message": "Cluster added/updated successfully"}
@app.delete("/clusters/{cluster_name}")
def remove_cluster(cluster_name: str):
"""Deletes a cluster."""
delete_cluster(cluster_name)
return {"message": "Cluster deleted successfully"}
def check_power_state(server_ip):
"""Check if the server is online by pinging it."""
return "on" if ping(server_ip, timeout=1) else "off"
@app.get("/states/{server_name}")
def get_power_status(server_name: str):
"""Returns the power status of a specific server."""
server = get_server_info(server_name)
if not server:
raise HTTPException(status_code=404, detail="Server not found")
return {"power": check_power_state(server["ip"])}
@app.get("/states")
def list_all_states():
"""Returns the power states of all servers."""
servers = list_servers()
power_states = {}
for server in servers:
try:
power_states[server["name"]] = check_power_state(server["ip"])
except Exception as e:
power_states[server["name"]] = {"power": "unknown", "error": str(e)}
return power_states
@app.put("/states/{server_name}")
@app.patch("/states/{server_name}")
def control_power(server_name: str, state: str):
"""Controls the server power state (wake or shutdown)."""
server = get_server_info(server_name)
if not server:
raise HTTPException(status_code=404, detail="Server not found")
if state == "on":
if check_power_state(server["ip"]) == "off":
send_magic_packet(server["mac"], ip_address="192.168.1.255")
return {"message": "Wake-on-LAN signal sent"}
raise HTTPException(status_code=400, detail="Server is already on")
elif state == "off":
try:
requests.post(f"https://{server["ip"]}:8006/api2/json/nodes/shutdown", verify=False)
return {"message": "Shutdown command sent"}
except requests.RequestException as e:
raise ProxmoxAPIError(f"Failed to send shutdown command: {str(e)}")
else:
raise HTTPException(status_code=400, detail="Invalid state. Use 'on' or 'off'")
@app.get("/servers")
def get_servers():
"""Returns the list of registered servers."""
return list_servers()
@app.post("/servers")
@app.put("/servers")
def add_server(server: ServerModel):
"""Adds or updates a server."""
add_or_update_server(server.name, server.ip, server.mac, server.cluster_name)
return {"message": "Server added/updated successfully"}
@app.delete("/servers/{server_name}")
def remove_server(server_name: str):
"""Deletes a server."""
delete_server(server_name)
return {"message": "Server deleted successfully"}
# Run the server with: uvicorn main:app --host 0.0.0.0 --port 8000