refact: use only server
This commit is contained in:
@@ -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
30
backend/Dockerfile.dev
Normal 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
247
backend/app/api/hosts.py
Normal 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
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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"}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
253
backend/app/services/proxmox_host_service.py
Normal file
253
backend/app/services/proxmox_host_service.py
Normal 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()
|
||||
@@ -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
99
backend/cleanup_old_tables.py
Executable 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())
|
||||
350
backend/migrate_to_unified_hosts.py
Executable file
350
backend/migrate_to_unified_hosts.py
Executable 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())
|
||||
@@ -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
91
backend/test_api.py
Executable 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)
|
||||
Reference in New Issue
Block a user