From 1590f368896664b1254e97c49ee5b152ac3edfbe Mon Sep 17 00:00:00 2001 From: Stefan Nilsson Date: Tue, 11 Feb 2025 11:20:52 +0100 Subject: [PATCH] Initial version --- Dockerfile | 12 ++++ api.yaml | 117 ++++++++++++++++++++++++++++++++++++ compose.yaml | 13 ++++ main.py | 153 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 17 ++++++ 5 files changed, 312 insertions(+) create mode 100644 Dockerfile create mode 100644 api.yaml create mode 100644 compose.yaml create mode 100644 main.py create mode 100644 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9dadaeb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/api.yaml b/api.yaml new file mode 100644 index 0000000..90b7ae9 --- /dev/null +++ b/api.yaml @@ -0,0 +1,117 @@ +openapi: 3.0.0 +info: + title: Proxmox Server Management API + description: API for managing Proxmox server statuses and power states. + version: 1.0.0 +servers: + - url: http://localhost:8000 +paths: + /servers: + get: + summary: Get the list of registered servers + operationId: getServers + responses: + '200': + description: A list of registered servers + content: + application/json: + schema: + type: array + items: + type: object + properties: + name: + type: string + ip: + type: string + mac: + type: string + post: + summary: Add or update a server + operationId: addServer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + ip: + type: string + mac: + type: string + responses: + '200': + description: Server added or updated successfully + /statuses: + get: + summary: Get the statuses of all servers + operationId: listAllStatuses + responses: + '200': + description: A list of all server statuses + content: + application/json: + schema: + type: object + /statuses/{server_name}: + get: + summary: Get the status of a specific server + operationId: getStatus + parameters: + - name: server_name + in: path + required: true + schema: + type: string + responses: + '200': + description: Server status + /states: + get: + summary: Get the power states of all servers + operationId: listAllStates + responses: + '200': + description: A list of all server power states + content: + application/json: + schema: + type: object + /states/{server_name}: + get: + summary: Get the power state of a specific server + operationId: getPowerStatus + parameters: + - name: server_name + in: path + required: true + schema: + type: string + responses: + '200': + description: Server power state + put: + summary: Control the power state of a server + operationId: controlPower + parameters: + - name: server_name + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + state: + type: string + enum: [on, off] + responses: + '200': + description: Power state updated diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..fd91043 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,13 @@ +version: '3.8' + +services: + api: + build: . + container_name: proxmox_api + ports: + - "8000:8000" + volumes: + - ./servers.db:/app/servers.db + environment: + - PYTHONUNBUFFERED=1 + restart: unless-stopped diff --git a/main.py b/main.py new file mode 100644 index 0000000..fc81fce --- /dev/null +++ b/main.py @@ -0,0 +1,153 @@ +from fastapi import FastAPI, HTTPException +from fastapi.responses import HTMLResponse +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 + +# Database setup +def init_db(): + conn = sqlite3.connect("servers.db") + cursor = conn.cursor() + cursor.execute('''CREATE TABLE IF NOT EXISTS servers ( + name TEXT PRIMARY KEY, + ip TEXT NOT NULL, + mac TEXT NOT NULL)''') + conn.commit() + conn.close() + +init_db() + +def get_server_info(server_name): + conn = sqlite3.connect("servers.db") + cursor = conn.cursor() + cursor.execute("SELECT ip, mac FROM servers WHERE name = ?", (server_name,)) + server = cursor.fetchone() + conn.close() + if server: + return {"ip": server[0], "mac": server[1]} + return None + +def list_servers(): + conn = sqlite3.connect("servers.db") + cursor = conn.cursor() + cursor.execute("SELECT name, ip, mac FROM servers") + servers = cursor.fetchall() + conn.close() + return [{"name": s[0], "ip": s[1], "mac": s[2]} for s in servers] + +def add_or_update_server(name: str, ip: str, mac: str): + conn = sqlite3.connect("servers.db") + cursor = conn.cursor() + cursor.execute("REPLACE INTO servers (name, ip, mac) VALUES (?, ?, ?)", (name, ip, mac)) + conn.commit() + conn.close() + +app = FastAPI() + +class ServerModel(BaseModel): + name: str + ip: str + mac: str + +def get_proxmox_status(server_ip): + """Fetch Proxmox server status.""" + try: + response = requests.get(f"https://{server_ip}:8006/api2/json/nodes", verify=False) # Assuming no authentication + response.raise_for_status() + data = response.json() + return {"status": data["data"][0]["status"]} # Extract node status + except requests.RequestException as e: + return {"status": "unknown", "error": 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=""" + + + + API Documentation + + + + + + + """, 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"]) + +@app.get("/statuses") +def list_all_statuses(): + """Returns the statuses of all servers.""" + servers = list_servers() + return {server["name"]: get_proxmox_status(server["ip"]) for server in servers} + +def check_power_state(server_ip): + """Check if the server is online by pinging it.""" + response = subprocess.run(["ping", "-c", "1", server_ip], stdout=subprocess.DEVNULL) + return "on" if response.returncode == 0 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() + return {server["name"]: check_power_state(server["ip"]) for server in servers} + +@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"]) + return {"message": "Wake-on-LAN signal sent"} + return {"message": "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 HTTPException(status_code=500, detail=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") +def add_server(server: ServerModel): + """Adds or updates a server.""" + add_or_update_server(server.name, server.ip, server.mac) + return {"message": "Server added/updated successfully"} + +# Run the server with: uvicorn script_name:app --host 0.0.0.0 --port 8000 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9643a1c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +annotated-types==0.7.0 +anyio==4.8.0 +certifi==2025.1.31 +charset-normalizer==3.4.1 +click==8.1.8 +fastapi==0.115.8 +h11==0.14.0 +idna==3.10 +pydantic==2.10.6 +pydantic_core==2.27.2 +requests==2.32.3 +sniffio==1.3.1 +starlette==0.45.3 +typing_extensions==4.12.2 +urllib3==2.3.0 +uvicorn==0.34.0 +wakeonlan==3.1.0