init: docker-migrate universal migration tool
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user