157 lines
4.6 KiB
Python
157 lines
4.6 KiB
Python
"""
|
|
Podman client wrapper with security validation.
|
|
|
|
Wraps Podman API with security validation and error handling.
|
|
All container operations are validated against security policy.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
from podman import PodmanClient as BasePodmanClient
|
|
|
|
from mcp_forge.security.allowlist import OperationValidator
|
|
from mcp_forge.security.audit import AuditLogger
|
|
|
|
|
|
class PodmanConnectionError(Exception):
|
|
"""Raised when connection to Podman fails."""
|
|
pass
|
|
|
|
|
|
class PodmanClient:
|
|
"""
|
|
Wrapper around Podman API with security validation.
|
|
|
|
All container operations are validated against security policy
|
|
before being sent to Podman. Provides lazy connection and
|
|
proper error handling.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
socket_path: Path,
|
|
validator: OperationValidator,
|
|
audit_logger: AuditLogger
|
|
):
|
|
"""
|
|
Initialize Podman client wrapper.
|
|
|
|
Args:
|
|
socket_path: Path to Podman socket
|
|
validator: Operation validator for security checks
|
|
audit_logger: Audit logger for operation logging
|
|
"""
|
|
self.socket_path = Path(socket_path)
|
|
self.validator = validator
|
|
self.audit_logger = audit_logger
|
|
self._client: Optional[BasePodmanClient] = None
|
|
|
|
def connect(self) -> None:
|
|
"""
|
|
Connect to Podman via socket.
|
|
|
|
Raises:
|
|
PodmanConnectionError: If connection fails
|
|
"""
|
|
try:
|
|
# Verify socket exists and is accessible
|
|
self.verify_socket_access()
|
|
|
|
# Create Podman client with Unix socket
|
|
base_url = f"unix://{self.socket_path}"
|
|
self._client = BasePodmanClient(base_url=base_url)
|
|
|
|
# Test connection with ping (only if client supports it)
|
|
if hasattr(self._client, 'ping'):
|
|
self._client.ping()
|
|
|
|
except PodmanConnectionError:
|
|
# Re-raise our own exceptions
|
|
raise
|
|
except Exception as e:
|
|
raise PodmanConnectionError(
|
|
f"Failed to connect to Podman at {self.socket_path}: {e}"
|
|
) from e
|
|
|
|
def ping(self) -> bool:
|
|
"""
|
|
Test connection to Podman.
|
|
|
|
Returns:
|
|
True if connection is healthy
|
|
|
|
Raises:
|
|
PodmanConnectionError: If not connected or ping fails
|
|
"""
|
|
if self._client is None:
|
|
raise PodmanConnectionError("Not connected to Podman")
|
|
|
|
try:
|
|
result = self._client.ping()
|
|
return result == "OK" or result is True
|
|
except Exception as e:
|
|
raise PodmanConnectionError(f"Ping failed: {e}") from e
|
|
|
|
def disconnect(self) -> None:
|
|
"""Disconnect from Podman and cleanup."""
|
|
if self._client is not None:
|
|
try:
|
|
self._client.close()
|
|
except Exception:
|
|
pass # Ignore errors during cleanup
|
|
finally:
|
|
self._client = None
|
|
|
|
def verify_socket_access(self) -> None:
|
|
"""
|
|
Verify that socket exists and is accessible.
|
|
|
|
Raises:
|
|
PodmanConnectionError: If socket is not accessible
|
|
"""
|
|
if not self.socket_path.exists():
|
|
raise PodmanConnectionError(
|
|
f"Podman socket not found: {self.socket_path}"
|
|
)
|
|
|
|
if not os.access(self.socket_path, os.R_OK):
|
|
raise PodmanConnectionError(
|
|
f"Podman socket is not readable: {self.socket_path}"
|
|
)
|
|
|
|
def check_api_version(self) -> dict:
|
|
"""
|
|
Get Podman API version information.
|
|
|
|
Returns:
|
|
Dictionary with version information
|
|
|
|
Raises:
|
|
PodmanConnectionError: If not connected
|
|
"""
|
|
if self._client is None:
|
|
raise PodmanConnectionError("Not connected to Podman")
|
|
|
|
try:
|
|
return self._client.version()
|
|
except Exception as e:
|
|
raise PodmanConnectionError(
|
|
f"Failed to get API version: {e}"
|
|
) from e
|
|
|
|
@property
|
|
def client(self) -> BasePodmanClient:
|
|
"""
|
|
Get underlying Podman client (lazy connection).
|
|
|
|
Returns:
|
|
Connected Podman client
|
|
|
|
Raises:
|
|
PodmanConnectionError: If connection fails
|
|
"""
|
|
if self._client is None:
|
|
self.connect()
|
|
assert self._client is not None # Type narrowing for mypy
|
|
return self._client
|