from docker.models.containers import Container from typing import Dict, List, Optional, Tuple import docker import logging import subprocess import time class DockerModule: """ Module for handling Docker container operations in SIA. Manages container lifecycle, I/O operations, and status monitoring. """ def _start_docker_engine(): """ Start the Docker engine """ subprocess.run(['/etc/init.d/docker', 'start']) time.sleep(1) def __init__(self): DockerModule._start_docker_engine() self.client = docker.DockerClient(base_url='unix://var/run/docker.sock') self.containers: Dict[str, Container] = {} self.container_sockets: Dict[str, docker.APIClient.AttachSocket] = {} self.logger = logging.getLogger(__name__) def start_container( self, image: str, name: Optional[str] = None, timeout: int = -1, command: Optional[str] = None, arguments: Optional[List[str]] = None, volumes: Optional[Dict[str, str]] = None, ports: Optional[Dict[str, str]] = None, environment: Optional[Dict[str, str]] = None, ) -> str: """Start a new Docker container with the specified configuration. Args: image: Docker image to use name: Unique container name for long running containers timeout: Timeout in milliseconds for short running containers command: Main command to run in container arguments: List of command line arguments volumes: Dictionary mapping host paths to container paths ports: Dictionary mapping host ports to container ports environment: Dictionary of environment variables and values Returns: For short-lived containers (with timeout): Container output For long-running containers: Container name Raises: docker.errors.ImageNotFound: If specified image doesn't exist docker.errors.APIError: If container creation/start fails """ # Validate inputs if name is None and timeout < 0: raise ValueError("Either name or timeout must be provided") # Prepare command with arguments cmd = [] if command: cmd.append(command) if arguments: cmd.extend(arguments) # Prepare volume bindings volume_binds = {} if volumes: for host_path, container_path in volumes.items(): volume_binds[host_path] = {'bind': container_path, 'mode': 'rw'} # Prepare port bindings port_bindings = {} if ports: for host_port, container_port in ports.items(): port_bindings[container_port] = host_port try: self.client.images.pull('busybox:latest') # Create container with tty and open stdin container = self.client.containers.create( image, command=cmd if cmd else None, name=name, detach=True, volumes=volume_binds, ports=port_bindings, environment=environment, tty=True, stdin_open=True ) # Attach socket before starting container socket = container.attach_socket(params={ 'stdin': 1, 'stdout': 1, 'stderr': 1, 'stream': 1 }) # Start the container container.start() if timeout >= 0: # For short-lived containers, wait for completion try: container.wait(timeout=timeout/1000) # Convert ms to seconds output = container.logs().decode('utf-8') socket.close() container.remove(force=True) return output except docker.errors.NotFound: self.logger.warning(f"Container was removed before timeout") return "" else: # For long-running containers, store references self.containers[name] = container self.container_sockets[name] = socket return name except Exception as e: self.logger.error(f"Error starting container: {str(e)}") raise def stop_container(self, name: str) -> None: """Stop and remove a Docker container by name. Args: name: Name of the container to stop Raises: KeyError: If container name not found docker.errors.APIError: If container stop/removal fails """ if name not in self.containers: raise KeyError(f"Container {name} not found") try: # Close socket if it exists if name in self.container_sockets: self.container_sockets[name].close() del self.container_sockets[name] # Stop and remove container container = self.containers[name] container.stop() container.remove() del self.containers[name] except Exception as e: self.logger.error(f"Error stopping container {name}: {str(e)}") raise def write_container_stdin(self, name: str, data: str) -> None: """Write data to a container's standard input. Args: name: Name of the target container data: Data to write to stdin Raises: KeyError: If container name not found docker.errors.APIError: If write operation fails """ if name not in self.containers or name not in self.container_sockets: raise KeyError(f"Container {name} not found or socket not initialized") try: socket = self.container_sockets[name] socket._sock.send(data.encode('utf-8')) except Exception as e: self.logger.error(f"Error writing to container {name} stdin: {str(e)}") raise def read_container_stdout(self, name: str, n: int = -1) -> str: """Read from a container's standard output buffer. Args: name: Name of the container n: Number of bytes to read; -1 means read all available Returns: Data read from stdout Raises: KeyError: If container name not found docker.errors.APIError: If read operation fails """ if name not in self.containers: raise KeyError(f"Container {name} not found") try: container = self.containers[name] logs = container.logs(stdout=True, stderr=False, tail=n if n > 0 else 'all') return logs.decode('utf-8') except Exception as e: self.logger.error(f"Error reading container {name} stdout: {str(e)}") raise def read_container_stderr(self, name: str, n: int = -1) -> str: """Read from a container's standard error buffer. Args: name: Name of the container n: Number of bytes to read; -1 means read all available Returns: Data read from stderr Raises: KeyError: If container name not found docker.errors.APIError: If read operation fails """ if name not in self.containers: raise KeyError(f"Container {name} not found") try: container = self.containers[name] logs = container.logs(stdout=False, stderr=True, tail=n if n > 0 else 'all') return logs.decode('utf-8') except Exception as e: self.logger.error(f"Error reading container {name} stderr: {str(e)}") raise def wait_container(self, name: str, timeout: int) -> Tuple[int, str]: """Wait for a container to finish execution. Args: name: Name of the container to wait for timeout: Time to wait in milliseconds Returns: Tuple of (exit_code, output) Raises: KeyError: If container name not found docker.errors.APIError: If wait operation fails TimeoutError: If container doesn't finish within timeout """ if name not in self.containers: raise KeyError(f"Container {name} not found") try: container = self.containers[name] result = container.wait(timeout=timeout/1000) # Convert ms to seconds logs = container.logs().decode('utf-8') return (result['StatusCode'], logs) except Exception as e: self.logger.error(f"Error waiting for container {name}: {str(e)}") raise def get_container_status(self, name: str) -> Dict[str, any]: """Get current status information for a container. Args: name: Name of the container Returns: Dictionary with container status information Raises: KeyError: If container name not found docker.errors.APIError: If status check fails """ if name not in self.containers: raise KeyError(f"Container {name} not found") try: container = self.containers[name] container.reload() # Refresh container info return { 'name': name, 'status': container.status, 'started_at': container.attrs['State']['StartedAt'], 'exit_code': container.attrs['State']['ExitCode'], 'error': container.attrs['State']['Error'], 'stdout_size': len(container.logs(stdout=True, stderr=False)), 'stderr_size': len(container.logs(stdout=False, stderr=True)) } except Exception as e: self.logger.error(f"Error getting status for container {name}: {str(e)}") raise