Initial version

This commit is contained in:
Stefan Nilsson 2025-02-11 11:20:52 +01:00
parent aef820e85c
commit 1590f36889
5 changed files with 312 additions and 0 deletions

12
Dockerfile Normal file
View File

@ -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"]

117
api.yaml Normal file
View File

@ -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

13
compose.yaml Normal file
View File

@ -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

153
main.py Normal file
View File

@ -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="""
<!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"])
@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

17
requirements.txt Normal file
View File

@ -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