Initial version
This commit is contained in:
parent
aef820e85c
commit
1590f36889
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
117
api.yaml
Normal 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
13
compose.yaml
Normal 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
153
main.py
Normal 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
17
requirements.txt
Normal 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
|
Loading…
Reference in New Issue
Block a user