init: docker-migrate universal migration tool

This commit is contained in:
2026-05-22 19:58:09 +04:00
commit ddb2ae9501
34 changed files with 2431 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
# -*- 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
# 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