init: docker-migrate universal migration tool
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,221 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
nginx.py — Discovery nginx-конфигов через nginx -T, lsof, ss
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from core.runner import run, run_json, exists
|
||||
from core.color import info, warn, success, log_cmd
|
||||
|
||||
|
||||
def find_nginx_processes():
|
||||
"""Ищет запущенные nginx master/worker процессы"""
|
||||
procs = []
|
||||
if exists("pgrep"):
|
||||
out = run("pgrep -a nginx", check=False)
|
||||
for ln in out.stdout.strip().splitlines():
|
||||
parts = ln.strip().split(None, 1)
|
||||
if parts:
|
||||
pid = parts[0]
|
||||
cmd = parts[1] if len(parts) > 1 else ""
|
||||
procs.append({"pid": pid, "cmd": cmd})
|
||||
return procs
|
||||
|
||||
|
||||
def get_nginx_full_config():
|
||||
"""
|
||||
Получает раскрытый конфиг через nginx -T.
|
||||
Если nginx не запущен/невалиден — ищем через find в /etc/nginx.
|
||||
"""
|
||||
if not exists("nginx"):
|
||||
warn("nginx не установлен")
|
||||
return None
|
||||
|
||||
# Пробуем nginx -T
|
||||
result = run("nginx -T 2>&1", check=False)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
|
||||
warn("nginx -T не удалось (возможно, конфиг невалиден). Fallback на ручной поиск конфигов.")
|
||||
return None
|
||||
|
||||
|
||||
def find_nginx_conf_files():
|
||||
"""Находит все .conf и конфиги nginx для ручного анализа"""
|
||||
candidates = ["/etc/nginx", "/usr/local/etc/nginx", "/opt/local/etc/nginx"]
|
||||
files = []
|
||||
for base in candidates:
|
||||
if os.path.isdir(base):
|
||||
for root, _, fnames in os.walk(base):
|
||||
for f in fnames:
|
||||
if f.endswith(".conf") or f.endswith(".inc"):
|
||||
files.append(os.path.join(root, f))
|
||||
return files
|
||||
|
||||
|
||||
def parse_nginx_config_dump(raw_text, service_ports, service_domain_hints):
|
||||
"""
|
||||
Парсит выхлоп nginx -T (раскрытый конфиг).
|
||||
Ищет server{} блоки, связанные с сервисом по:
|
||||
- proxy_pass на localhost/127.0.0.1 + наш порт
|
||||
- server_name совпадает с доменом из env
|
||||
Возвращает список: {"file": ..., "server_name": ..., "proxy_pass": ..., "ssl_cert": ..., "ssl_key": ...}
|
||||
"""
|
||||
# nginx -T выводит каждый файл с комментарием вида:
|
||||
# # configuration file /etc/nginx/conf.d/site.conf:
|
||||
file_comment_re = re.compile(r"#\s*configuration file\s+(.+?):")
|
||||
|
||||
blocks = []
|
||||
current_file = None
|
||||
current_block_lines = []
|
||||
|
||||
lines = raw_text.splitlines()
|
||||
for ln in lines:
|
||||
m = file_comment_re.match(ln)
|
||||
if m:
|
||||
current_file = m.group(1)
|
||||
continue
|
||||
current_block_lines.append(ln)
|
||||
|
||||
# Простая регулярка: ищем server{ ... } — ловим всё между server { и }
|
||||
# Это не 100%, но для раскрытого конфига достаточно.
|
||||
text = raw_text
|
||||
servers = []
|
||||
# Ищем server-блоки более грубо: поиск от server{ до следующей закрывающей } на нулевом уровне
|
||||
i = 0
|
||||
while i < len(text):
|
||||
idx = text.find("server {", i)
|
||||
if idx == -1:
|
||||
break
|
||||
# найдем закрывающую }
|
||||
start = idx
|
||||
depth = 0
|
||||
j = start
|
||||
while j < len(text):
|
||||
if text[j] == '{':
|
||||
depth += 1
|
||||
elif text[j] == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j += 1
|
||||
block = text[start:j+1]
|
||||
|
||||
# определим файл: ищем ближайший file comment до start
|
||||
file_match = None
|
||||
for m in file_comment_re.finditer(text[:start]):
|
||||
file_match = m.group(1)
|
||||
servers.append({"file": file_match, "block": block})
|
||||
i = j + 1
|
||||
|
||||
related = []
|
||||
# port hints (например, 8000, 443)
|
||||
# domain hints (например, example.com)
|
||||
for s in servers:
|
||||
block = s["block"]
|
||||
score = 0
|
||||
proxy_pass_match = re.search(r"proxy_pass\s+(\S+)", block)
|
||||
sn_match = re.search(r"server_name\s+([^;]+)", block)
|
||||
ssl_cert_match = re.search(r"ssl_certificate\s+([^;]+)", block)
|
||||
ssl_key_match = re.search(r"ssl_certificate_key\s+([^;]+)", block)
|
||||
|
||||
proxy_pass = proxy_pass_match.group(1) if proxy_pass_match else None
|
||||
server_name = sn_match.group(1).strip() if sn_match else None
|
||||
ssl_cert = ssl_cert_match.group(1).strip() if ssl_cert_match else None
|
||||
ssl_key = ssl_key_match.group(1).strip() if ssl_key_match else None
|
||||
|
||||
# Если proxy_pass указывает на 127.0.0.1 или localhost и порт — проверяем
|
||||
if proxy_pass:
|
||||
for hp in service_ports:
|
||||
if str(hp) in proxy_pass:
|
||||
score += 100
|
||||
if "127.0.0.1" in proxy_pass or "localhost" in proxy_pass:
|
||||
score += 20
|
||||
|
||||
# server_name совпадает с доменом
|
||||
if server_name:
|
||||
for hint in service_domain_hints:
|
||||
if hint in server_name:
|
||||
score += 80
|
||||
# wildcard *.domain
|
||||
if hint.startswith("*."):
|
||||
domain = hint.lstrip("*.")
|
||||
if domain in server_name:
|
||||
score += 80
|
||||
|
||||
if score > 0:
|
||||
related.append({
|
||||
"file": s["file"],
|
||||
"server_name": server_name,
|
||||
"proxy_pass": proxy_pass,
|
||||
"ssl_certificate": ssl_cert,
|
||||
"ssl_certificate_key": ssl_key,
|
||||
"raw_block": block,
|
||||
"score": score,
|
||||
})
|
||||
|
||||
return related
|
||||
|
||||
|
||||
def discover_nginx(service_ports, service_domain_hints):
|
||||
"""
|
||||
Главная функция discovery nginx.
|
||||
service_ports: список портов из Docker (например, [8000, 443])
|
||||
service_domain_hints: список доменов из .env или labels.
|
||||
Возвращает list dict.
|
||||
"""
|
||||
info("Ищем связанные nginx-конфиги ...")
|
||||
procs = find_nginx_processes()
|
||||
if procs:
|
||||
success(f"Найден nginx: {len(procs)} процесс(ов)")
|
||||
else:
|
||||
warn("nginx-просессы не найдены")
|
||||
|
||||
raw = get_nginx_full_config()
|
||||
if raw:
|
||||
related = parse_nginx_config_dump(raw, service_ports, service_domain_hints)
|
||||
if related:
|
||||
success(f"Найдено связанных nginx server-блоков: {len(related)}")
|
||||
return related
|
||||
else:
|
||||
# fallback: поиск файлов и grep proxy_pass/server_name
|
||||
files = find_nginx_conf_files()
|
||||
related = []
|
||||
for f in files:
|
||||
try:
|
||||
content = open(f, "r", encoding="utf-8", errors="ignore").read()
|
||||
except Exception:
|
||||
continue
|
||||
score = 0
|
||||
for hp in service_ports:
|
||||
if f":{hp}" in content or f"127.0.0.1:{hp}" in content:
|
||||
score += 100
|
||||
for d in service_domain_hints:
|
||||
if d in content:
|
||||
score += 80
|
||||
if score > 0:
|
||||
related.append({
|
||||
"file": f,
|
||||
"score": score,
|
||||
"method": "fallback_grep",
|
||||
})
|
||||
return related
|
||||
|
||||
|
||||
def get_nginx_systemd_unit():
|
||||
"""Ищет unit-файл nginx"""
|
||||
units = []
|
||||
for unit in ("nginx.service", "nginx"):
|
||||
out = run(f"systemctl is-active {unit}", check=False)
|
||||
if out.returncode == 0 or out.stdout.strip() in ("active", "inactive"):
|
||||
# Найдём файл unit
|
||||
try:
|
||||
uout = run(f"systemctl show {unit} -p FragmentPath --value", check=False)
|
||||
path = uout.stdout.strip()
|
||||
if path:
|
||||
units.append({"unit": unit, "path": path})
|
||||
except Exception:
|
||||
pass
|
||||
return units
|
||||
Reference in New Issue
Block a user