798 lines
34 KiB
Python
798 lines
34 KiB
Python
#Worker.py
|
|
import functools
|
|
from logger import logger
|
|
from worker_tasks.error_types import SchedulingError
|
|
from worker_tasks.ovs_bridge_scanner import OVSBridgeScannerTask
|
|
from worker_tasks.pci_device_scanner import PCIDeviceScannerTask
|
|
|
|
from vncsession import VNCSession
|
|
from worker_tasks.container import ContainerTask
|
|
from worker_tasks.dns import DnsTask
|
|
from worker_tasks.file_presence import FilePresenceTask
|
|
from worker_tasks.ping import PingTask
|
|
from worker_tasks.ping import PingTask
|
|
from worker_tasks.report import ReportTask
|
|
from worker_tasks.ovs_sdn import OVS_SDN
|
|
from settings import settings # Import global settings
|
|
import json
|
|
import asyncio
|
|
import socketio
|
|
import threading
|
|
import time
|
|
class WorkerClient:
|
|
def __init__(self, event_queue=None, docker_monitor=None, libvirt_monitor=None):
|
|
"""Initializes the WorkerClient using global settings."""
|
|
try:
|
|
self.worker_id = settings.get_value("WORKER_ID")
|
|
self.worker_secret = settings.get_value("WORKER_SECRET")
|
|
self.server_url = settings.get_value("WEBSOCKET_SERVER_URL")
|
|
self.socketio_path = settings.get_value("WEBSOCKET_SERVER_PATH", "/ws").lstrip("/")
|
|
self.vnc_host = settings.get_value("VNC_HOST", "127.0.0.1")
|
|
self.debug_socketio = settings.get_value("DEBUG_SOCKETIO", False)
|
|
self.enable_websocket_ping_debug = settings.get_value("ENABLE_WEBSOCKET_PING_DEBUG", False)
|
|
# Store monitors for starting after join
|
|
self.docker_monitor = docker_monitor
|
|
self.libvirt_monitor = libvirt_monitor
|
|
# Libvirt config is now also potentially in settings, or keep defaults here
|
|
self.libvirt_config = {
|
|
"default_volume_path": settings.get_value("DEFAULT_VOLUME_PATH", "/tmp")
|
|
}
|
|
except KeyError as e:
|
|
logger.error(f"Failed to initialize WorkerClient: Missing required setting: {e}")
|
|
raise ValueError(f"Missing required setting for WorkerClient: {e}") from e
|
|
|
|
self.event_queue = event_queue
|
|
self.running = True
|
|
self.join_attempts = 0
|
|
self.max_join_attempts = 5
|
|
self.monitor_threads = [] # Changed from monitor_processes to monitor_threads
|
|
self.monitors_started = False
|
|
self.first_join_completed = False # Track if this is the first join
|
|
self.reconnection_in_progress = False # Track if we're in a reconnection cycle
|
|
|
|
|
|
if self.debug_socketio:
|
|
self.sio = socketio.AsyncClient(
|
|
logger=logger,
|
|
engineio_logger=logger,
|
|
reconnection=False # Disable automatic reconnection - we handle it manually
|
|
)
|
|
else:
|
|
self.sio = socketio.AsyncClient(
|
|
logger=False,
|
|
engineio_logger=False,
|
|
reconnection=False # Disable automatic reconnection - we handle it manually
|
|
)
|
|
self.joined_server = False
|
|
|
|
# Bind events
|
|
self.sio.on("connect", self.on_connect)
|
|
self.sio.on("connect_error", self.on_connect_error)
|
|
self.sio.on("disconnect", self.on_disconnect)
|
|
self.sio.on("task", self.handle_task)
|
|
self.sio.on("join_accept", self.on_join_accept)
|
|
self.sio.on("join_reject", self.on_join_reject)
|
|
self.sio.on("message", self.on_message)
|
|
# self.sio.on("*", self.debug_all_events)
|
|
|
|
|
|
self.sio.on("container-log",self.logs_retrieve)
|
|
|
|
|
|
self.log_stream_threads = {}
|
|
self.sio.on("start_container_log_stream", self.logs_stream_start)
|
|
self.sio.on("stop_container_log_stream", self.logs_stream_stop)
|
|
|
|
# VM console log streaming (mirrors the container log stream pattern)
|
|
self.vm_log_stream_threads = {}
|
|
self.sio.on("start_vm_log_stream", self.vm_logs_stream_start)
|
|
self.sio.on("stop_vm_log_stream", self.vm_logs_stream_stop)
|
|
|
|
self.terminal_sessions = {}
|
|
self.sio.on("start_terminal", self.start_terminal_session)
|
|
self.sio.on("terminal_input", self.terminal_input)
|
|
self.sio.on("stop_terminal", self.stop_terminal_session)
|
|
|
|
|
|
|
|
self.vnc_sessions = {}
|
|
self.sio.on("start_vnc_stream_on_worker", self.start_vnc_stream)
|
|
self.sio.on("stop_vnc_stream_on_worker", self.stop_vnc_stream)
|
|
self.sio.on("vnc_frame_from_novnc", self.vnc_frame_from_novnc) # Add this line
|
|
|
|
self.sio.on("worker_ping", self.handle_worker_ping)
|
|
|
|
# OVS Bridge Scanner
|
|
try:
|
|
self.ovs_scanner = OVSBridgeScannerTask(logger)
|
|
except ValueError as e:
|
|
logger.error(f"Failed to initialize OVS scanner: {e}")
|
|
self.ovs_scanner = None
|
|
|
|
# PCI Device Scanner
|
|
try:
|
|
self.pci_scanner = PCIDeviceScannerTask(logger)
|
|
except ValueError as e:
|
|
logger.error(f"Failed to initialize PCI scanner: {e}")
|
|
self.pci_scanner = None
|
|
|
|
# DNS Task Handler
|
|
try:
|
|
import docker
|
|
docker_client = docker.from_env()
|
|
self.dns_task = DnsTask(logger, docker_client, settings.get_value("WORKER_ID"))
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize DNS task handler: {e}")
|
|
self.dns_task = None
|
|
|
|
def start_monitors(self):
|
|
"""Start monitor threads if monitors are available."""
|
|
if self.docker_monitor:
|
|
try:
|
|
docker_thread = threading.Thread(
|
|
target=self.docker_monitor.run,
|
|
daemon=True,
|
|
name="DockerMonitor"
|
|
)
|
|
docker_thread.start()
|
|
self.monitor_threads.append(docker_thread)
|
|
logger.info("Started Docker monitor thread")
|
|
except Exception as e:
|
|
logger.error(f"Failed to start Docker monitor thread: {e}")
|
|
if self.libvirt_monitor:
|
|
try:
|
|
libvirt_thread = threading.Thread(
|
|
target=self.libvirt_monitor.run,
|
|
daemon=True,
|
|
name="LibvirtMonitor"
|
|
)
|
|
libvirt_thread.start()
|
|
self.monitor_threads.append(libvirt_thread)
|
|
logger.info("Started Libvirt monitor thread")
|
|
except Exception as e:
|
|
logger.error(f"Failed to start Libvirt monitor thread: {e}")
|
|
|
|
def stop_monitors(self):
|
|
"""Stop all monitor threads and monitors."""
|
|
# Set stop flags first to signal threads to exit
|
|
if self.docker_monitor:
|
|
logger.info("Stopping Docker Monitor")
|
|
try:
|
|
self.docker_monitor.stop()
|
|
except Exception as e:
|
|
logger.error(f"Error stopping Docker monitor: {e}")
|
|
if self.libvirt_monitor:
|
|
logger.info("Stopping Libvirt Monitor")
|
|
try:
|
|
self.libvirt_monitor.stop()
|
|
except Exception as e:
|
|
logger.error(f"Error stopping Libvirt monitor: {e}")
|
|
|
|
# Wait for threads to finish gracefully
|
|
for thread in self.monitor_threads:
|
|
if thread.is_alive():
|
|
logger.info(f"Waiting for thread {thread.name} to finish")
|
|
thread.join(timeout=5)
|
|
if thread.is_alive():
|
|
logger.warning(f"Thread {thread.name} did not finish within timeout")
|
|
|
|
self.monitor_threads.clear()
|
|
|
|
async def handle_worker_ping(self, data):
|
|
"""
|
|
Respond to a liveness ping from the WebSocket server.
|
|
Emits a 'worker_pong' event with the same ping_id and this worker's ID.
|
|
"""
|
|
try:
|
|
ping_id = data["ping_id"]
|
|
worker_id = self.worker_id
|
|
|
|
# Diagnostic: Log transport state during ping only if debug flag is enabled
|
|
if self.enable_websocket_ping_debug:
|
|
try:
|
|
transport = self.sio.transport()
|
|
logger.debug(f"[PING] Received ping {ping_id[:8]}... | Transport: {transport} | connected={self.sio.connected}")
|
|
except:
|
|
logger.debug(f"[PING] Received ping {ping_id[:8]}... | connected={self.sio.connected}")
|
|
|
|
await self.sio.emit("worker_pong", {"ping_id": ping_id,"worker_id": worker_id})
|
|
except Exception as e:
|
|
logger.error(f"Error handling worker_ping: {e}")
|
|
|
|
async def on_connect(self):
|
|
# Enhanced diagnostic logging
|
|
try:
|
|
current_transport = self.sio.transport()
|
|
logger.info(f"✓ CONNECTED - Transport: {current_transport} | sio.connected={self.sio.connected}")
|
|
except Exception as e:
|
|
logger.info(f"✓ CONNECTED (transport unknown: {e}) | sio.connected={self.sio.connected}")
|
|
await self.send_join_request()
|
|
|
|
async def on_connect_error(self, data):
|
|
logger.error(f"Connection failed: {data}")
|
|
self.joined_server = False
|
|
|
|
async def on_disconnect(self, *args):
|
|
# Enhanced disconnect logging
|
|
logger.warning(f"✗ DISCONNECTED | joined={self.joined_server} | monitors={self.monitors_started} | Args: {args}")
|
|
self.joined_server = False
|
|
self.monitors_started = False
|
|
self.reconnection_in_progress = True
|
|
self.stop_monitors()
|
|
# Start reconnection process
|
|
asyncio.create_task(self.reconnect())
|
|
|
|
async def on_message(self, data):
|
|
logger.info(f"Message received: {data}")
|
|
|
|
async def on_join_accept(self, data):
|
|
# Enhanced join logging
|
|
try:
|
|
transport = self.sio.transport()
|
|
logger.info(f"✓ JOIN ACCEPTED | Transport: {transport} | first_join={self.first_join_completed}")
|
|
except:
|
|
logger.info(f"✓ JOIN ACCEPTED | first_join={self.first_join_completed}")
|
|
|
|
self.joined_server = True
|
|
|
|
# Stop existing monitors if already started
|
|
if self.monitors_started:
|
|
self.stop_monitors()
|
|
|
|
# Start new monitor processes
|
|
self.start_monitors()
|
|
self.monitors_started = True
|
|
|
|
# Start OVS bridge scanner if available
|
|
if self.ovs_scanner:
|
|
# Initial scan
|
|
self.ovs_scanner.execute()
|
|
|
|
# Start PCI device scanner if available
|
|
if self.pci_scanner:
|
|
# Initial scan
|
|
self.pci_scanner.execute()
|
|
|
|
self.first_join_completed = True
|
|
|
|
async def on_join_reject(self, data):
|
|
logger.error("Join rejected.")
|
|
self.joined_server = False
|
|
|
|
if self.join_attempts < self.max_join_attempts:
|
|
delay = 2 ** (self.join_attempts - 1) # Exponential backoff
|
|
logger.info(f"Retrying join in {delay} seconds (attempt {self.join_attempts}/{self.max_join_attempts})")
|
|
asyncio.create_task(self.retry_join_after_delay(delay))
|
|
else:
|
|
logger.error(f"Max join attempts ({self.max_join_attempts}) reached. Exiting.")
|
|
exit()
|
|
|
|
async def retry_join_after_delay(self, delay):
|
|
await asyncio.sleep(delay)
|
|
if not self.sio.connected:
|
|
try:
|
|
await self.sio.connect(self.server_url, socketio_path=self.socketio_path)
|
|
except Exception as e:
|
|
logger.error(f"Failed to reconnect: {e}")
|
|
return
|
|
await self.send_join_request()
|
|
|
|
async def reconnect(self):
|
|
"""Handle reconnection to the server after disconnection."""
|
|
logger.warning(f"[RECONNECT] Starting reconnection process | sio.connected={self.sio.connected}")
|
|
reconnect_delay = 1 # Start with 1 second delay
|
|
max_reconnect_delay = 30 # Maximum delay of 30 seconds
|
|
|
|
while self.reconnection_in_progress:
|
|
try:
|
|
logger.info(f"[RECONNECT] Attempt #{int(reconnect_delay)} | delay: {reconnect_delay}s | sio.connected={self.sio.connected}")
|
|
await asyncio.sleep(reconnect_delay)
|
|
|
|
# Try to connect
|
|
if not self.sio.connected:
|
|
logger.info(f"[RECONNECT] Calling sio.connect({self.server_url})")
|
|
await self.sio.connect(self.server_url, socketio_path=self.socketio_path)
|
|
else:
|
|
logger.warning(f"[RECONNECT] Already connected! sio.connected={self.sio.connected}")
|
|
|
|
# If we get here, we've successfully reconnected
|
|
logger.info(f"[RECONNECT] ✓ Success | sio.connected={self.sio.connected}")
|
|
self.reconnection_in_progress = False
|
|
# The on_connect handler will send the join request
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"[RECONNECT] ✗ Failed: {e}")
|
|
# Increase delay exponentially, up to max_reconnect_delay
|
|
reconnect_delay = min(reconnect_delay * 2, max_reconnect_delay)
|
|
continue
|
|
|
|
|
|
async def send_join_request(self):
|
|
"""Notify the server about this worker (Initial join)."""
|
|
logger.info("Sending join request")
|
|
if not self.joined_server:
|
|
self.join_attempts += 1
|
|
await self.sio.emit("join_request", {"worker_id": self.worker_id, "worker_secret": self.worker_secret})
|
|
logger.info(f"Worker {self.worker_id} asked to join the server (attempt {self.join_attempts}).")
|
|
|
|
async def handle_task(self, data):
|
|
"""Handle incoming tasks from the server."""
|
|
if not self.joined_server:
|
|
logger.error("Not joined to server. ignoring task")
|
|
return
|
|
|
|
task_type = data["type"]
|
|
task_id = data["task_id"]
|
|
job_details = json.loads(data["job_details"])
|
|
task_worker_id = data["worker_id"]
|
|
|
|
if self.docker_monitor and task_type.startswith("container-"):
|
|
job_details["docker_monitor"] = self.docker_monitor
|
|
logger.debug("Added docker_monitor to job_details for container task")
|
|
|
|
logger.info(f"Received task {task_id} of type '{task_type}' with job_details: {job_details}")
|
|
|
|
loop = asyncio.get_event_loop()
|
|
result = None
|
|
|
|
try:
|
|
# ----------------------- Task dispatch ------------------------ #
|
|
if task_type == "report":
|
|
result = await loop.run_in_executor(None, functools.partial(ReportTask("", logger).Execute))
|
|
elif task_type == "ping":
|
|
result = await loop.run_in_executor(None, functools.partial(PingTask(job_details, logger).Execute))
|
|
elif task_type == "sdn-update":
|
|
result = await loop.run_in_executor(None, functools.partial(OVS_SDN(job_details, logger).Execute))
|
|
elif task_type == "file_presence":
|
|
result = await loop.run_in_executor(None, functools.partial(FilePresenceTask(job_details, logger).Execute))
|
|
elif task_type == "pod-update":
|
|
result = await loop.run_in_executor(None, functools.partial(ContainerTask(logger,self.docker_monitor).handle_pod_update_with_reconciliation, job_details))
|
|
elif task_type == "virtual-machine-create":
|
|
from worker_tasks.libvirt import LibvirtVirtualMachineTask
|
|
result = await loop.run_in_executor(None, functools.partial(LibvirtVirtualMachineTask(job_details, logger).execute))
|
|
elif task_type == "virtual-machine-delete":
|
|
from worker_tasks.libvirt import LibvirtVirtualMachineTask
|
|
result = await loop.run_in_executor(None, functools.partial(LibvirtVirtualMachineTask(job_details, logger).execute))
|
|
elif task_type == "container-reconcile":
|
|
result = await loop.run_in_executor(None, functools.partial(ContainerTask(logger,self.docker_monitor).reconcile_all_containers))
|
|
elif task_type == "reconcile_and_delete":
|
|
result = await loop.run_in_executor(None, functools.partial(ContainerTask(logger,self.docker_monitor).handle_reconcile_and_delete, job_details))
|
|
elif task_type == "dns-update":
|
|
if self.dns_task:
|
|
result = await loop.run_in_executor(None, functools.partial(self.dns_task.handle_dns_update, job_details))
|
|
else:
|
|
result = {"success": False, "error": "DNS task handler not initialized"}
|
|
else:
|
|
raise ValueError(f"Unknown task type: {task_type}")
|
|
|
|
# --------------------- ACK back to server --------------------- #
|
|
await self.send_task_result(task_id, result, task_worker_id)
|
|
|
|
# ---------------- Emit launch-failure events ------------------ #
|
|
if task_type == "pod-update" and result and result.get("launch_failures"):
|
|
for fail in result["launch_failures"]:
|
|
failed_id = fail["id"]
|
|
error_info = fail["error"]
|
|
|
|
event_data=({
|
|
"source": "docker",
|
|
"event_type": "launch_failed",
|
|
"container_id": failed_id,
|
|
"system_container_id": failed_id,
|
|
"container_name": failed_id,
|
|
"timestamp": time.time(),
|
|
"error": error_info
|
|
})
|
|
|
|
# Put event in the queue for the worker client
|
|
self.event_queue.put(event_data)
|
|
logger.debug(f"Added Docker event to queue: launch_failed for container {failed_id}.")
|
|
|
|
|
|
except SchedulingError as e:
|
|
logger.error(f"Error processing task {task_id} [{e.error_type.value}]: {e}")
|
|
await self.send_task_result(task_id, e.to_result(), task_worker_id)
|
|
except Exception as e:
|
|
logger.error(f"Error processing task {task_id}: {e}")
|
|
await self.send_task_result(task_id, {"success": False, "response": str(e), "error_type": "UNKNOWN"}, task_worker_id)
|
|
|
|
async def send_task_result(self, task_id, result, worker_id):
|
|
"""Send task result back to the server, waiting for reconnection if needed."""
|
|
for attempt in range(10):
|
|
if self.sio.connected:
|
|
await self.sio.emit("ack", {"task_id": task_id, "worker_id": worker_id, "result": result})
|
|
logger.debug(f"Sent result for task {task_id}: {result}")
|
|
return
|
|
logger.warning(f"[RESULT] Socket not connected, waiting to send task {task_id} result (attempt {attempt + 1}/10)")
|
|
await asyncio.sleep(2 ** attempt if attempt < 4 else 16)
|
|
logger.error(f"[RESULT] Failed to send result for task {task_id} after retries — socket never reconnected")
|
|
|
|
async def send_event(self, event_data):
|
|
"""Send monitoring event to the server, handling libvirt and docker events separately."""
|
|
if not self.joined_server:
|
|
return
|
|
|
|
source = event_data.get('source', 'unknown')
|
|
event_type = event_data.get('event_type', 'event')
|
|
|
|
# Construct the base event payload
|
|
payload = {
|
|
"worker_id": self.worker_id,
|
|
"type": f"{source}_{event_type}",
|
|
"details": event_data
|
|
}
|
|
|
|
# Handle libvirt events
|
|
if source == "libvirt":
|
|
payload["details"] = {
|
|
"system_vm_id": event_data.get("system_vm_id"),
|
|
"libvirt_vm_id": event_data.get("libvirt_vm_id"),
|
|
"timestamp": event_data.get("timestamp"),
|
|
}
|
|
|
|
# Emit the event to the server
|
|
await self.sio.emit("libvirt_event", payload)
|
|
logger.debug(f"Sent libvirt event: {event_type}\n{payload}")
|
|
|
|
|
|
# Handle docker events
|
|
elif source == "docker":
|
|
payload["details"] = {
|
|
"container_id": event_data.get("container_id"),
|
|
"system_container_id": event_data.get("system_container_id"),
|
|
"container_name": event_data.get("container_name"),
|
|
"timestamp": event_data.get("timestamp"),
|
|
"status": event_data.get("details", {}).get("status"),
|
|
"image": event_data.get("details", {}).get("from"),
|
|
"action": event_data.get("details", {}).get("Action"),
|
|
**({"error": event_data.get("error")} if event_data.get("error") else {})
|
|
}
|
|
|
|
# Include pull_progress data for pull_progress events
|
|
if event_type == "pull_progress" and "pull_progress" in event_data:
|
|
logger.debug("Sengin pull progreesss info")
|
|
payload["details"]["pull_progress"] = event_data["pull_progress"]
|
|
|
|
# Emit the event to the server
|
|
await self.sio.emit("docker_event", payload)
|
|
logger.debug(f"Sent {source} event: {event_type}\n{payload}")
|
|
|
|
async def process_event_queue(self):
|
|
"""Process events from the queue and send them to the server."""
|
|
if not self.event_queue:
|
|
return
|
|
|
|
# Dictionary to store the most recent event for each container and status combination
|
|
pending_events = {}
|
|
|
|
# Timer to ensure events are sent even if no new events arrive
|
|
last_send_time = time.time()
|
|
send_interval = 1.0 # Send events at least every second
|
|
|
|
while self.running:
|
|
try:
|
|
# Non-blocking check for events
|
|
events_processed = 0
|
|
while not self.event_queue.empty() and events_processed < 100: # Process up to 100 events at once
|
|
event = self.event_queue.get(block=False)
|
|
events_processed += 1
|
|
|
|
# Create a key for deduplication based on container and event type
|
|
container_id = event.get('system_container_id') or event.get('container_id')
|
|
event_type = event.get('event_type')
|
|
source = event.get('source')
|
|
|
|
if container_id and event_type and source:
|
|
# Create a unique key for this container and event type combination
|
|
key = f"{source}:{container_id}:{event_type}"
|
|
|
|
# Store the most recent event for this key
|
|
pending_events[key] = event
|
|
logger.debug(f"Added/updated pending event: {key}")
|
|
else:
|
|
# If we can't create a key, send the event immediately
|
|
logger.debug(f"Processing event immediately (no key): {event}")
|
|
await self.send_event(event)
|
|
|
|
# Send pending events if we have any or if enough time has passed
|
|
current_time = time.time()
|
|
if pending_events and (len(pending_events) >= 10 or (current_time - last_send_time) >= send_interval):
|
|
logger.debug(f"Sending {len(pending_events)} pending events")
|
|
for key, event in list(pending_events.items()):
|
|
logger.debug(f"Processing event from queue: {event}")
|
|
await self.send_event(event)
|
|
del pending_events[key]
|
|
last_send_time = current_time
|
|
|
|
# Small delay to prevent CPU thrashing
|
|
await asyncio.sleep(0.01)
|
|
except Exception as e:
|
|
logger.error(f"Error in event processing: {e}")
|
|
|
|
async def start(self):
|
|
"""Worker connects to the API server and processes tasks."""
|
|
try:
|
|
logger.info(f"[START] Worker {self.worker_id} connecting to {self.server_url}...")
|
|
await self.sio.connect(self.server_url, socketio_path=self.socketio_path)
|
|
|
|
logger.info(f"[START] Initial connection established | sio.connected={self.sio.connected}")
|
|
|
|
# Start the event queue processor if we have an event queue
|
|
if self.event_queue:
|
|
event_processor = asyncio.create_task(self.process_event_queue())
|
|
await self.sio.wait()
|
|
event_processor.cancel()
|
|
|
|
else:
|
|
await self.sio.wait()
|
|
|
|
except Exception as e:
|
|
logger.error(f"[START] Error: {e}")
|
|
await self.sio.disconnect()
|
|
|
|
def stop(self):
|
|
"""Stop the worker client."""
|
|
logger.info("Stopping worker client...")
|
|
self.running = False
|
|
self.monitors_started = False
|
|
try:
|
|
self.stop_monitors()
|
|
except Exception as e:
|
|
logger.error(f"Error stopping monitors: {e}")
|
|
logger.info("Worker client stopped.")
|
|
|
|
|
|
|
|
async def logs_retrieve(self,data):
|
|
logger.info(f"Handling container-log {data}")
|
|
|
|
container_name = data.get("container_name")
|
|
lines = int(data.get("lines", 100))
|
|
request_id = data.get("request_id")
|
|
worker_id = data.get("worker_id")
|
|
|
|
if worker_id != self.worker_id:
|
|
logger.debug(f"Ignoring container-log request for worker {worker_id}")
|
|
return
|
|
|
|
logger.info(f"Handling container-log for {container_name}, lines={lines}")
|
|
|
|
log_data=ContainerTask(logger,self.docker_monitor).handle_container_log_request(data)
|
|
|
|
log_data["worker_id"] = self.worker_id
|
|
log_data["request_id"] = request_id
|
|
|
|
await self.sio.emit("container_log_response", log_data)
|
|
logger.debug(f"Sent container-log-response for {container_name}")
|
|
|
|
async def logs_stream_start(self, data):
|
|
request_id = data["request_id"]
|
|
container_name = data["container_name"]
|
|
worker_id = data.get("worker_id")
|
|
logger.debug(f"Starting stream with {data}")
|
|
|
|
if worker_id != self.worker_id:
|
|
logger.debug(f"Ignoring stream request for worker {worker_id}")
|
|
return
|
|
|
|
if request_id in self.log_stream_threads:
|
|
logger.warning(f"Log stream already running for request {request_id}")
|
|
return
|
|
|
|
# Set cancellation flag
|
|
self.log_stream_threads[request_id] = True
|
|
|
|
container_task = ContainerTask(logger,self.docker_monitor)
|
|
|
|
def cancel_flag():
|
|
return not self.log_stream_threads.get(request_id, False)
|
|
|
|
def on_log(log_line):
|
|
asyncio.run(self.sio.emit("container-log-stream", {
|
|
"request_id": request_id,
|
|
"worker_id": self.worker_id,
|
|
"logs": log_line
|
|
}))
|
|
|
|
def on_error(error_msg):
|
|
asyncio.run(self.sio.emit("container-log-stream", {
|
|
"request_id": request_id,
|
|
"worker_id": self.worker_id,
|
|
"logs": f"Stream error: {error_msg}"
|
|
}))
|
|
|
|
thread = threading.Thread(
|
|
target=container_task.stream_logs,
|
|
args=(container_name, on_log, on_error, cancel_flag),
|
|
daemon=True
|
|
)
|
|
thread.start()
|
|
|
|
logger.info(f"Started log stream thread for request {request_id}")
|
|
|
|
async def logs_stream_stop(self, data):
|
|
request_id = data.get("request_id")
|
|
if request_id in self.log_stream_threads:
|
|
self.log_stream_threads[request_id] = False
|
|
logger.info(f"Stopped log stream for request {request_id}")
|
|
else:
|
|
logger.warning(f"No active log stream found for request {request_id}")
|
|
|
|
|
|
async def vm_logs_stream_start(self, data):
|
|
"""Start streaming a VM's serial console log to the WebSocket server."""
|
|
request_id = data.get("request_id")
|
|
vm_id = data.get("vm_id")
|
|
worker_id = data.get("worker_id")
|
|
|
|
if worker_id != self.worker_id:
|
|
logger.debug(f"Ignoring vm log stream request for worker {worker_id}")
|
|
return
|
|
|
|
if request_id in self.vm_log_stream_threads:
|
|
logger.warning(f"VM log stream already running for request {request_id}")
|
|
return
|
|
|
|
logger.info(f"[{request_id}] Starting VM log stream for VM '{vm_id}'")
|
|
self.vm_log_stream_threads[request_id] = True
|
|
|
|
from worker_tasks.libvirt import LibvirtVirtualMachineTask
|
|
|
|
task = LibvirtVirtualMachineTask.__new__(LibvirtVirtualMachineTask)
|
|
task.logger = logger
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
def cancel_flag():
|
|
return not self.vm_log_stream_threads.get(request_id, False)
|
|
|
|
def on_log(line):
|
|
asyncio.run_coroutine_threadsafe(
|
|
self.sio.emit("vm-log-stream", {
|
|
"request_id": request_id,
|
|
"worker_id": self.worker_id,
|
|
"log": line
|
|
}),
|
|
loop
|
|
)
|
|
|
|
def on_error(msg):
|
|
asyncio.run_coroutine_threadsafe(
|
|
self.sio.emit("vm-log-stream", {
|
|
"request_id": request_id,
|
|
"worker_id": self.worker_id,
|
|
"log": f"[stream error] {msg}"
|
|
}),
|
|
loop
|
|
)
|
|
|
|
thread = threading.Thread(
|
|
target=task.stream_vm_logs,
|
|
args=(vm_id, on_log, on_error, cancel_flag),
|
|
daemon=True
|
|
)
|
|
thread.start()
|
|
logger.info(f"[{request_id}] VM log stream thread started for '{vm_id}'")
|
|
|
|
async def vm_logs_stream_stop(self, data):
|
|
request_id = data.get("request_id")
|
|
if request_id in self.vm_log_stream_threads:
|
|
self.vm_log_stream_threads[request_id] = False
|
|
logger.info(f"[{request_id}] Stopped VM log stream")
|
|
else:
|
|
logger.warning(f"[{request_id}] No active VM log stream found")
|
|
|
|
|
|
async def start_terminal_session(self, data):
|
|
request_id = data["request_id"]
|
|
container_name = data["container_name"]
|
|
command = data.get("command", "/bin/sh")
|
|
logger.debug(f"Starting terminal session with {data}")
|
|
|
|
if request_id in self.terminal_sessions:
|
|
logger.warning(f"Terminal session already exists for {request_id}")
|
|
return
|
|
|
|
container_task = ContainerTask(logger,self.docker_monitor)
|
|
session = container_task.start_terminal(logger, container_name, command)
|
|
|
|
def cancel_flag():
|
|
return request_id not in self.terminal_sessions
|
|
|
|
def on_output(output):
|
|
asyncio.run(self.sio.emit("terminal_output", {
|
|
"request_id": request_id,
|
|
"data": output
|
|
}))
|
|
|
|
self.terminal_sessions[request_id] = session
|
|
|
|
threading.Thread(
|
|
target=session.stream_output,
|
|
args=(logger, on_output, cancel_flag),
|
|
daemon=True
|
|
).start()
|
|
|
|
logger.info(f"Started terminal session for {request_id}")
|
|
|
|
async def terminal_input(self, data):
|
|
logger.debug(f"Terminal input {data}")
|
|
request_id = data["request_id"]
|
|
session = self.terminal_sessions.get(request_id)
|
|
if session:
|
|
session.write(data["data"])
|
|
|
|
async def stop_terminal_session(self, data):
|
|
logger.debug(f"Stopping terminal session with {data}")
|
|
request_id = data["request_id"]
|
|
session = self.terminal_sessions.pop(request_id, None)
|
|
if session:
|
|
session.close()
|
|
logger.info(f"Closed terminal session for {request_id}")
|
|
|
|
|
|
|
|
async def start_vnc_stream(self, data):
|
|
logger.debug(f"start vnc {data}")
|
|
request_id = data["vnc_request_id"]
|
|
virtual_machine_id = data.get("virtual_machine_id")
|
|
vnc_host = self.vnc_host
|
|
|
|
if not virtual_machine_id:
|
|
logger.error(f"[VNC] Rejecting session {request_id}: no virtual_machine_id provided")
|
|
await self.sio.emit("vnc_error", {"vnc_request_id": request_id, "error": "no virtual_machine_id"})
|
|
return
|
|
|
|
if not self.libvirt_monitor or not self.libvirt_monitor.conn:
|
|
logger.error(f"[VNC] Rejecting session {request_id}: libvirt not available")
|
|
await self.sio.emit("vnc_error", {"vnc_request_id": request_id, "error": "libvirt unavailable"})
|
|
return
|
|
|
|
try:
|
|
dom = self.libvirt_monitor.conn.lookupByName(virtual_machine_id)
|
|
vnc_port = self.libvirt_monitor.get_vnc_port(dom)
|
|
except Exception as e:
|
|
logger.error(f"[VNC] Rejecting session {request_id}: VM {virtual_machine_id} not found in libvirt: {e}")
|
|
await self.sio.emit("vnc_error", {"vnc_request_id": request_id, "error": "VM not found in libvirt"})
|
|
return
|
|
|
|
if not vnc_port or vnc_port <= 0:
|
|
logger.error(f"[VNC] Rejecting session {request_id}: VM {virtual_machine_id} has no VNC port configured")
|
|
await self.sio.emit("vnc_error", {"vnc_request_id": request_id, "error": "VM has no VNC port"})
|
|
return
|
|
|
|
logger.info(f"[VNC] Starting session {request_id} on port {vnc_port}")
|
|
|
|
if request_id in self.vnc_sessions:
|
|
logger.warning(f"[VNC] Session already running for {request_id}")
|
|
return
|
|
|
|
def cancel_flag():
|
|
return request_id not in self.vnc_sessions
|
|
|
|
def on_output(vnc_data):
|
|
asyncio.run(self.sio.emit("vnc_frame_from_worker", {
|
|
"vnc_request_id": request_id,
|
|
"data": vnc_data
|
|
}))
|
|
|
|
session = VNCSession(logger, request_id, vnc_host, vnc_port, on_output, cancel_flag)
|
|
self.vnc_sessions[request_id] = session
|
|
|
|
thread = threading.Thread(target=session.start, daemon=True)
|
|
thread.start()
|
|
logger.info(f"[VNC] Session thread started for {request_id}")
|
|
|
|
async def vnc_frame_from_novnc(self, data):
|
|
request_id = data["vnc_request_id"]
|
|
session = self.vnc_sessions.get(request_id)
|
|
if session:
|
|
session.write(data["data"])
|
|
|
|
async def stop_vnc_stream(self, data):
|
|
request_id = data["vnc_request_id"]
|
|
session = self.vnc_sessions.pop(request_id, None)
|
|
if session:
|
|
session.close()
|
|
logger.info(f"[VNC] Stopped session for {request_id}")
|