feat: première version fonctionnelle
This commit is contained in:
1
backend/app/api/__init__.py
Normal file
1
backend/app/api/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# API package
|
||||
166
backend/app/api/proxmox.py
Normal file
166
backend/app/api/proxmox.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, ProxmoxCluster
|
||||
from app.models.schemas import ProxmoxCluster as ProxmoxClusterSchema, ProxmoxClusterCreate, ProxmoxVM
|
||||
from app.services.proxmox_service import ProxmoxService
|
||||
from app.services.logging_service import LoggingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/clusters", response_model=List[ProxmoxClusterSchema])
|
||||
async def get_clusters(db: Session = Depends(get_db)):
|
||||
clusters = db.query(ProxmoxCluster).all()
|
||||
return clusters
|
||||
|
||||
@router.post("/clusters", response_model=ProxmoxClusterSchema)
|
||||
async def create_cluster(cluster: ProxmoxClusterCreate, db: Session = Depends(get_db)):
|
||||
try:
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
if not await proxmox_service.test_connection():
|
||||
raise HTTPException(status_code=400, detail="Cannot connect to Proxmox cluster")
|
||||
|
||||
db_cluster = ProxmoxCluster(**cluster.dict())
|
||||
db.add(db_cluster)
|
||||
db.commit()
|
||||
db.refresh(db_cluster)
|
||||
|
||||
# Log cluster creation
|
||||
LoggingService.log_proxmox_cluster_action(
|
||||
db=db,
|
||||
action="create",
|
||||
cluster_id=db_cluster.id,
|
||||
cluster_name=db_cluster.name,
|
||||
success=True,
|
||||
message=f"Proxmox cluster '{db_cluster.name}' created successfully"
|
||||
)
|
||||
|
||||
return db_cluster
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error creating cluster: {str(e)}")
|
||||
|
||||
@router.get("/clusters/{cluster_id}", response_model=ProxmoxClusterSchema)
|
||||
async def get_cluster(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
return cluster
|
||||
|
||||
@router.delete("/clusters/{cluster_id}")
|
||||
async def delete_cluster(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
# Log cluster deletion before deleting
|
||||
LoggingService.log_proxmox_cluster_action(
|
||||
db=db,
|
||||
action="delete",
|
||||
cluster_id=cluster.id,
|
||||
cluster_name=cluster.name,
|
||||
success=True,
|
||||
message=f"Proxmox cluster '{cluster.name}' deleted successfully"
|
||||
)
|
||||
|
||||
db.delete(cluster)
|
||||
db.commit()
|
||||
return {"message": "Cluster deleted successfully"}
|
||||
|
||||
@router.get("/clusters/{cluster_id}/vms", response_model=List[ProxmoxVM])
|
||||
async def get_cluster_vms(cluster_id: int, db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
try:
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
vms = await proxmox_service.get_vms()
|
||||
return vms
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Error getting VMs: {str(e)}")
|
||||
|
||||
@router.post("/clusters/{cluster_id}/vms/{vmid}/start")
|
||||
async def start_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
success = await proxmox_service.start_vm(node, vmid, vm_type)
|
||||
|
||||
# Get VM name from the cluster's VMs
|
||||
vms = await proxmox_service.get_vms()
|
||||
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
|
||||
|
||||
# Log VM start action
|
||||
LoggingService.log_proxmox_vm_action(
|
||||
db=db,
|
||||
action="start",
|
||||
vmid=vmid,
|
||||
vm_name=vm_name,
|
||||
node=node,
|
||||
success=success,
|
||||
message=f"VM {vm_name} ({'started' if success else 'failed to start'}) on node {node}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to start VM")
|
||||
|
||||
return {"message": f"VM {vmid} start command sent", "success": True}
|
||||
|
||||
@router.post("/clusters/{cluster_id}/vms/{vmid}/stop")
|
||||
async def stop_vm(cluster_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
|
||||
cluster = db.query(ProxmoxCluster).filter(ProxmoxCluster.id == cluster_id).first()
|
||||
if not cluster:
|
||||
raise HTTPException(status_code=404, detail="Cluster not found")
|
||||
|
||||
proxmox_service = ProxmoxService(
|
||||
host=cluster.host,
|
||||
user=cluster.username,
|
||||
password=cluster.password,
|
||||
port=cluster.port,
|
||||
verify_ssl=cluster.verify_ssl
|
||||
)
|
||||
|
||||
success = await proxmox_service.stop_vm(node, vmid, vm_type)
|
||||
|
||||
# Get VM name from the cluster's VMs
|
||||
vms = await proxmox_service.get_vms()
|
||||
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
|
||||
|
||||
# Log VM stop action
|
||||
LoggingService.log_proxmox_vm_action(
|
||||
db=db,
|
||||
action="stop",
|
||||
vmid=vmid,
|
||||
vm_name=vm_name,
|
||||
node=node,
|
||||
success=success,
|
||||
message=f"VM {vm_name} ({'stopped' if success else 'failed to stop'}) on node {node}"
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=500, detail="Failed to stop VM")
|
||||
|
||||
return {"message": f"VM {vmid} stop command sent", "success": True}
|
||||
90
backend/app/api/servers.py
Normal file
90
backend/app/api/servers.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, Server
|
||||
from app.models.schemas import Server as ServerSchema, ServerCreate
|
||||
from app.services.wol_service import WolService
|
||||
from app.services.logging_service import LoggingService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/", response_model=List[ServerSchema])
|
||||
async def get_servers(db: Session = Depends(get_db)):
|
||||
servers = db.query(Server).all()
|
||||
return servers
|
||||
|
||||
@router.post("/", response_model=ServerSchema)
|
||||
async def create_server(server: ServerCreate, db: Session = Depends(get_db)):
|
||||
db_server = Server(**server.dict())
|
||||
db.add(db_server)
|
||||
db.commit()
|
||||
db.refresh(db_server)
|
||||
|
||||
# Log server creation
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="create",
|
||||
server_id=db_server.id,
|
||||
server_name=db_server.name,
|
||||
success=True,
|
||||
message=f"Server '{db_server.name}' created successfully"
|
||||
)
|
||||
|
||||
return db_server
|
||||
|
||||
@router.get("/{server_id}", response_model=ServerSchema)
|
||||
async def get_server(server_id: int, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
return server
|
||||
|
||||
@router.put("/{server_id}", response_model=ServerSchema)
|
||||
async def update_server(server_id: int, server_update: ServerCreate, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
old_name = server.name
|
||||
for key, value in server_update.dict().items():
|
||||
setattr(server, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
|
||||
# Log server update
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="update",
|
||||
server_id=server.id,
|
||||
server_name=server.name,
|
||||
success=True,
|
||||
message=f"Server '{old_name}' updated successfully"
|
||||
)
|
||||
|
||||
return server
|
||||
|
||||
@router.delete("/{server_id}")
|
||||
async def delete_server(server_id: int, db: Session = Depends(get_db)):
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
# Log server deletion before deleting
|
||||
LoggingService.log_server_action(
|
||||
db=db,
|
||||
action="delete",
|
||||
server_id=server.id,
|
||||
server_name=server.name,
|
||||
success=True,
|
||||
message=f"Server '{server.name}' deleted successfully"
|
||||
)
|
||||
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
return {"message": "Server deleted successfully"}
|
||||
|
||||
@router.post("/check-status")
|
||||
async def check_all_servers_status(db: Session = Depends(get_db)):
|
||||
await WolService.check_all_servers_status(db)
|
||||
return {"message": "Server status checked for all servers"}
|
||||
50
backend/app/api/wol.py
Normal file
50
backend/app/api/wol.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.database import get_db, WolLog, ActionLog
|
||||
from app.models.schemas import WolLog as WolLogSchema, ActionLog as ActionLogSchema
|
||||
from app.services.wol_service import WolService
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/wake/{server_id}")
|
||||
async def wake_server(server_id: int, db: Session = Depends(get_db)):
|
||||
success = await WolService.wake_server(db, server_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Server not found or WOL failed")
|
||||
return {"message": f"WOL packet sent to server {server_id}", "success": True}
|
||||
|
||||
@router.post("/ping/{server_id}")
|
||||
async def ping_server(server_id: int, db: Session = Depends(get_db)):
|
||||
from app.database import Server
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
is_online = await WolService.ping_server(server.ip_address)
|
||||
server.is_online = is_online
|
||||
db.commit()
|
||||
|
||||
return {"server_id": server_id, "is_online": is_online}
|
||||
|
||||
@router.get("/logs", response_model=List[WolLogSchema])
|
||||
async def get_wol_logs(db: Session = Depends(get_db), limit: int = 50):
|
||||
logs = db.query(WolLog).order_by(WolLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/logs/{server_id}", response_model=List[WolLogSchema])
|
||||
async def get_server_wol_logs(server_id: int, db: Session = Depends(get_db), limit: int = 20):
|
||||
logs = db.query(WolLog).filter(WolLog.server_id == server_id).order_by(WolLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/all-logs", response_model=List[ActionLogSchema])
|
||||
async def get_all_action_logs(db: Session = Depends(get_db), limit: int = 100):
|
||||
"""Get all action logs (WOL, Proxmox, Server actions)"""
|
||||
logs = db.query(ActionLog).order_by(ActionLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
|
||||
@router.get("/all-logs/{action_type}", response_model=List[ActionLogSchema])
|
||||
async def get_action_logs_by_type(action_type: str, db: Session = Depends(get_db), limit: int = 50):
|
||||
"""Get action logs filtered by type (wol, proxmox, server)"""
|
||||
logs = db.query(ActionLog).filter(ActionLog.action_type == action_type).order_by(ActionLog.timestamp.desc()).limit(limit).all()
|
||||
return logs
|
||||
69
backend/app/database.py
Normal file
69
backend/app/database.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from sqlalchemy import create_engine, Column, Integer, String, DateTime, Boolean
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db")
|
||||
|
||||
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
class Server(Base):
|
||||
__tablename__ = "servers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
ip_address = Column(String, nullable=False)
|
||||
mac_address = Column(String, nullable=False)
|
||||
description = Column(String)
|
||||
is_online = Column(Boolean, default=False)
|
||||
last_ping = Column(DateTime)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class ProxmoxCluster(Base):
|
||||
__tablename__ = "proxmox_clusters"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String, index=True, nullable=False)
|
||||
host = Column(String, nullable=False)
|
||||
username = Column(String, nullable=False)
|
||||
password = Column(String, nullable=False)
|
||||
port = Column(Integer, default=8006)
|
||||
verify_ssl = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
class ActionLog(Base):
|
||||
__tablename__ = "action_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
action_type = Column(String, nullable=False) # 'wol', 'proxmox', 'server'
|
||||
target_id = Column(Integer, nullable=True) # server_id, cluster_id, vm_id, etc.
|
||||
target_name = Column(String, nullable=True) # server name, vm name, etc.
|
||||
action = Column(String, nullable=False) # 'wake', 'start', 'stop', 'create', 'delete', etc.
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
success = Column(Boolean, default=True)
|
||||
message = Column(String)
|
||||
details = Column(String, nullable=True) # JSON string for additional data
|
||||
|
||||
# Keep WolLog for backward compatibility
|
||||
class WolLog(Base):
|
||||
__tablename__ = "wol_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
server_id = Column(Integer, nullable=False)
|
||||
action = Column(String, nullable=False)
|
||||
timestamp = Column(DateTime, default=datetime.utcnow)
|
||||
success = Column(Boolean, default=True)
|
||||
message = Column(String)
|
||||
|
||||
def init_db():
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
44
backend/app/main.py
Normal file
44
backend/app/main.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from app.database import init_db
|
||||
from app.api import servers, proxmox, wol
|
||||
import traceback
|
||||
|
||||
app = FastAPI(title="Zebra Power", description="Wake-on-LAN and Proxmox Management API")
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": f"Internal server error: {str(exc)}"},
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "*",
|
||||
"Access-Control-Allow-Headers": "*",
|
||||
}
|
||||
)
|
||||
|
||||
app.include_router(servers.router, prefix="/api/servers", tags=["servers"])
|
||||
app.include_router(proxmox.router, prefix="/api/proxmox", tags=["proxmox"])
|
||||
app.include_router(wol.router, prefix="/api/wol", tags=["wol"])
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
init_db()
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {"message": "Zebra Power API is running"}
|
||||
|
||||
@app.get("/api/health")
|
||||
async def health_check():
|
||||
return {"status": "healthy"}
|
||||
1
backend/app/models/__init__.py
Normal file
1
backend/app/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Models package
|
||||
75
backend/app/models/schemas.py
Normal file
75
backend/app/models/schemas.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
class ServerBase(BaseModel):
|
||||
name: str
|
||||
ip_address: str
|
||||
mac_address: str
|
||||
description: Optional[str] = None
|
||||
|
||||
class ServerCreate(ServerBase):
|
||||
pass
|
||||
|
||||
class Server(ServerBase):
|
||||
id: int
|
||||
is_online: bool
|
||||
last_ping: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProxmoxClusterBase(BaseModel):
|
||||
name: str
|
||||
host: str
|
||||
username: str
|
||||
password: str
|
||||
port: int = 8006
|
||||
verify_ssl: bool = True
|
||||
|
||||
class ProxmoxClusterCreate(ProxmoxClusterBase):
|
||||
pass
|
||||
|
||||
class ProxmoxCluster(ProxmoxClusterBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ProxmoxVM(BaseModel):
|
||||
vmid: str
|
||||
name: str
|
||||
status: str
|
||||
node: str
|
||||
type: str
|
||||
|
||||
class WolLogCreate(BaseModel):
|
||||
server_id: int
|
||||
action: str
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
|
||||
class WolLog(WolLogCreate):
|
||||
id: int
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
class ActionLogCreate(BaseModel):
|
||||
action_type: str
|
||||
target_id: Optional[int] = None
|
||||
target_name: Optional[str] = None
|
||||
action: str
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
|
||||
class ActionLog(ActionLogCreate):
|
||||
id: int
|
||||
timestamp: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
1
backend/app/services/__init__.py
Normal file
1
backend/app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Services package
|
||||
82
backend/app/services/logging_service.py
Normal file
82
backend/app/services/logging_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import ActionLog
|
||||
from typing import Optional
|
||||
import json
|
||||
|
||||
class LoggingService:
|
||||
@staticmethod
|
||||
def log_action(
|
||||
db: Session,
|
||||
action_type: str,
|
||||
action: str,
|
||||
success: bool = True,
|
||||
message: Optional[str] = None,
|
||||
target_id: Optional[int] = None,
|
||||
target_name: Optional[str] = None,
|
||||
details: Optional[dict] = None
|
||||
):
|
||||
"""Log an action to the unified action log"""
|
||||
log_entry = ActionLog(
|
||||
action_type=action_type,
|
||||
target_id=target_id,
|
||||
target_name=target_name,
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
details=json.dumps(details) if details else None
|
||||
)
|
||||
db.add(log_entry)
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def log_wol_action(db: Session, server_id: int, server_name: str, success: bool, message: str):
|
||||
"""Log WOL action"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="wol",
|
||||
action="wake",
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=server_id,
|
||||
target_name=server_name
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_proxmox_vm_action(db: Session, action: str, vmid: str, vm_name: str, node: str, success: bool, message: str):
|
||||
"""Log Proxmox VM actions (start/stop)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="proxmox",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=int(vmid) if vmid.isdigit() else None,
|
||||
target_name=vm_name,
|
||||
details={"node": node, "vmid": vmid}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_proxmox_cluster_action(db: Session, action: str, cluster_id: int, cluster_name: str, success: bool, message: str):
|
||||
"""Log Proxmox cluster actions (create/delete)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="proxmox",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=cluster_id,
|
||||
target_name=cluster_name
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def log_server_action(db: Session, action: str, server_id: int, server_name: str, success: bool, message: str):
|
||||
"""Log server actions (create/update/delete)"""
|
||||
LoggingService.log_action(
|
||||
db=db,
|
||||
action_type="server",
|
||||
action=action,
|
||||
success=success,
|
||||
message=message,
|
||||
target_id=server_id,
|
||||
target_name=server_name
|
||||
)
|
||||
114
backend/app/services/proxmox_service.py
Normal file
114
backend/app/services/proxmox_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
from proxmoxer import ProxmoxAPI
|
||||
from typing import List, Optional
|
||||
from app.models.schemas import ProxmoxVM
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ProxmoxService:
|
||||
def __init__(self, host: str, user: str, password: str, port: int = 8006, verify_ssl: bool = True):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.port = port
|
||||
self.verify_ssl = verify_ssl
|
||||
self._proxmox = None
|
||||
|
||||
def _get_connection(self):
|
||||
if not self._proxmox:
|
||||
try:
|
||||
self._proxmox = ProxmoxAPI(
|
||||
self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
port=self.port,
|
||||
verify_ssl=self.verify_ssl,
|
||||
timeout=10
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Proxmox {self.host}: {str(e)}")
|
||||
raise ConnectionError(f"Cannot connect to Proxmox: {str(e)}")
|
||||
return self._proxmox
|
||||
|
||||
async def test_connection(self) -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
proxmox.version.get()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Proxmox connection test failed: {str(e)}")
|
||||
return False
|
||||
|
||||
async def get_nodes(self) -> List[dict]:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
return proxmox.nodes.get()
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get nodes: {str(e)}")
|
||||
return []
|
||||
|
||||
async def get_vms(self, node: Optional[str] = None) -> List[ProxmoxVM]:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
vms = []
|
||||
|
||||
if node:
|
||||
nodes = [node]
|
||||
else:
|
||||
nodes_list = await self.get_nodes()
|
||||
nodes = [n['node'] for n in nodes_list]
|
||||
|
||||
for node_name in nodes:
|
||||
try:
|
||||
qemu_vms = proxmox.nodes(node_name).qemu.get()
|
||||
for vm in qemu_vms:
|
||||
if vm.get('template', 0) != 1:
|
||||
vms.append(ProxmoxVM(
|
||||
vmid=str(vm['vmid']),
|
||||
name=vm.get('name', f"VM-{vm['vmid']}"),
|
||||
status=vm.get('status', 'unknown'),
|
||||
node=node_name,
|
||||
type='qemu'
|
||||
))
|
||||
|
||||
lxc_containers = proxmox.nodes(node_name).lxc.get()
|
||||
for container in lxc_containers:
|
||||
vms.append(ProxmoxVM(
|
||||
vmid=str(container['vmid']),
|
||||
name=container.get('name', f"CT-{container['vmid']}"),
|
||||
status=container.get('status', 'unknown'),
|
||||
node=node_name,
|
||||
type='lxc'
|
||||
))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VMs from node {node_name}: {str(e)}")
|
||||
continue
|
||||
|
||||
return vms
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get VMs: {str(e)}")
|
||||
return []
|
||||
|
||||
async def start_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
if vm_type == 'lxc':
|
||||
proxmox.nodes(node).lxc(vmid).status.start.post()
|
||||
else:
|
||||
proxmox.nodes(node).qemu(vmid).status.start.post()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to start VM {vmid}: {str(e)}")
|
||||
return False
|
||||
|
||||
async def stop_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool:
|
||||
try:
|
||||
proxmox = self._get_connection()
|
||||
if vm_type == 'lxc':
|
||||
proxmox.nodes(node).lxc(vmid).status.shutdown.post()
|
||||
else:
|
||||
proxmox.nodes(node).qemu(vmid).status.shutdown.post()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop VM {vmid}: {str(e)}")
|
||||
return False
|
||||
89
backend/app/services/wol_service.py
Normal file
89
backend/app/services/wol_service.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import asyncio
|
||||
import subprocess
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import Server, WolLog
|
||||
from app.models.schemas import WolLogCreate
|
||||
from app.services.logging_service import LoggingService
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WolService:
|
||||
@staticmethod
|
||||
async def send_wol_packet(mac_address: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["wakeonlan", mac_address],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
return result.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"WOL timeout for MAC: {mac_address}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"WOL error for MAC {mac_address}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def ping_server(ip_address: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ping", "-c", "1", "-W", "3", ip_address],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except subprocess.TimeoutExpired:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Ping error for IP {ip_address}: {str(e)}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def wake_server(db: Session, server_id: int) -> bool:
|
||||
server = db.query(Server).filter(Server.id == server_id).first()
|
||||
if not server:
|
||||
return False
|
||||
|
||||
success = await WolService.send_wol_packet(server.mac_address)
|
||||
|
||||
# Log to both WolLog (backward compatibility) and ActionLog (new unified system)
|
||||
log_entry = WolLog(
|
||||
server_id=server_id,
|
||||
action="wake",
|
||||
success=success,
|
||||
message=f"WOL packet sent to {server.mac_address}" if success else "Failed to send WOL packet"
|
||||
)
|
||||
db.add(log_entry)
|
||||
|
||||
# Log to unified action log
|
||||
LoggingService.log_wol_action(
|
||||
db=db,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
success=success,
|
||||
message=f"WOL packet {'sent successfully' if success else 'failed'} to {server.name} ({server.mac_address})"
|
||||
)
|
||||
|
||||
return success
|
||||
|
||||
@staticmethod
|
||||
async def check_all_servers_status(db: Session) -> None:
|
||||
servers = db.query(Server).all()
|
||||
|
||||
tasks = []
|
||||
for server in servers:
|
||||
tasks.append(WolService.ping_server(server.ip_address))
|
||||
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
from datetime import datetime
|
||||
for server, is_online in zip(servers, results):
|
||||
server.is_online = is_online
|
||||
server.last_ping = datetime.utcnow()
|
||||
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user