387 lines
No EOL
17 KiB
Python
387 lines
No EOL
17 KiB
Python
import docker
|
|
from typing import List, Dict, Any, Optional, AsyncGenerator
|
|
import asyncio
|
|
import logging
|
|
from ..models.container import Container, ContainerStats, NetworkStats, PortBinding
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class DockerService:
|
|
def __init__(self):
|
|
"""Initialise le service Docker."""
|
|
self.client = None
|
|
self.init_client()
|
|
|
|
def init_client(self):
|
|
"""Initialise ou réinitialise le client Docker."""
|
|
try:
|
|
if self.client:
|
|
try:
|
|
# Tester si le client existant est toujours valide
|
|
self.client.ping()
|
|
logger.info("Client Docker existant est valide")
|
|
return
|
|
except:
|
|
logger.warning("Client Docker existant n'est plus valide")
|
|
self.client = None
|
|
|
|
self.client = docker.from_env()
|
|
self.client.ping() # Vérifier que la connexion fonctionne
|
|
logger.info("Nouveau client Docker initialisé avec succès")
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de l'initialisation du client Docker: {e}")
|
|
raise
|
|
|
|
def ensure_client(self):
|
|
"""S'assure que le client Docker est initialisé et valide."""
|
|
try:
|
|
if not self.client:
|
|
logger.warning("Client Docker non initialisé, tentative de réinitialisation")
|
|
self.init_client()
|
|
return
|
|
|
|
# Test simple pour vérifier que le client est fonctionnel
|
|
self.client.ping()
|
|
except Exception as e:
|
|
logger.warning(f"Client Docker invalide, tentative de réinitialisation: {e}")
|
|
self.init_client()
|
|
|
|
def _parse_port_bindings(self, ports: Dict) -> List[PortBinding]:
|
|
"""Convertit les ports Docker en modèle PortBinding."""
|
|
bindings = []
|
|
if not ports:
|
|
return bindings
|
|
|
|
for container_port, host_bindings in ports.items():
|
|
if not host_bindings:
|
|
continue
|
|
|
|
# Format: "8080/tcp"
|
|
port, proto = container_port.split("/")
|
|
for binding in host_bindings:
|
|
bindings.append(PortBinding(
|
|
host_ip=binding.get("HostIp", "0.0.0.0"),
|
|
host_port=int(binding.get("HostPort", port)),
|
|
container_port=int(port),
|
|
protocol=proto
|
|
))
|
|
return bindings
|
|
|
|
def _parse_environment(self, env: List[str]) -> Dict[str, str]:
|
|
"""Convertit les variables d'environnement en dictionnaire."""
|
|
result = {}
|
|
if not env:
|
|
return result
|
|
|
|
for var in env:
|
|
if "=" in var:
|
|
key, value = var.split("=", 1)
|
|
result[key] = value
|
|
return result
|
|
|
|
async def list_containers(self) -> List[Container]:
|
|
"""Liste tous les conteneurs Docker."""
|
|
try:
|
|
self.ensure_client()
|
|
logger.info("Début de la récupération de la liste des conteneurs")
|
|
|
|
containers = self.client.containers.list(all=True)
|
|
logger.info(f"Nombre de conteneurs trouvés: {len(containers)}")
|
|
|
|
result = []
|
|
for container in containers:
|
|
try:
|
|
attrs = container.attrs
|
|
container_model = Container(
|
|
id=container.id,
|
|
name=container.name,
|
|
status=container.status,
|
|
image=container.image.tags[0] if container.image.tags else "none",
|
|
created=attrs['Created'],
|
|
ports=self._parse_port_bindings(attrs['NetworkSettings']['Ports']),
|
|
volumes=[v['Source'] for v in attrs['Mounts']],
|
|
environment=self._parse_environment(attrs['Config']['Env']),
|
|
networks=list(attrs['NetworkSettings']['Networks'].keys()),
|
|
health_status=attrs.get('State', {}).get('Health', {}).get('Status'),
|
|
restart_policy=attrs['HostConfig']['RestartPolicy']['Name'],
|
|
command=attrs['Config']['Cmd'][0] if attrs['Config']['Cmd'] else None
|
|
)
|
|
result.append(container_model)
|
|
logger.debug(f"Conteneur ajouté: {container.name}")
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la conversion du conteneur {container.name}: {e}")
|
|
continue
|
|
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la liste des conteneurs: {e}")
|
|
raise
|
|
|
|
async def get_container(self, container_id: str) -> Container:
|
|
"""Récupère les informations d'un conteneur spécifique."""
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
attrs = container.attrs
|
|
return Container(
|
|
id=container.id,
|
|
name=container.name,
|
|
status=container.status,
|
|
image=container.image.tags[0] if container.image.tags else "none",
|
|
created=attrs['Created'],
|
|
ports=self._parse_port_bindings(attrs['NetworkSettings']['Ports']),
|
|
volumes=[v['Source'] for v in attrs['Mounts']],
|
|
environment=self._parse_environment(attrs['Config']['Env']),
|
|
networks=list(attrs['NetworkSettings']['Networks'].keys()),
|
|
health_status=attrs.get('State', {}).get('Health', {}).get('Status'),
|
|
restart_policy=attrs['HostConfig']['RestartPolicy']['Name'],
|
|
command=attrs['Config']['Cmd'][0] if attrs['Config']['Cmd'] else None
|
|
)
|
|
|
|
async def get_container_logs(self, container_id: str, tail: int = 100) -> List[Dict[str, str]]:
|
|
"""Récupère les logs d'un conteneur."""
|
|
try:
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
if not container:
|
|
logger.error(f"Conteneur {container_id} non trouvé")
|
|
return []
|
|
|
|
logger.info(f"Récupération des logs pour le conteneur {container_id}")
|
|
|
|
# Utilisation de l'API Docker pour obtenir les logs
|
|
logs = container.logs(
|
|
tail=tail,
|
|
timestamps=True,
|
|
stdout=True,
|
|
stderr=True
|
|
)
|
|
|
|
if not logs:
|
|
logger.warning(f"Aucun log trouvé pour le conteneur {container_id}")
|
|
return []
|
|
|
|
decoded_logs = logs.decode('utf-8')
|
|
log_lines = []
|
|
|
|
for line in decoded_logs.split('\n'):
|
|
if not line.strip():
|
|
continue
|
|
|
|
# Format attendu: "2024-04-01T11:10:27.000000000Z message..."
|
|
try:
|
|
timestamp, message = line.split(' ', 1)
|
|
log_lines.append({
|
|
"timestamp": timestamp,
|
|
"message": message.strip(),
|
|
"stream": "stdout" # Par défaut stdout
|
|
})
|
|
except ValueError:
|
|
# Si le format n'est pas celui attendu, on garde la ligne complète
|
|
log_lines.append({
|
|
"timestamp": "",
|
|
"message": line.strip(),
|
|
"stream": "stdout"
|
|
})
|
|
|
|
logger.info(f"Nombre de lignes de logs trouvées : {len(log_lines)}")
|
|
return log_lines
|
|
|
|
except docker.errors.NotFound:
|
|
logger.error(f"Conteneur {container_id} non trouvé")
|
|
return []
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération des logs du conteneur {container_id}: {e}")
|
|
raise
|
|
|
|
async def get_container_stats(self, container_id: str) -> ContainerStats:
|
|
"""Récupère les statistiques d'un conteneur."""
|
|
try:
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
stats = container.stats(stream=False)
|
|
|
|
logger.debug(f"Stats brutes reçues pour {container_id}: {stats}")
|
|
|
|
# Vérifier la présence des clés nécessaires
|
|
if not all(key in stats for key in ['cpu_stats', 'precpu_stats', 'memory_stats']):
|
|
logger.error(f"Données de stats incomplètes pour le conteneur {container_id}")
|
|
raise ValueError("Données de statistiques incomplètes")
|
|
|
|
# Calculer le pourcentage CPU avec vérification des valeurs
|
|
try:
|
|
cpu_delta = stats['cpu_stats']['cpu_usage']['total_usage'] - stats['precpu_stats']['cpu_usage']['total_usage']
|
|
system_delta = stats['cpu_stats']['system_cpu_usage'] - stats['precpu_stats']['system_cpu_usage']
|
|
online_cpus = stats['cpu_stats'].get('online_cpus', 1) # Utiliser 1 comme valeur par défaut
|
|
|
|
if system_delta > 0:
|
|
cpu_percent = (cpu_delta / system_delta) * 100.0 * online_cpus
|
|
else:
|
|
cpu_percent = 0.0
|
|
except (KeyError, TypeError) as e:
|
|
logger.warning(f"Erreur lors du calcul du CPU pour {container_id}: {e}")
|
|
cpu_percent = 0.0
|
|
|
|
# Convertir les statistiques réseau avec gestion des erreurs
|
|
networks = {}
|
|
for net_name, net_stats in stats.get('networks', {}).items():
|
|
try:
|
|
networks[net_name] = NetworkStats(
|
|
rx_bytes=net_stats.get('rx_bytes', 0),
|
|
rx_packets=net_stats.get('rx_packets', 0),
|
|
rx_errors=net_stats.get('rx_errors', 0),
|
|
rx_dropped=net_stats.get('rx_dropped', 0),
|
|
tx_bytes=net_stats.get('tx_bytes', 0),
|
|
tx_packets=net_stats.get('tx_packets', 0),
|
|
tx_errors=net_stats.get('tx_errors', 0),
|
|
tx_dropped=net_stats.get('tx_dropped', 0)
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f"Erreur lors de la conversion des stats réseau pour {net_name}: {e}")
|
|
continue
|
|
|
|
# Récupérer les statistiques de bloc avec gestion des erreurs
|
|
try:
|
|
io_stats = stats.get('blkio_stats', {}).get('io_service_bytes_recursive', [])
|
|
block_read = io_stats[0].get('value', 0) if len(io_stats) > 0 else 0
|
|
block_write = io_stats[1].get('value', 0) if len(io_stats) > 1 else 0
|
|
except (IndexError, KeyError, AttributeError) as e:
|
|
logger.warning(f"Erreur lors de la récupération des stats de bloc pour {container_id}: {e}")
|
|
block_read = 0
|
|
block_write = 0
|
|
|
|
return ContainerStats(
|
|
cpu_percent=cpu_percent,
|
|
memory_usage=stats.get('memory_stats', {}).get('usage', 0),
|
|
memory_limit=stats.get('memory_stats', {}).get('limit', 0),
|
|
network=networks,
|
|
block_read=block_read,
|
|
block_write=block_write
|
|
)
|
|
|
|
except docker.errors.NotFound:
|
|
logger.error(f"Conteneur {container_id} non trouvé")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors de la récupération des stats du conteneur {container_id}: {e}")
|
|
raise
|
|
|
|
async def start_container(self, container_id: str) -> None:
|
|
"""Démarre un conteneur."""
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
container.start()
|
|
|
|
async def stop_container(self, container_id: str) -> None:
|
|
"""Arrête un conteneur."""
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
container.stop()
|
|
|
|
async def restart_container(self, container_id: str) -> None:
|
|
"""Redémarre un conteneur."""
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
container.restart()
|
|
|
|
async def update_container(self, container_id: str) -> None:
|
|
"""Met à jour un conteneur."""
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
container.restart()
|
|
|
|
async def follow_container_logs(self, container_id: str) -> AsyncGenerator[Dict[str, str], None]:
|
|
"""Suit les logs d'un conteneur en temps réel en utilisant l'API Docker Python."""
|
|
try:
|
|
self.ensure_client()
|
|
container = self.client.containers.get(container_id)
|
|
|
|
logger.info(f"Démarrage du suivi des logs pour le conteneur {container_id}")
|
|
|
|
# Utiliser l'API Docker Python pour obtenir un flux de logs continu
|
|
log_stream = container.logs(
|
|
stream=True,
|
|
follow=True,
|
|
timestamps=True,
|
|
stdout=True,
|
|
stderr=True,
|
|
tail=100 # Commencer avec les 100 derniers logs
|
|
)
|
|
|
|
for log in log_stream:
|
|
try:
|
|
# Décoder la ligne de log
|
|
line = log.decode('utf-8').strip()
|
|
if not line:
|
|
continue
|
|
|
|
logger.debug(f"Ligne de log brute reçue: {line}")
|
|
|
|
# Format attendu: "2024-04-01T11:10:27.000000000Z message..."
|
|
try:
|
|
# Trouver le premier espace qui sépare la date du message
|
|
space_index = line.find(' ')
|
|
if space_index > 0:
|
|
timestamp = line[:space_index]
|
|
message = line[space_index + 1:].strip()
|
|
|
|
logger.debug(f"Timestamp extrait: {timestamp}")
|
|
logger.debug(f"Message extrait: {message}")
|
|
|
|
# Vérifier si le timestamp est valide (format ISO 8601)
|
|
if timestamp.endswith('Z'):
|
|
# Convertir le timestamp en format ISO 8601 standard
|
|
timestamp = timestamp.replace('Z', '+00:00')
|
|
|
|
# Vérifier que le format est valide
|
|
if not timestamp.replace('.', '').replace(':', '').replace('-', '').replace('T', '').replace('+', '').isdigit():
|
|
logger.warning(f"Format de timestamp invalide: {timestamp}")
|
|
timestamp = ""
|
|
else:
|
|
logger.warning(f"Timestamp ne se termine pas par Z: {timestamp}")
|
|
timestamp = ""
|
|
|
|
yield {
|
|
"timestamp": timestamp,
|
|
"message": message,
|
|
"stream": "stdout"
|
|
}
|
|
else:
|
|
# Si pas de timestamp valide, envoyer le message sans timestamp
|
|
logger.warning(f"Pas d'espace trouvé dans la ligne: {line}")
|
|
yield {
|
|
"timestamp": "",
|
|
"message": line,
|
|
"stream": "stdout"
|
|
}
|
|
except ValueError as e:
|
|
logger.error(f"Erreur lors du parsing de la ligne: {e}")
|
|
yield {
|
|
"timestamp": "",
|
|
"message": line,
|
|
"stream": "stdout"
|
|
}
|
|
|
|
except UnicodeDecodeError:
|
|
# Fallback sur latin-1 si UTF-8 échoue
|
|
line = log.decode('latin-1').strip()
|
|
yield {
|
|
"timestamp": "",
|
|
"message": line,
|
|
"stream": "stdout"
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors du décodage d'un log : {e}")
|
|
continue
|
|
|
|
except docker.errors.NotFound:
|
|
logger.error(f"Conteneur {container_id} non trouvé")
|
|
except Exception as e:
|
|
logger.error(f"Erreur lors du suivi des logs du conteneur {container_id}: {e}")
|
|
raise
|
|
finally:
|
|
logger.info(f"Arrêt du suivi des logs pour le conteneur {container_id}")
|
|
if 'log_stream' in locals():
|
|
try:
|
|
log_stream.close()
|
|
except:
|
|
pass |