Skip to content

Commit 81b6ccd

Browse files
committed
fix(backend): add vnc-port and screenshot endpoints to slave agent
1 parent 9bda16f commit 81b6ccd

File tree

4 files changed

+83
-17
lines changed

4 files changed

+83
-17
lines changed

backend/app/routes/slave_agent.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
33
All endpoints require X-Slave-Token authentication.
44
"""
5-
from fastapi import APIRouter, Depends, status
5+
from fastapi import APIRouter, Depends, Response, status
66
from app.models.vm import VmCreate, VmRead
77
from app.services.vm_service import VmService
88
from app.services.host_service import HostService
9+
from app.services.vm_screenshot import capture_screenshot
10+
from app.utils.vnc import get_vnc_port
911
from app.utils.slave_auth import require_slave_token
1012

1113
router = APIRouter()
@@ -60,6 +62,30 @@ def delete_vm(vm_id: str):
6062
VmService.remove_vm(vm_id)
6163

6264

65+
@router.get(
66+
"/vms/{vm_id}/vnc-port",
67+
status_code=status.HTTP_200_OK,
68+
dependencies=[Depends(require_slave_token)],
69+
)
70+
def get_vm_vnc_port(vm_id: str):
71+
port = get_vnc_port(vm_id)
72+
return {"port": port}
73+
74+
75+
@router.get(
76+
"/vms/{vm_id}/screenshot",
77+
status_code=status.HTTP_200_OK,
78+
dependencies=[Depends(require_slave_token)],
79+
)
80+
def get_vm_screenshot(vm_id: str):
81+
jpeg_bytes = capture_screenshot(vm_id)
82+
return Response(
83+
content=jpeg_bytes,
84+
media_type="image/jpeg",
85+
headers={"Cache-Control": "no-store"},
86+
)
87+
88+
6389
@router.get(
6490
"/host/info",
6591
status_code=status.HTTP_200_OK,

backend/app/routes/tunnel.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from app.utils.crypto import decrypt_secret
33
from app.utils.auth import decode_access_token, user_has_policy
44
from app.services.guacamole import build_instruction, guacd_handshake, read_instruction
5+
from app.services.vm_service import VmService
56
from app.orm.vm_credential import VmCredentialORM
67
from app.orm.user import UserORM
78
from app.core.config import engine, GUACD_HOST, GUACD_PORT, VNC_HOST
@@ -144,47 +145,65 @@ async def vm_tunnel(
144145
await websocket.close(code=4001, reason="Provide credential or vm_id+token")
145146
return
146147

147-
try:
148-
vnc_port = await asyncio.to_thread(get_vnc_port, resolved_vm_id)
149-
except Exception as exc:
150-
detail = getattr(exc, "detail", str(exc))
151-
await websocket.close(code=4002, reason=detail)
152-
return
148+
slave = await asyncio.to_thread(VmService._get_slave_for_vm, resolved_vm_id)
149+
150+
if slave:
151+
from app.services.slave_client import slave_get_vnc_port
152+
try:
153+
vnc_port = await asyncio.to_thread(slave_get_vnc_port, slave, resolved_vm_id)
154+
except Exception as exc:
155+
detail = getattr(exc, "detail", str(exc))
156+
await websocket.close(code=4002, reason=detail)
157+
return
158+
guacd_host = slave.hostname
159+
guacd_port = GUACD_PORT
160+
vnc_host = "127.0.0.1"
161+
else:
162+
try:
163+
vnc_port = await asyncio.to_thread(get_vnc_port, resolved_vm_id)
164+
except Exception as exc:
165+
detail = getattr(exc, "detail", str(exc))
166+
await websocket.close(code=4002, reason=detail)
167+
return
168+
guacd_host = GUACD_HOST
169+
guacd_port = GUACD_PORT
170+
vnc_host = VNC_HOST
153171

154172
logger.warning(
155-
"Tunnel: vm_id=%s vnc=%s:%s guacd=%s:%s",
156-
resolved_vm_id, VNC_HOST, vnc_port, GUACD_HOST, GUACD_PORT,
173+
"Tunnel: vm_id=%s vnc=%s:%s guacd=%s:%s slave=%s",
174+
resolved_vm_id, vnc_host, vnc_port, guacd_host, guacd_port,
175+
slave.hostname if slave else None,
157176
)
158177

159178
await websocket.accept(subprotocol="guacamole")
160179

161180
try:
162-
reader, writer = await asyncio.open_connection(GUACD_HOST, GUACD_PORT)
181+
reader, writer = await asyncio.open_connection(guacd_host, guacd_port)
163182
except Exception as exc:
164183
await websocket.close(
165184
code=1011,
166-
reason=f"Cannot connect to guacd {GUACD_HOST}:{GUACD_PORT}: {exc}",
185+
reason=f"Cannot connect to guacd {guacd_host}:{guacd_port}: {exc}",
167186
)
168187
return
169188

170189
try:
171190
first_instruction = await guacd_handshake(
172191
reader,
173192
writer,
174-
vnc_host=VNC_HOST,
193+
vnc_host=vnc_host,
175194
vnc_port=vnc_port,
176195
width=width,
177196
height=height,
178197
)
179198
logger.warning(
180-
"Tunnel connected via configured vnc host=%s port=%s",
181-
VNC_HOST,
199+
"Tunnel connected via vnc host=%s port=%s",
200+
vnc_host,
182201
vnc_port,
183202
)
184203
except Exception as exc:
185204
logger.warning(
186-
"Tunnel VNC connect failed for configured host=%s port=%s: %s",
187-
VNC_HOST,
205+
"Tunnel VNC connect failed for host=%s port=%s: %s",
206+
vnc_host,
188207
vnc_port,
189208
exc,
190209
)

backend/app/routes/vm.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ async def get_vm_screenshot(vm_id: str, token: str = Query(...)):
3838
if not user_has_policy(user, "vms:get"):
3939
raise HTTPException(status.HTTP_403_FORBIDDEN,
4040
"Missing vms:get policy")
41-
jpeg_bytes = await asyncio.to_thread(capture_screenshot, vm_id)
41+
slave = await asyncio.to_thread(VmService._get_slave_for_vm, vm_id)
42+
if slave:
43+
from app.services.slave_client import slave_get_screenshot
44+
jpeg_bytes = await asyncio.to_thread(slave_get_screenshot, slave, vm_id)
45+
else:
46+
jpeg_bytes = await asyncio.to_thread(capture_screenshot, vm_id)
4247
return Response(
4348
content=jpeg_bytes,
4449
media_type="image/jpeg",

backend/app/services/slave_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ def slave_delete_vm(slave: SlaveORM, vm_id: str) -> dict:
6161
return slave_request(slave, "DELETE", f"/vms/{vm_id}")
6262

6363

64+
def slave_get_vnc_port(slave: SlaveORM, vm_id: str) -> int:
65+
"""Get the VNC port for a VM on a slave node."""
66+
data = slave_request(slave, "GET", f"/vms/{vm_id}/vnc-port")
67+
return data["port"]
68+
69+
70+
def slave_get_screenshot(slave: SlaveORM, vm_id: str) -> bytes:
71+
"""Get a screenshot from a VM on a slave node."""
72+
url = f"{_slave_base_url(slave)}/vms/{vm_id}/screenshot"
73+
headers = _slave_headers(slave)
74+
with httpx.Client(timeout=TIMEOUT) as client:
75+
response = client.get(url, headers=headers)
76+
response.raise_for_status()
77+
return response.content
78+
79+
6480
def slave_get_host_info(slave: SlaveORM) -> dict:
6581
"""Get host info from a slave node."""
6682
return slave_request(slave, "GET", "/host/info")

0 commit comments

Comments
 (0)