init: docker-migrate universal migration tool
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
network.py — Discovery сети хоста, loopback-proxy, sidecar, routes, iptables, sysctl
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import socket
|
||||
import ipaddress
|
||||
from core.runner import run, exists
|
||||
from core.color import info, warn, success, log_cmd
|
||||
|
||||
|
||||
def get_container_host_connections(cid):
|
||||
"""
|
||||
Смотрит внутри контейнера (через nsenter -t <pid> -n) установленные/слушающие соединения.
|
||||
Возвращает список: {proto, local, remote, estado}
|
||||
"""
|
||||
from discover.docker import get_container_pid
|
||||
pid = get_container_pid(cid)
|
||||
if not pid:
|
||||
return []
|
||||
|
||||
# Проверим ss внутри netns контейнера
|
||||
out = run(f"nsenter -t {pid} -n ss -tlnp", check=False)
|
||||
# и established тоже
|
||||
out2 = run(f"nsenter -t {pid} -n ss -tnp state established", check=False)
|
||||
|
||||
results = []
|
||||
for src in (out, out2):
|
||||
for ln in src.stdout.splitlines():
|
||||
parts = ln.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
# Формат: State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
|
||||
try:
|
||||
proto = "tcp"
|
||||
local = parts[3]
|
||||
remote = parts[4] if len(parts) > 4 else None
|
||||
state = parts[0]
|
||||
results.append({"proto": proto, "local": local, "remote": remote, "state": state})
|
||||
except Exception:
|
||||
continue
|
||||
return results
|
||||
|
||||
|
||||
def find_listeners_on_host(port):
|
||||
"""На хосте ищет, кто слушает указанный TCP порт"""
|
||||
listeners = []
|
||||
out = run(f"ss -tlnp 'sport = :{port}'", check=False)
|
||||
for ln in out.stdout.splitlines():
|
||||
# На некоторых системах столбец users содержит pid/process
|
||||
# Пример: LISTEN 0 128 127.0.0.1:40000 0.0.0.0:* users:(("warp-svc",pid=1234,fd=5))
|
||||
m = re.search(r'users:\(\("([^"]+)"', ln)
|
||||
if m:
|
||||
proc = m.group(1)
|
||||
listeners.append({"process": proc, "line": ln.strip()})
|
||||
else:
|
||||
# fallback: ищем pid через lsof
|
||||
lof = run(f"lsof -i TCP:{port} -sTCP:LISTEN -t", check=False)
|
||||
if lof.stdout.strip():
|
||||
for pid in lof.stdout.strip().split():
|
||||
try:
|
||||
cmdline = open(f"/proc/{pid}/comm", "r").read().strip()
|
||||
listeners.append({"pid": pid, "process": cmdline, "line": ln.strip()})
|
||||
except Exception:
|
||||
pass
|
||||
return listeners
|
||||
|
||||
|
||||
def find_sidecar_processes(cid, container_ports):
|
||||
"""
|
||||
Ищет sidecar-процессы на хосте, к которым контейнер стучится через loopback.
|
||||
"""
|
||||
info("Ищем loopback-соединения контейнера (sidecar / proxy / WARP) ...")
|
||||
conns = get_container_host_connections(cid)
|
||||
sidecars = []
|
||||
|
||||
for c in conns:
|
||||
local = c.get("local", "")
|
||||
if "127.0.0.1" in local or "localhost" in local or "::1" in local:
|
||||
# Извлекаем порт
|
||||
m = re.search(r':(\d+)$', local)
|
||||
if m:
|
||||
port = int(m.group(1))
|
||||
listeners = find_listeners_on_host(port)
|
||||
if listeners:
|
||||
for l in listeners:
|
||||
info(f" Контейнер подключается к {local} → процесс на хосте: {l.get('process', '?')} (pid={l.get('pid', '?')})")
|
||||
sidecars.append({
|
||||
"type": "loopback_listener",
|
||||
"container_port_target": port,
|
||||
"host_process": l.get("process"),
|
||||
"host_pid": l.get("pid"),
|
||||
"method": "ss_lsof",
|
||||
})
|
||||
if sidecars:
|
||||
success(f"Найдено sidecar/loopback зависимостей: {len(sidecars)}")
|
||||
return sidecars
|
||||
|
||||
|
||||
def get_process_details(pid):
|
||||
"""Собирает детали о процессе: exe, cmdline, cwd, open files, systemd unit"""
|
||||
try:
|
||||
base = f"/proc/{pid}"
|
||||
exe = os.readlink(f"{base}/exe") if os.path.islink(f"{base}/exe") else None
|
||||
cmdline = open(f"{base}/cmdline", "rb").read().replace(b'\x00', b' ').decode("utf-8", "ignore").strip()
|
||||
cwd = os.readlink(f"{base}/cwd") if os.path.islink(f"{base}/cwd") else None
|
||||
# open files
|
||||
fds = os.listdir(f"{base}/fd")
|
||||
files = []
|
||||
for fd in fds:
|
||||
try:
|
||||
p = os.readlink(f"{base}/fd/{fd}")
|
||||
if p.startswith("/") and not p.startswith("/dev/") and not p.startswith("/proc/"):
|
||||
files.append(p)
|
||||
except Exception:
|
||||
pass
|
||||
# systemd unit
|
||||
unit = None
|
||||
try:
|
||||
cgroup = open(f"{base}/cgroup", "r").read()
|
||||
for ln in cgroup.splitlines():
|
||||
if ".service" in ln:
|
||||
parts = ln.split(":")
|
||||
if len(parts) >= 3:
|
||||
# 0::/system.slice/nginx.service
|
||||
m = re.search(r'/([^/]+\.service)$', parts[-1])
|
||||
if m:
|
||||
unit = m.group(1)
|
||||
except Exception:
|
||||
pass
|
||||
return {"exe": exe, "cmdline": cmdline, "cwd": cwd, "files": list(set(files)), "unit": unit}
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def gather_host_network_info():
|
||||
"""
|
||||
Собирает текущее состояние сети хоста для manifest.
|
||||
"""
|
||||
info("Собираем сетевую конфигурацию хоста ...")
|
||||
data = {}
|
||||
|
||||
# routes
|
||||
if exists("ip"):
|
||||
r = run("ip route show", check=False)
|
||||
data["ip_routes"] = r.stdout.strip().splitlines()
|
||||
r2 = run("ip rule show", check=False)
|
||||
data["ip_rules"] = r2.stdout.strip().splitlines()
|
||||
|
||||
# interfaces
|
||||
if exists("ip"):
|
||||
r = run("ip addr show", check=False)
|
||||
data["ip_addr"] = r.stdout.strip().splitlines()
|
||||
|
||||
# iptables
|
||||
data["iptables"] = {}
|
||||
for table in ("filter", "nat", "mangle", "raw"):
|
||||
r = run(f"iptables -t {table} -S", check=False)
|
||||
data["iptables"][table] = r.stdout.strip().splitlines()
|
||||
|
||||
# nftables
|
||||
if exists("nft"):
|
||||
r = run("nft list ruleset", check=False)
|
||||
data["nftables"] = r.stdout.strip().splitlines()
|
||||
|
||||
# sysctl отличия от дефолта (берём всё, потом diff-анализ можно делать вручную)
|
||||
r = run("sysctl -a", check=False)
|
||||
data["sysctl"] = r.stdout.strip().splitlines()
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def find_systemd_units_related(procs):
|
||||
"""
|
||||
Ищет systemd unit-файлы для процессов (sidecar и других).
|
||||
"""
|
||||
units = []
|
||||
seen = set()
|
||||
for p in procs:
|
||||
# p может быть dict с pid
|
||||
pid = p.get("pid")
|
||||
if not pid:
|
||||
continue
|
||||
# systemd unit через systemctl status
|
||||
try:
|
||||
out = run(f"systemctl status {pid}", check=False)
|
||||
# строка вида: ... Loaded: loaded (/lib/systemd/system/xxx.service; ...)
|
||||
for ln in out.stdout.splitlines():
|
||||
if "Loaded:" in ln and ".service" in ln:
|
||||
m = re.search(r'loaded\s+\(([^)]+)\)', ln)
|
||||
if m:
|
||||
path = m.group(1).split(";")[0].strip()
|
||||
name = os.path.basename(path)
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
units.append({"name": name, "path": path, "related_to": p.get("process", "?")})
|
||||
except Exception:
|
||||
pass
|
||||
return units
|
||||
|
||||
|
||||
def gather_cron_jobs(user_hint=None):
|
||||
"""
|
||||
Ищет cron-задания, связанные с сервисом (по имени или пути).
|
||||
Если user_hint — список подсказок (название сервиса, путь), ищем по ним.
|
||||
"""
|
||||
jobs = []
|
||||
# crontab -l для текущего пользователя и root
|
||||
for user in (os.getlogin(), "root"):
|
||||
try:
|
||||
out = run(f"crontab -u {user} -l", check=False)
|
||||
for ln in out.stdout.splitlines():
|
||||
if ln.strip().startswith("#"):
|
||||
continue
|
||||
if user_hint:
|
||||
for hint in user_hint:
|
||||
if hint.lower() in ln.lower():
|
||||
jobs.append({"user": user, "line": ln.strip()})
|
||||
break
|
||||
else:
|
||||
jobs.append({"user": user, "line": ln.strip()})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# /etc/cron.d, /etc/cron.hourly, etc.
|
||||
cron_dirs = ["/etc/cron.d", "/etc/cron.hourly", "/etc/cron.daily", "/etc/cron.weekly", "/etc/cron.monthly"]
|
||||
for d in cron_dirs:
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
for f in os.listdir(d):
|
||||
fp = os.path.join(d, f)
|
||||
try:
|
||||
content = open(fp, "r", encoding="utf-8", errors="ignore").read()
|
||||
if user_hint:
|
||||
for hint in user_hint:
|
||||
if hint.lower() in content.lower():
|
||||
jobs.append({"file": fp, "content": content[:500]})
|
||||
break
|
||||
else:
|
||||
jobs.append({"file": fp, "content": content[:500]})
|
||||
except Exception:
|
||||
pass
|
||||
return jobs
|
||||
Reference in New Issue
Block a user