- 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
238 lines
8.0 KiB
Python
238 lines
8.0 KiB
Python
# -*- 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
|