netns: nsenter в topology.sh, netns_common и документации (#56)

- topology.sh: ns_exec для всех ip -n <ns> и ip netns exec, коды 0/1/3/4.
- netns_common.py: ns_exec_mode()/ns_exec_direct(), addrs/links/link_is_up через ns_sh.
- docker-compose.test.yml: проброс AWG_NS_EXEC_MODE в netns-сервис.
- tests/netns/README.md: режимы входа в netns, коды возврата, новый pitfall.

Refs #21, Refs #15, Refs #56
This commit is contained in:
2026-10-28 21:30:22 +03:00
parent f0847001ee
commit 4aa69132ad
4 changed files with 109 additions and 32 deletions
+57 -5
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import json
import os
import pathlib
import shutil
import subprocess
import time
from typing import Any, Iterable
@@ -61,6 +62,10 @@ class StandError(RuntimeError):
"""Ошибка стенда (не теста): топология/утилиты не отвечают как ожидается."""
# Кэш выбранного режима входа в netns (ip|nsenter), см. ns_exec_mode().
_NS_EXEC_MODE: str | None = None
def sh(args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subprocess.CompletedProcess:
"""Запуск команды в корневом netns."""
cp = subprocess.run(
@@ -75,9 +80,56 @@ def sh(args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subpro
return cp
def ns_exec_direct(
mode: str,
ns: str,
args: Iterable[str],
check: bool = True,
timeout: float = 20.0,
) -> subprocess.CompletedProcess:
"""Одна попытка входа в netns выбранным способом (ip|nsenter)."""
cmd = (["nsenter", f"--net=/var/run/netns/{ns}"] if mode == "nsenter" else ["ip", "netns", "exec", ns]) + list(args)
return sh(cmd, check=check, timeout=timeout)
def _probe_ns_exec(mode: str) -> bool:
probe = os.environ.get("AWG_NETNS_PREFIX", "awgt21") + "-probe"
if sh(["ip", "netns", "add", probe], check=False).returncode != 0:
return False
try:
return ns_exec_direct(mode, probe, ["sh", "-c", "true"], check=False, timeout=10.0).returncode == 0
finally:
sh(["ip", "netns", "delete", probe], check=False)
def ns_exec_mode() -> str:
"""Режим входа в netns: ip | nsenter (issue #56).
`ip netns exec` (iproute2 6.x) перемонтирует /sys и в контейнере падает с
"mount of /sys failed: Operation not permitted"; nsenter входит в
/var/run/netns/<ns> без перемонтирования. Режим кэшируется на процесс,
AWG_NS_EXEC_MODE=ip|nsenter|auto переопределяет выбор.
"""
global _NS_EXEC_MODE
if _NS_EXEC_MODE is None:
want = os.environ.get("AWG_NS_EXEC_MODE", "auto")
if want in ("ip", "nsenter"):
_NS_EXEC_MODE = want
elif _probe_ns_exec("ip"):
_NS_EXEC_MODE = "ip"
elif shutil.which("nsenter") and _probe_ns_exec("nsenter"):
_NS_EXEC_MODE = "nsenter"
else:
raise StandError(
"ни 'ip netns exec', ни 'nsenter --net' не работают в этом окружении "
"(нет CAP_SYS_ADMIN/NET_ADMIN) — issue #56"
)
return _NS_EXEC_MODE
def ns_sh(ns: str, args: Iterable[str], check: bool = True, timeout: float = 20.0) -> subprocess.CompletedProcess:
"""Запуск команды внутри указанного netns."""
return sh(["ip", "netns", "exec", ns, *args], check=check, timeout=timeout)
"""Запуск команды внутри указанного netns (ip или nsenter — см. ns_exec_mode)."""
return ns_exec_direct(ns_exec_mode(), ns, args, check=check, timeout=timeout)
def ns_exists(ns: str) -> bool:
@@ -86,15 +138,15 @@ def ns_exists(ns: str) -> bool:
def addrs(ns: str) -> str:
return sh(["ip", "-n", ns, "-br", "addr", "show"]).stdout
return ns_sh(ns, ["ip", "-br", "addr", "show"]).stdout
def links(ns: str) -> str:
return sh(["ip", "-n", ns, "-br", "link", "show"]).stdout
return ns_sh(ns, ["ip", "-br", "link", "show"]).stdout
def link_is_up(ns: str, ifname: str) -> bool:
cp = sh(["ip", "-n", ns, "-br", "link", "show", ifname], check=False)
cp = ns_sh(ns, ["ip", "-br", "link", "show", ifname], check=False)
return cp.returncode == 0 and "UP" in cp.stdout