Files
docker-migrate/discover/docker.py
T
Stitch505 97b3623915 bugfix: comprehensive stability audit and fixes
- discover/docker.py: handle comma-separated compose_file in labels
- discover/network.py: replace os.getlogin() with robust user detection
- target.py: add lsb_release fallback via hostnamectl, guard None compose_file,
  use container name (not cached CID) for docker logs
- main.py: call reset_state(mode='target') before target mode,
  improve EOF handling info message
- source.py: remove redundant set_stage('DONE') inside transfer_offer
- transfer.py: fix stage naming for resume after transfer
- add dry_run.py for local logic validation
2026-05-22 20:58:37 +04:00

238 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
docker.py — Discovery Docker контейнеров, compose, volumes, сети
"""
import json
import os
import re
from core.runner import run, run_json, exists
from core.color import info, warn, success, log_cmd
def find_container(name_or_id):
"""
Ищет контейнер по имени (частичное совпадение) или ID.
Возвращает tuple (container_id, container_name, inspect_dict).
"""
if not exists("docker"):
raise RuntimeError("Docker не найден на сервере")
# Сначала exact id или name
try:
out = run_json(f"docker inspect --type=container {name_or_id}")
if out and len(out) > 0:
c = out[0]
return c["Id"], c["Name"].lstrip("/"), c
except RuntimeError:
pass
# Частичный поиск по имени
ps_out = run("docker ps -a --format json", check=False)
lines = [ln for ln in ps_out.stdout.strip().splitlines() if ln.strip()]
matches = []
for ln in lines:
try:
item = json.loads(ln)
names = item.get("Names", "")
image = item.get("Image", "")
cid = item.get("ID", "")
if name_or_id.lower() in names.lower() or name_or_id.lower() in image.lower():
matches.append((cid, names))
except json.JSONDecodeError:
continue
if not matches:
raise RuntimeError(f"Контейнер '{name_or_id}' не найден среди docker ps -a")
if len(matches) == 1:
cid, cname = matches[0]
out = run_json(f"docker inspect {cid}")
c = out[0]
return c["Id"], c["Name"].lstrip("/"), c
else:
from core.color import prompt
print("Найдено несколько контейнеров:")
for i, (cid, cname) in enumerate(matches, 1):
print(f" {i}. {cname} ({cid[:12]})")
sel = prompt("Выберите номер")
idx = int(sel) - 1
cid, cname = matches[idx]
out = run_json(f"docker inspect {cid}")
c = out[0]
return c["Id"], c["Name"].lstrip("/"), c
def get_compose_file_from_container(inspect):
"""
Пытается найти compose-файл по Labels или по рабочей директории контейнера.
"""
labels = inspect.get("Config", {}).get("Labels", {}) or {}
# Compose labels: com.docker.compose.project.working_dir
working_dir = labels.get("com.docker.compose.project.working_dir")
compose_file = labels.get("com.docker.compose.project.config_files")
if compose_file and os.path.isfile(compose_file):
return compose_file
# Если labels содержит несколько файлов через запятую — берём первый существующий
if compose_file and ", " in compose_file:
for fp in compose_file.split(", "):
fp = fp.strip()
if os.path.isfile(fp):
return fp
elif compose_file and "," in compose_file:
for fp in compose_file.split(","):
fp = fp.strip()
if os.path.isfile(fp):
return fp
# Fallback: ищем docker-compose.yml / compose.yml рядом с working_dir
if working_dir and os.path.isdir(working_dir):
for fname in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
fp = os.path.join(working_dir, fname)
if os.path.isfile(fp):
return fp
# Fallback по Env (PWD если контейнер запущен через compose)
env = inspect.get("Config", {}).get("Env", [])
pwd = None
for e in env:
if e.startswith("PWD="):
pwd = e.split("=", 1)[1]
break
if pwd and os.path.isdir(pwd):
for fname in ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"):
fp = os.path.join(pwd, fname)
if os.path.isfile(fp):
return fp
return None
def get_env_file(compose_path, inspect):
"""
Ищет .env рядом с compose-файлом или в working_dir.
"""
candidates = []
if compose_path:
candidates.append(os.path.join(os.path.dirname(compose_path), ".env"))
labels = inspect.get("Config", {}).get("Labels", {}) or {}
wd = labels.get("com.docker.compose.project.working_dir")
if wd:
candidates.append(os.path.join(wd, ".env"))
env_list = inspect.get("Config", {}).get("Env", [])
pwd = None
for e in env_list:
if e.startswith("PWD="):
pwd = e.split("=", 1)[1]
break
if pwd:
candidates.append(os.path.join(pwd, ".env"))
for c in candidates:
if os.path.isfile(c):
return c
return None
def get_mounts_and_volumes(inspect):
"""
Возвращает список dict: {type: bind|volume|tmpfs, source, destination, mode}
"""
mounts = inspect.get("Mounts", []) or []
result = []
for m in mounts:
result.append({
"type": m.get("Type", "unknown"),
"source": m.get("Source", ""),
"destination": m.get("Destination", ""),
"mode": m.get("Mode", ""),
})
return result
def get_ports(inspect):
"""
Возвращает словарь host_port -> container_port/proto.
"""
hostconfig = inspect.get("HostConfig", {})
port_bindings = hostconfig.get("PortBindings", {}) or {}
result = {}
for container_port_proto, bindings in port_bindings.items():
if bindings:
for b in bindings:
hp = b.get("HostPort")
ha = b.get("HostIp", "0.0.0.0")
result[f"{ha}:{hp}"] = container_port_proto
return result
def get_networks(inspect):
nets = inspect.get("NetworkSettings", {}).get("Networks", {}) or {}
return list(nets.keys())
def get_host_config_flags(inspect):
hc = inspect.get("HostConfig", {})
return {
"network_mode": hc.get("NetworkMode"),
"privileged": hc.get("Privileged"),
"cap_add": hc.get("CapAdd", []),
"devices": hc.get("Devices", []),
"restart_policy": hc.get("RestartPolicy", {}),
"pid_mode": hc.get("PidMode"),
"ipc_mode": hc.get("IpcMode"),
}
def get_image(inspect):
return inspect.get("Config", {}).get("Image", inspect.get("Image", ""))
def discover_docker(name_or_id):
"""
Главная функция. Возвращает dict с полной информацией docker-части.
"""
info(f"Ищем Docker контейнер: {name_or_id} ...")
cid, cname, inspect = find_container(name_or_id)
success(f"Найден контейнер: {cname} ({cid[:12]})")
compose_file = get_compose_file_from_container(inspect)
env_file = get_env_file(compose_file, inspect) if compose_file else None
mounts = get_mounts_and_volumes(inspect)
ports = get_ports(inspect)
networks = get_networks(inspect)
hostcfg = get_host_config_flags(inspect)
image = get_image(inspect)
data = {
"container_id": cid,
"container_name": cname,
"image": image,
"status": inspect.get("State", {}).get("Status", "unknown"),
"compose_file": compose_file,
"env_file": env_file,
"mounts": mounts,
"ports": ports,
"networks": networks,
"host_config": hostcfg,
"labels": inspect.get("Config", {}).get("Labels", {}),
}
if compose_file:
success(f"Найден compose-файл: {compose_file}")
else:
warn("Compose-файл не найден автоматически (возможно, запуск через docker run)")
if env_file:
success(f"Найден .env: {env_file}")
return data
def get_container_pid(cid):
"""Возвращает PID контейнера на хосте (для nsenter)"""
try:
out = run(f"docker inspect -f '{{{{.State.Pid}}}}' {cid}", check=False)
return int(out.stdout.strip())
except Exception:
return None