Configure VM provisioning with ZFS mirror disks and virtual TPM 2.0 module
This commit is contained in:
+2
-2
@@ -54,10 +54,10 @@ class Settings:
|
|||||||
|
|
||||||
# Storage paths
|
# Storage paths
|
||||||
iso_storage_path: str = os.getenv(
|
iso_storage_path: str = os.getenv(
|
||||||
"ISO_STORAGE_PATH", "/var/lib/vz/template/iso"
|
"ISO_STORAGE_PATH", _creds.get("ISO_STORAGE_PATH", "/opt/sovereign-orchestrator/data/isos")
|
||||||
)
|
)
|
||||||
build_output_path: str = os.getenv(
|
build_output_path: str = os.getenv(
|
||||||
"BUILD_OUTPUT_PATH", "/tmp/sovereign-orchestrator/builds"
|
"BUILD_OUTPUT_PATH", _creds.get("BUILD_OUTPUT_PATH", "/opt/sovereign-orchestrator/data/builds")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
|
|||||||
+4
-3
@@ -220,10 +220,11 @@ async def _run_build(build: BuildStatus) -> None:
|
|||||||
f"proxmox-auto-install-assistant exited with code {return_code}"
|
f"proxmox-auto-install-assistant exited with code {return_code}"
|
||||||
)
|
)
|
||||||
|
|
||||||
build.iso_filename = output_filename
|
prepared_filename = output_filename.replace(".iso", "-auto-from-iso.iso")
|
||||||
|
build.iso_filename = prepared_filename
|
||||||
build.state = BuildState.COMPLETED
|
build.state = BuildState.COMPLETED
|
||||||
build.log(f"Build completed: {output_filename}")
|
build.log(f"Build completed: {prepared_filename}")
|
||||||
logger.info("Build %s completed: %s", build.id, output_path)
|
logger.info("Build %s completed: %s", build.id, work_dir / prepared_filename)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
build.state = BuildState.FAILED
|
build.state = BuildState.FAILED
|
||||||
|
|||||||
+7
-5
@@ -292,13 +292,15 @@ async def deploy_vm(config: DeployConfig) -> DeployResult:
|
|||||||
"cores": config.cores,
|
"cores": config.cores,
|
||||||
"memory": config.memory,
|
"memory": config.memory,
|
||||||
"ostype": "l26",
|
"ostype": "l26",
|
||||||
"scsihw": "virtio-scsi-single",
|
"sata0": f"{config.storage}:{config.disk_size.rstrip('G')}",
|
||||||
"scsi0": f"{config.storage}:{config.disk_size.rstrip('G')},iothread=1",
|
"sata1": f"{config.storage}:1000",
|
||||||
|
"sata2": f"{config.storage}:1000",
|
||||||
"ide2": f"{iso_volid},media=cdrom",
|
"ide2": f"{iso_volid},media=cdrom",
|
||||||
"boot": "order=ide2;scsi0",
|
"boot": f"order=sata0;ide2",
|
||||||
"net0": "virtio,bridge=vmbr0",
|
"net0": f"virtio,bridge={config.bridge}",
|
||||||
"serial0": "socket",
|
"serial0": "socket",
|
||||||
"vga": "serial0",
|
"vga": "std",
|
||||||
|
"tpmstate0": f"{config.storage}:4",
|
||||||
}
|
}
|
||||||
|
|
||||||
create_upid = await proxmox.create_vm(node, vmid, vm_config)
|
create_upid = await proxmox.create_vm(node, vmid, vm_config)
|
||||||
|
|||||||
+4
-1
@@ -93,7 +93,7 @@ class DeployConfig(BaseModel):
|
|||||||
default=8192, ge=512, description="Memory in MiB"
|
default=8192, ge=512, description="Memory in MiB"
|
||||||
)
|
)
|
||||||
disk_size: str = Field(
|
disk_size: str = Field(
|
||||||
default="64G", description="Root disk size (e.g. 64G)"
|
default="128G", description="Root disk size (e.g. 128G)"
|
||||||
)
|
)
|
||||||
build_id: Optional[str] = Field(
|
build_id: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
@@ -105,6 +105,9 @@ class DeployConfig(BaseModel):
|
|||||||
iso_storage: str = Field(
|
iso_storage: str = Field(
|
||||||
default="local", description="Proxmox storage containing ISOs"
|
default="local", description="Proxmox storage containing ISOs"
|
||||||
)
|
)
|
||||||
|
bridge: str = Field(
|
||||||
|
default="vmbr1", description="Network bridge to attach the VM to"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Add app directory to sys.path so we can import from app
|
||||||
|
sys.path.append(str(Path(__file__).parent))
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.proxmox_client import proxmox
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
node = "dl380-0"
|
||||||
|
vmid = 900
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
vmid = int(sys.argv[1])
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
node = sys.argv[2]
|
||||||
|
|
||||||
|
print(f"Requesting termproxy ticket for VM {vmid} on node {node}...")
|
||||||
|
try:
|
||||||
|
res = await proxmox._request("POST", f"/nodes/{node}/qemu/{vmid}/termproxy")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error requesting termproxy: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
port = res["port"]
|
||||||
|
ticket = res["ticket"]
|
||||||
|
user = res["user"]
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
# Construct ws url
|
||||||
|
host_clean = settings.proxmox_host.replace("https://", "").replace("http://", "").split("/")[0]
|
||||||
|
ws_url = f"wss://{host_clean}/api2/json/nodes/{node}/qemu/{vmid}/vncwebsocket?port={port}&vncticket={quote(ticket)}"
|
||||||
|
|
||||||
|
print(f"Connecting to websocket: {ws_url}")
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
ssl_context = ssl._create_unverified_context()
|
||||||
|
|
||||||
|
# Proxmox websocket protocol requires sending the ticket to authenticate
|
||||||
|
try:
|
||||||
|
async with websockets.connect(
|
||||||
|
ws_url,
|
||||||
|
ssl=ssl_context,
|
||||||
|
subprotocols=["binary"]
|
||||||
|
) as ws:
|
||||||
|
# Handshake format: user:ticket\n
|
||||||
|
handshake = f"{user}:{ticket}\n"
|
||||||
|
await ws.send(handshake)
|
||||||
|
print("Handshake sent, reading console stream (Ctrl+C to exit)...")
|
||||||
|
|
||||||
|
# Proxmox termproxy sends data as frame-wrapped or raw binary
|
||||||
|
while True:
|
||||||
|
msg = await ws.recv()
|
||||||
|
if isinstance(msg, bytes):
|
||||||
|
# Proxmox termproxy data format: 1 byte channel, then data
|
||||||
|
# Channel 0 is stdout, channel 1 is stderr
|
||||||
|
if len(msg) > 1:
|
||||||
|
sys.stdout.buffer.write(msg[1:])
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
else:
|
||||||
|
sys.stdout.write(msg)
|
||||||
|
sys.stdout.flush()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nExiting console reader.")
|
||||||
|
except websockets.exceptions.ConnectionClosed as e:
|
||||||
|
print(f"\nConnection closed: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -38,10 +38,14 @@ if ! command -v proxmox-auto-install-assistant &>/dev/null; then
|
|||||||
echo " -> Installing proxmox-auto-install-assistant from Proxmox repos..."
|
echo " -> Installing proxmox-auto-install-assistant from Proxmox repos..."
|
||||||
# Add Proxmox repository for the assistant tool
|
# Add Proxmox repository for the assistant tool
|
||||||
if [ ! -f /etc/apt/sources.list.d/proxmox.list ]; then
|
if [ ! -f /etc/apt/sources.list.d/proxmox.list ]; then
|
||||||
echo "deb [arch=amd64] http://download.proxmox.com/debian/pve trixie pve-no-subscription" \
|
CODENAME=$(grep VERSION_CODENAME /etc/os-release | cut -d= -f2 | tr -d '"')
|
||||||
|
if [ -z "$CODENAME" ]; then
|
||||||
|
CODENAME="bookworm"
|
||||||
|
fi
|
||||||
|
echo "deb [arch=amd64] http://download.proxmox.com/debian/pve $CODENAME pve-no-subscription" \
|
||||||
> /etc/apt/sources.list.d/proxmox.list
|
> /etc/apt/sources.list.d/proxmox.list
|
||||||
wget -qO /etc/apt/trusted.gpg.d/proxmox-release-trixie.gpg \
|
wget -qO "/etc/apt/trusted.gpg.d/proxmox-release-$CODENAME.gpg" \
|
||||||
http://download.proxmox.com/debian/proxmox-release-trixie.gpg 2>/dev/null || true
|
"http://download.proxmox.com/debian/proxmox-release-$CODENAME.gpg" 2>/dev/null || true
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
fi
|
fi
|
||||||
apt-get install -y -qq proxmox-auto-install-assistant 2>/dev/null || {
|
apt-get install -y -qq proxmox-auto-install-assistant 2>/dev/null || {
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
@@ -347,6 +347,6 @@
|
|||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user