refact: use only server

This commit is contained in:
2025-08-27 13:28:50 +02:00
parent cf8a37f183
commit 2934224787
39 changed files with 6843 additions and 1223 deletions

View File

@@ -1,17 +1,29 @@
FROM python:3.11-slim
FROM python:3.13-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
wakeonlan \
iputils-ping \
curl \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Install uv for faster package management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Copy requirements and install dependencies with uv
COPY requirements.txt .
RUN uv pip install --system --no-cache -r requirements.txt
# Copy application code
COPY app/ ./app/
COPY migrate_to_unified_hosts.py ./
# Create data directory for SQLite
RUN mkdir -p /app/data
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# Production server without auto-reload
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

30
backend/Dockerfile.dev Normal file
View File

@@ -0,0 +1,30 @@
FROM python:3.13-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
wakeonlan \
iputils-ping \
curl \
&& rm -rf /var/lib/apt/lists/*
# Install uv for faster package management
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
# Copy requirements and install dependencies with uv
COPY requirements.txt .
RUN uv pip install --system --no-cache -r requirements.txt
# Copy application code
COPY app/ ./app/
COPY migrate_to_unified_hosts.py ./
COPY test_api.py ./
# Create data directory for SQLite
RUN mkdir -p /app/data
EXPOSE 8000
# Development server with auto-reload
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

247
backend/app/api/hosts.py Normal file
View File

@@ -0,0 +1,247 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List
from app.database import get_db, ProxmoxHost
from app.models.schemas import ProxmoxHost as ProxmoxHostSchema, ProxmoxHostCreate, ProxmoxVM
from app.services.proxmox_host_service import ProxmoxHostService
from app.services.logging_service import LoggingService
router = APIRouter()
@router.get("/", response_model=List[ProxmoxHostSchema])
async def get_hosts(db: Session = Depends(get_db)):
"""Get all Proxmox hosts"""
hosts = db.query(ProxmoxHost).all()
return hosts
@router.post("/", response_model=ProxmoxHostSchema)
async def create_host(host: ProxmoxHostCreate, db: Session = Depends(get_db)):
"""Create a new Proxmox host"""
try:
# Test Proxmox connection before creating
temp_host = ProxmoxHost(**host.dict())
host_service = ProxmoxHostService(temp_host)
if not await host_service.test_proxmox_connection():
raise HTTPException(status_code=400, detail="Cannot connect to Proxmox host")
# Create the host in database
db_host = ProxmoxHost(**host.dict())
db.add(db_host)
db.commit()
db.refresh(db_host)
# Log host creation
LoggingService.log_host_action(
db=db,
action="create",
host_id=db_host.id,
host_name=db_host.name,
success=True,
message=f"Proxmox host '{db_host.name}' created successfully"
)
return db_host
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error creating host: {str(e)}")
@router.get("/{host_id}", response_model=ProxmoxHostSchema)
async def get_host(host_id: int, db: Session = Depends(get_db)):
"""Get a specific Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
return host
@router.put("/{host_id}", response_model=ProxmoxHostSchema)
async def update_host(host_id: int, host_update: ProxmoxHostCreate, db: Session = Depends(get_db)):
"""Update a Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
# Test new connection if Proxmox settings changed
temp_host = ProxmoxHost(**host_update.dict(), id=host_id)
host_service = ProxmoxHostService(temp_host)
if not await host_service.test_proxmox_connection():
raise HTTPException(status_code=400, detail="Cannot connect to Proxmox with new settings")
old_name = host.name
for key, value in host_update.dict().items():
setattr(host, key, value)
db.commit()
db.refresh(host)
# Log host update
LoggingService.log_host_action(
db=db,
action="update",
host_id=host.id,
host_name=host.name,
success=True,
message=f"Proxmox host '{old_name}' updated successfully"
)
return host
@router.delete("/{host_id}")
async def delete_host(host_id: int, db: Session = Depends(get_db)):
"""Delete a Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
# Log host deletion before deleting
LoggingService.log_host_action(
db=db,
action="delete",
host_id=host.id,
host_name=host.name,
success=True,
message=f"Proxmox host '{host.name}' deleted successfully"
)
db.delete(host)
db.commit()
return {"message": "Host deleted successfully"}
@router.post("/{host_id}/wake")
async def wake_host(host_id: int, db: Session = Depends(get_db)):
"""Wake up a Proxmox host using Wake-on-LAN"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
host_service = ProxmoxHostService(host)
success = await host_service.wake_host(db)
if not success:
raise HTTPException(status_code=500, detail="Failed to wake host")
return {"message": f"WOL packet sent to {host.name}", "success": True}
@router.post("/{host_id}/shutdown")
async def shutdown_host(host_id: int, db: Session = Depends(get_db)):
"""Shutdown a Proxmox host (and all its VMs)"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
host_service = ProxmoxHostService(host)
success = await host_service.shutdown_host(db)
if not success:
raise HTTPException(status_code=500, detail="Failed to shutdown host")
return {"message": f"Shutdown initiated for {host.name}", "success": True}
@router.get("/{host_id}/vms", response_model=List[ProxmoxVM])
async def get_host_vms(host_id: int, db: Session = Depends(get_db)):
"""Get all VMs/containers from a Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
try:
host_service = ProxmoxHostService(host)
vms = await host_service.get_vms()
return vms
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error getting VMs: {str(e)}")
@router.post("/{host_id}/vms/{vmid}/start")
async def start_vm(host_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
"""Start a VM/container on a Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
host_service = ProxmoxHostService(host)
success = await host_service.start_vm(node, vmid, vm_type)
# Get VM name for logging
try:
vms = await host_service.get_vms()
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
except:
vm_name = 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("/{host_id}/vms/{vmid}/stop")
async def stop_vm(host_id: int, vmid: str, node: str, vm_type: str = "qemu", db: Session = Depends(get_db)):
"""Stop a VM/container on a Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
host_service = ProxmoxHostService(host)
success = await host_service.stop_vm(node, vmid, vm_type)
# Get VM name for logging
try:
vms = await host_service.get_vms()
vm_name = next((vm.name for vm in vms if vm.vmid == vmid), f"VM-{vmid}")
except:
vm_name = 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}
@router.post("/check-status")
async def check_all_hosts_status(db: Session = Depends(get_db)):
"""Check online status of all Proxmox hosts"""
await ProxmoxHostService.check_all_hosts_status(db)
return {"message": "Host status checked for all hosts"}
@router.get("/{host_id}/status")
async def check_host_status(host_id: int, db: Session = Depends(get_db)):
"""Check online status of a specific Proxmox host"""
host = db.query(ProxmoxHost).filter(ProxmoxHost.id == host_id).first()
if not host:
raise HTTPException(status_code=404, detail="Host not found")
is_online = await ProxmoxHostService.ping_host(host.ip_address)
from datetime import datetime
host.is_online = is_online
host.last_ping = datetime.utcnow()
db.commit()
return {
"host_id": host_id,
"host_name": host.name,
"is_online": is_online,
"last_ping": host.last_ping
}

View File

@@ -1,166 +0,0 @@
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}

View File

@@ -1,90 +0,0 @@
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"}

View File

@@ -34,6 +34,28 @@ class ProxmoxCluster(Base):
verify_ssl = Column(Boolean, default=True)
created_at = Column(DateTime, default=datetime.utcnow)
class ProxmoxHost(Base):
__tablename__ = "proxmox_hosts"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True, nullable=False)
description = Column(String)
# Network configuration for WOL
ip_address = Column(String, nullable=False)
mac_address = Column(String, nullable=False)
# Proxmox configuration
proxmox_host = Column(String, nullable=False) # Can be same as ip_address
proxmox_username = Column(String, nullable=False)
proxmox_password = Column(String, nullable=False)
proxmox_port = Column(Integer, default=8006)
verify_ssl = Column(Boolean, default=True)
# Host status
is_online = Column(Boolean, default=False)
last_ping = Column(DateTime)
# Optional shutdown endpoint for host shutdown
shutdown_endpoint = Column(String, nullable=True) # e.g., /api/hosts/shutdown
created_at = Column(DateTime, default=datetime.utcnow)
class ActionLog(Base):
__tablename__ = "action_logs"

View File

@@ -2,7 +2,7 @@ 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
from app.api import wol, hosts
import traceback
app = FastAPI(title="Zebra Power", description="Wake-on-LAN and Proxmox Management API")
@@ -27,9 +27,12 @@ async def global_exception_handler(request: Request, exc: Exception):
}
)
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"])
# Anciens endpoints conservés temporairement pour transition
# app.include_router(servers.router, prefix="/api/servers", tags=["servers"]) - SUPPRIMÉ
# app.include_router(proxmox.router, prefix="/api/proxmox", tags=["proxmox"]) - SUPPRIMÉ
app.include_router(wol.router, prefix="/api/wol", tags=["wol"]) # Conservé pour les logs
app.include_router(hosts.router, prefix="/api/hosts", tags=["hosts"]) # Nouvelle API unifiée
@app.on_event("startup")
async def startup_event():

View File

@@ -71,5 +71,29 @@ class ActionLog(ActionLogCreate):
id: int
timestamp: datetime
class Config:
from_attributes = True
class ProxmoxHostBase(BaseModel):
name: str
description: Optional[str] = None
ip_address: str
mac_address: str
proxmox_host: str
proxmox_username: str
proxmox_password: str
proxmox_port: int = 8006
verify_ssl: bool = True
shutdown_endpoint: Optional[str] = None
class ProxmoxHostCreate(ProxmoxHostBase):
pass
class ProxmoxHost(ProxmoxHostBase):
id: int
is_online: bool
last_ping: Optional[datetime] = None
created_at: datetime
class Config:
from_attributes = True

View File

@@ -79,4 +79,17 @@ class LoggingService:
message=message,
target_id=server_id,
target_name=server_name
)
@staticmethod
def log_host_action(db: Session, action: str, host_id: int, host_name: str, success: bool, message: str):
"""Log Proxmox host actions (create/update/delete/wake/shutdown)"""
LoggingService.log_action(
db=db,
action_type="host",
action=action,
success=success,
message=message,
target_id=host_id,
target_name=host_name
)

View File

@@ -0,0 +1,253 @@
import asyncio
import subprocess
import requests
from typing import List, Optional
from sqlalchemy.orm import Session
from datetime import datetime
from proxmoxer import ProxmoxAPI
from app.database import ProxmoxHost
from app.models.schemas import ProxmoxVM
from app.services.logging_service import LoggingService
import logging
logger = logging.getLogger(__name__)
class ProxmoxHostService:
def __init__(self, host_config: ProxmoxHost):
self.host_config = host_config
self._proxmox = None
def _get_proxmox_connection(self):
if not self._proxmox:
try:
self._proxmox = ProxmoxAPI(
self.host_config.proxmox_host,
user=self.host_config.proxmox_username,
password=self.host_config.proxmox_password,
port=self.host_config.proxmox_port,
verify_ssl=self.host_config.verify_ssl,
timeout=10
)
except Exception as e:
logger.error(f"Failed to connect to Proxmox {self.host_config.proxmox_host}: {str(e)}")
raise ConnectionError(f"Cannot connect to Proxmox: {str(e)}")
return self._proxmox
@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_host(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
async def wake_host(self, db: Session) -> bool:
success = await self.send_wol_packet(self.host_config.mac_address)
# Log WOL action
LoggingService.log_host_action(
db=db,
action="wake",
host_id=self.host_config.id,
host_name=self.host_config.name,
success=success,
message=f"WOL packet {'sent successfully' if success else 'failed'} to {self.host_config.name} ({self.host_config.mac_address})"
)
return success
async def shutdown_host(self, db: Session) -> bool:
try:
# Try using shutdown endpoint if configured
if self.host_config.shutdown_endpoint:
try:
response = requests.post(
f"http://{self.host_config.ip_address}{self.host_config.shutdown_endpoint}",
timeout=10
)
success = response.status_code == 200
except Exception as e:
logger.error(f"Shutdown endpoint failed: {str(e)}")
success = False
else:
# Try Proxmox API shutdown (shutdown all VMs then the node)
proxmox = self._get_proxmox_connection()
nodes = proxmox.nodes.get()
success = True
for node_data in nodes:
node_name = node_data['node']
try:
# Stop all VMs/containers first
vms = await self.get_vms_for_node(node_name)
for vm in vms:
if vm.status == 'running':
await self.stop_vm(node_name, vm.vmid, vm.type)
# Then shutdown the node
proxmox.nodes(node_name).status.post(command='shutdown')
except Exception as e:
logger.error(f"Failed to shutdown node {node_name}: {str(e)}")
success = False
# Log shutdown action
LoggingService.log_host_action(
db=db,
action="shutdown",
host_id=self.host_config.id,
host_name=self.host_config.name,
success=success,
message=f"Host {self.host_config.name} {'shutdown initiated' if success else 'shutdown failed'}"
)
return success
except Exception as e:
logger.error(f"Host shutdown failed: {str(e)}")
LoggingService.log_host_action(
db=db,
action="shutdown",
host_id=self.host_config.id,
host_name=self.host_config.name,
success=False,
message=f"Host shutdown failed: {str(e)}"
)
return False
async def test_proxmox_connection(self) -> bool:
try:
proxmox = self._get_proxmox_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_proxmox_connection()
return proxmox.nodes.get()
except Exception as e:
logger.error(f"Failed to get nodes: {str(e)}")
return []
async def get_vms(self) -> List[ProxmoxVM]:
try:
proxmox = self._get_proxmox_connection()
vms = []
nodes_list = await self.get_nodes()
nodes = [n['node'] for n in nodes_list]
for node_name in nodes:
vms.extend(await self.get_vms_for_node(node_name))
return vms
except Exception as e:
logger.error(f"Failed to get VMs: {str(e)}")
return []
async def get_vms_for_node(self, node_name: str) -> List[ProxmoxVM]:
try:
proxmox = self._get_proxmox_connection()
vms = []
# Get QEMU VMs
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'
))
except Exception as e:
logger.error(f"Failed to get QEMU VMs from node {node_name}: {str(e)}")
# Get LXC Containers
try:
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 LXC containers from node {node_name}: {str(e)}")
return vms
except Exception as e:
logger.error(f"Failed to get VMs for node {node_name}: {str(e)}")
return []
async def start_vm(self, node: str, vmid: str, vm_type: str = 'qemu') -> bool:
try:
proxmox = self._get_proxmox_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_proxmox_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
@staticmethod
async def check_all_hosts_status(db: Session) -> None:
hosts = db.query(ProxmoxHost).all()
tasks = []
for host in hosts:
tasks.append(ProxmoxHostService.ping_host(host.ip_address))
results = await asyncio.gather(*tasks)
for host, is_online in zip(hosts, results):
host.is_online = is_online
host.last_ping = datetime.utcnow()
db.commit()

View File

@@ -1,114 +0,0 @@
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

99
backend/cleanup_old_tables.py Executable file
View File

@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Script pour supprimer les anciennes tables après migration réussie
⚠️ ATTENTION: Cette opération est irréversible !
"""
import sys
import os
from sqlalchemy import create_engine, text
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db")
def cleanup_old_tables():
print("🗑️ Nettoyage des anciennes tables")
print("=" * 50)
print("⚠️ ATTENTION: Cette opération est IRRÉVERSIBLE !")
print("⚠️ Assurez-vous que:")
print(" 1. La migration vers ProxmoxHost a réussi")
print(" 2. L'application fonctionne correctement avec /api/hosts")
print(" 3. Vous avez fait une sauvegarde de zebra.db")
response = input("\n❓ Êtes-vous sûr de vouloir supprimer les anciennes tables ? (oui/NON): ").strip()
if response.lower() != 'oui':
print("❌ Opération annulée.")
return False
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
try:
with engine.connect() as conn:
# Vérifier que la nouvelle table existe et contient des données
result = conn.execute(text("SELECT COUNT(*) FROM proxmox_hosts")).scalar()
if result == 0:
print("❌ La table 'proxmox_hosts' est vide!")
print(" Lancez d'abord la migration avec: python migrate_to_unified_hosts.py")
return False
print(f"✅ Table 'proxmox_hosts' contient {result} hosts")
# Vérifier si les anciennes tables existent encore
tables_to_drop = []
try:
conn.execute(text("SELECT COUNT(*) FROM servers")).scalar()
tables_to_drop.append("servers")
print("📋 Table 'servers' trouvée")
except:
pass
try:
conn.execute(text("SELECT COUNT(*) FROM proxmox_clusters")).scalar()
tables_to_drop.append("proxmox_clusters")
print("📋 Table 'proxmox_clusters' trouvée")
except:
pass
if not tables_to_drop:
print(" Aucune ancienne table à supprimer.")
return True
print(f"\n🗑️ Tables à supprimer: {', '.join(tables_to_drop)}")
final_confirm = input("❓ Confirmer la suppression ? (oui/NON): ").strip()
if final_confirm.lower() != 'oui':
print("❌ Opération annulée.")
return False
# Supprimer les tables
for table in tables_to_drop:
conn.execute(text(f"DROP TABLE IF EXISTS {table}"))
print(f" ✅ Table '{table}' supprimée")
# Supprimer aussi les index associés
try:
conn.execute(text("DROP INDEX IF EXISTS ix_servers_id"))
conn.execute(text("DROP INDEX IF EXISTS ix_servers_name"))
conn.execute(text("DROP INDEX IF EXISTS ix_proxmox_clusters_id"))
conn.execute(text("DROP INDEX IF EXISTS ix_proxmox_clusters_name"))
print(" ✅ Index associés supprimés")
except:
pass
conn.commit()
print(f"\n🎉 Nettoyage terminé avec succès!")
print(f" - {len(tables_to_drop)} tables supprimées")
print(f" - La nouvelle architecture unifiée est maintenant en place")
return True
except Exception as e:
print(f"❌ Erreur lors du nettoyage: {e}")
return False
def main():
success = cleanup_old_tables()
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,350 @@
#!/usr/bin/env python3
"""
Script de migration pour fusionner les tables 'servers' et 'proxmox_clusters'
vers la nouvelle table unifiée 'proxmox_hosts'
"""
import sys
import os
import json
from datetime import datetime
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
# Ajouter le dossier app au path pour les imports
sys.path.append(os.path.join(os.path.dirname(__file__), 'app'))
from app.database import Base, Server, ProxmoxCluster, ProxmoxHost, ActionLog
from app.services.logging_service import LoggingService
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./zebra.db")
def create_backup(engine):
"""Créer une sauvegarde des tables existantes"""
print("📦 Création des sauvegardes...")
with engine.connect() as conn:
# Backup servers table
servers_backup = conn.execute(text("SELECT * FROM servers")).fetchall()
with open(f"backup_servers_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", 'w') as f:
servers_data = []
for row in servers_backup:
servers_data.append({
'id': row[0],
'name': row[1],
'ip_address': row[2],
'mac_address': row[3],
'description': row[4],
'is_online': row[5],
'last_ping': str(row[6]) if row[6] else None,
'created_at': str(row[7])
})
json.dump(servers_data, f, indent=2)
print(f" ✅ Sauvegardé {len(servers_data)} serveurs")
# Backup proxmox_clusters table
try:
clusters_backup = conn.execute(text("SELECT * FROM proxmox_clusters")).fetchall()
with open(f"backup_proxmox_clusters_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", 'w') as f:
clusters_data = []
for row in clusters_backup:
clusters_data.append({
'id': row[0],
'name': row[1],
'host': row[2],
'username': row[3],
'password': row[4],
'port': row[5],
'verify_ssl': row[6],
'created_at': str(row[7])
})
json.dump(clusters_data, f, indent=2)
print(f" ✅ Sauvegardé {len(clusters_data)} clusters Proxmox")
except Exception as e:
print(f" Aucun cluster Proxmox à sauvegarder: {e}")
clusters_data = []
return len(servers_data), len(clusters_data)
def migrate_servers_to_hosts(session):
"""Migrer les serveurs existants vers la table hosts en demandant les infos Proxmox"""
print("\n🔄 Migration des serveurs vers hosts...")
servers = session.query(Server).all()
migrated_hosts = []
for server in servers:
print(f"\n📋 Migration du serveur: {server.name}")
print(f" IP: {server.ip_address}")
print(f" MAC: {server.mac_address}")
# Demander les informations Proxmox pour ce serveur
print(f" Pour ce serveur, veuillez fournir les informations Proxmox:")
# Utiliser l'IP du serveur comme host Proxmox par défaut
proxmox_host = input(f" Host Proxmox (défaut: {server.ip_address}): ").strip()
if not proxmox_host:
proxmox_host = server.ip_address
proxmox_username = input(" Nom d'utilisateur Proxmox: ").strip()
if not proxmox_username:
print(" ⚠️ Nom d'utilisateur requis! Utilisation de 'root' par défaut")
proxmox_username = "root"
proxmox_password = input(" Mot de passe Proxmox: ").strip()
if not proxmox_password:
print(" ⚠️ Mot de passe requis! Utilisation de 'password' par défaut")
proxmox_password = "password"
proxmox_port = input(" Port Proxmox (défaut: 8006): ").strip()
if not proxmox_port:
proxmox_port = 8006
else:
try:
proxmox_port = int(proxmox_port)
except ValueError:
proxmox_port = 8006
verify_ssl_input = input(" Vérifier SSL? (o/N): ").strip().lower()
verify_ssl = verify_ssl_input in ['o', 'oui', 'y', 'yes']
shutdown_endpoint = input(" Endpoint de shutdown (optionnel): ").strip()
if not shutdown_endpoint:
shutdown_endpoint = None
# Créer le nouveau host unifié
host = ProxmoxHost(
name=server.name,
description=server.description,
ip_address=server.ip_address,
mac_address=server.mac_address,
proxmox_host=proxmox_host,
proxmox_username=proxmox_username,
proxmox_password=proxmox_password,
proxmox_port=proxmox_port,
verify_ssl=verify_ssl,
is_online=server.is_online,
last_ping=server.last_ping,
shutdown_endpoint=shutdown_endpoint,
created_at=server.created_at
)
session.add(host)
session.commit()
session.refresh(host)
# Logger la migration
LoggingService.log_host_action(
db=session,
action="migrate",
host_id=host.id,
host_name=host.name,
success=True,
message=f"Serveur '{server.name}' migré vers host unifié (ancien ID serveur: {server.id})"
)
migrated_hosts.append((server.id, host.id, server.name))
print(f" ✅ Migré vers host ID {host.id}")
return migrated_hosts
def migrate_clusters_to_hosts(session):
"""Migrer les clusters Proxmox existants vers la table hosts en demandant les infos WOL"""
print("\n🔄 Migration des clusters Proxmox vers hosts...")
clusters = session.query(ProxmoxCluster).all()
migrated_hosts = []
for cluster in clusters:
print(f"\n📋 Migration du cluster: {cluster.name}")
print(f" Host Proxmox: {cluster.host}:{cluster.port}")
print(f" Utilisateur: {cluster.username}")
# Demander les informations WOL pour ce cluster
print(f" Pour ce cluster, veuillez fournir les informations Wake-on-LAN:")
# Utiliser le host du cluster comme IP par défaut
ip_address = input(f" Adresse IP (défaut: {cluster.host}): ").strip()
if not ip_address:
ip_address = cluster.host
mac_address = input(" Adresse MAC: ").strip()
if not mac_address:
print(" ⚠️ Adresse MAC requise! Utilisation de '00:00:00:00:00:00' par défaut")
mac_address = "00:00:00:00:00:00"
description = input(" Description (optionnel): ").strip()
shutdown_endpoint = input(" Endpoint de shutdown (optionnel): ").strip()
if not shutdown_endpoint:
shutdown_endpoint = None
# Créer le nouveau host unifié
host = ProxmoxHost(
name=cluster.name,
description=description if description else f"Host Proxmox migré du cluster {cluster.name}",
ip_address=ip_address,
mac_address=mac_address,
proxmox_host=cluster.host,
proxmox_username=cluster.username,
proxmox_password=cluster.password,
proxmox_port=cluster.port,
verify_ssl=cluster.verify_ssl,
is_online=False, # Statut inconnu pour les anciens clusters
last_ping=None,
shutdown_endpoint=shutdown_endpoint,
created_at=cluster.created_at
)
session.add(host)
session.commit()
session.refresh(host)
# Logger la migration
LoggingService.log_host_action(
db=session,
action="migrate",
host_id=host.id,
host_name=host.name,
success=True,
message=f"Cluster Proxmox '{cluster.name}' migré vers host unifié (ancien ID cluster: {cluster.id})"
)
migrated_hosts.append((cluster.id, host.id, cluster.name))
print(f" ✅ Migré vers host ID {host.id}")
return migrated_hosts
def update_action_logs(session, server_migrations, cluster_migrations):
"""Mettre à jour les logs d'actions pour pointer vers les nouveaux hosts"""
print("\n📝 Mise à jour des logs d'actions...")
# Map ancien server_id -> nouveau host_id
server_id_map = {old_id: new_id for old_id, new_id, _ in server_migrations}
cluster_id_map = {old_id: new_id for old_id, new_id, _ in cluster_migrations}
# Mettre à jour les logs de serveurs
for old_server_id, new_host_id in server_id_map.items():
logs = session.query(ActionLog).filter(
ActionLog.action_type == "server",
ActionLog.target_id == old_server_id
).all()
for log in logs:
log.action_type = "host"
log.target_id = new_host_id
# Ajouter une note dans le message
if not log.message.endswith(" (migré)"):
log.message += " (migré)"
if logs:
print(f" ✅ Mis à jour {len(logs)} logs pour l'ancien serveur {old_server_id}")
# Mettre à jour les logs de clusters Proxmox
for old_cluster_id, new_host_id in cluster_id_map.items():
logs = session.query(ActionLog).filter(
ActionLog.action_type == "proxmox",
ActionLog.target_id == old_cluster_id,
ActionLog.action.in_(["create", "delete", "update"]) # Actions sur le cluster lui-même
).all()
for log in logs:
log.action_type = "host"
log.target_id = new_host_id
if not log.message.endswith(" (migré)"):
log.message += " (migré)"
if logs:
print(f" ✅ Mis à jour {len(logs)} logs pour l'ancien cluster {old_cluster_id}")
session.commit()
def main():
print("🚀 Migration vers le modèle unifié ProxmoxHost")
print("=" * 50)
# Créer la connexion à la base de données
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Créer les tables si elles n'existent pas
Base.metadata.create_all(bind=engine)
session = SessionLocal()
try:
# Vérifier s'il y a déjà des hosts
existing_hosts = session.query(ProxmoxHost).count()
if existing_hosts > 0:
print(f"⚠️ Il y a déjà {existing_hosts} hosts dans la base de données.")
response = input("Voulez-vous continuer la migration? (o/N): ").strip().lower()
if response not in ['o', 'oui', 'y', 'yes']:
print("Migration annulée.")
return
# Sauvegarder les données existantes
servers_count, clusters_count = create_backup(engine)
if servers_count == 0 and clusters_count == 0:
print(" Aucune donnée à migrer.")
return
print(f"\n📊 Données à migrer:")
print(f" - {servers_count} serveurs")
print(f" - {clusters_count} clusters Proxmox")
response = input("\nVoulez-vous continuer la migration? (o/N): ").strip().lower()
if response not in ['o', 'oui', 'y', 'yes']:
print("Migration annulée.")
return
# Migration des serveurs vers hosts
server_migrations = []
if servers_count > 0:
server_migrations = migrate_servers_to_hosts(session)
# Migration des clusters vers hosts
cluster_migrations = []
if clusters_count > 0:
cluster_migrations = migrate_clusters_to_hosts(session)
# Mettre à jour les logs d'actions
if server_migrations or cluster_migrations:
update_action_logs(session, server_migrations, cluster_migrations)
print(f"\n✅ Migration terminée avec succès!")
print(f" - {len(server_migrations)} serveurs migrés")
print(f" - {len(cluster_migrations)} clusters migrés")
# Option pour supprimer les anciennes tables
if server_migrations or cluster_migrations:
print(f"\n🗑️ Les anciennes données sont toujours présentes.")
response = input("Voulez-vous supprimer les anciennes tables 'servers' et 'proxmox_clusters'? (o/N): ").strip().lower()
if response in ['o', 'oui', 'y', 'yes']:
with engine.connect() as conn:
if servers_count > 0:
conn.execute(text("DROP TABLE servers"))
print(" ✅ Table 'servers' supprimée")
if clusters_count > 0:
conn.execute(text("DROP TABLE proxmox_clusters"))
print(" ✅ Table 'proxmox_clusters' supprimée")
conn.commit()
print(" ⚠️ Anciennes tables supprimées. Migration irréversible!")
else:
print(" Anciennes tables conservées pour rollback si nécessaire")
print(f"\n🎉 Migration vers le modèle unifié terminée!")
print(f"Vous pouvez maintenant utiliser /api/hosts pour gérer vos hosts Proxmox")
except Exception as e:
print(f"❌ Erreur lors de la migration: {e}")
session.rollback()
return 1
finally:
session.close()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,9 +1,9 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
pydantic==2.5.0
python-multipart==0.0.6
fastapi==0.115.0
uvicorn[standard]==0.30.0
sqlalchemy==2.0.35
pydantic==2.9.0
python-multipart==0.0.10
proxmoxer==2.0.1
requests==2.31.0
requests==2.32.0
wakeonlan==3.1.0
httpx==0.25.2
httpx==0.27.0

91
backend/test_api.py Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env python3
"""
Test basique de l'API unifiée hosts
"""
import requests
import json
import sys
def test_api():
base_url = "http://localhost:8000/api"
print("🧪 Test de l'API Zebra Power - Hosts unifiés")
print("=" * 50)
try:
# Test 1: Health check
print("\n1. Test de santé de l'API...")
response = requests.get(f"{base_url}/health", timeout=5)
if response.status_code == 200:
print(" ✅ API en ligne")
else:
print(f" ❌ API hors service ({response.status_code})")
return False
# Test 2: Liste des hosts (vide au début)
print("\n2. Test de récupération des hosts...")
response = requests.get(f"{base_url}/hosts", timeout=5)
if response.status_code == 200:
hosts = response.json()
print(f"{len(hosts)} hosts trouvés")
else:
print(f" ❌ Erreur récupération hosts ({response.status_code})")
return False
# Test 3: Créer un host de test
print("\n3. Test de création d'un host...")
test_host = {
"name": "Host Test",
"description": "Host de test pour l'API",
"ip_address": "192.168.1.100",
"mac_address": "00:11:22:33:44:55",
"proxmox_host": "192.168.1.100",
"proxmox_username": "root",
"proxmox_password": "password",
"proxmox_port": 8006,
"verify_ssl": False,
"shutdown_endpoint": "/api/shutdown"
}
# Note: Ce test échouera car nous n'avons pas de Proxmox réel
response = requests.post(f"{base_url}/hosts", json=test_host, timeout=10)
if response.status_code == 200:
created_host = response.json()
print(f" ✅ Host créé avec ID {created_host['id']}")
# Test 4: Récupérer le host créé
print("\n4. Test de récupération du host créé...")
response = requests.get(f"{base_url}/hosts/{created_host['id']}", timeout=5)
if response.status_code == 200:
host_data = response.json()
print(f" ✅ Host récupéré: {host_data['name']}")
# Test 5: Supprimer le host de test
print("\n5. Test de suppression du host...")
response = requests.delete(f"{base_url}/hosts/{created_host['id']}", timeout=5)
if response.status_code == 200:
print(" ✅ Host supprimé avec succès")
else:
print(f" ❌ Erreur suppression host ({response.status_code})")
else:
print(f" ❌ Erreur récupération host ({response.status_code})")
else:
print(f" ❌ Échec création host ({response.status_code})")
print(f" Raison: {response.text}")
# C'est normal si pas de Proxmox réel
print(f"\n🎉 Tests terminés - API fonctionnelle")
return True
except requests.exceptions.ConnectionError:
print(" ❌ Impossible de se connecter à l'API")
print(" Vérifiez que le serveur backend est démarré sur http://localhost:8000")
return False
except Exception as e:
print(f" ❌ Erreur inattendue: {e}")
return False
if __name__ == "__main__":
success = test_api()
sys.exit(0 if success else 1)