# Проверка dev/staging стенда (#21) — фактические прогоны на Docker-хосте Дата: 2026-10-22 · Хост: `192.168.1.147` (Debian 13, Docker 29.4.1, compose v5.1.3, kernel 6.17) Ветка: `feat/21-dev-stand`, HEAD `d63b9cea` · Ref: #21, #15, #17, #18, #31, #56 ## 0. Стенд запускает именно то, что в ветке Каталог стенда `(/opt/awg-dash-dev/repo)` не является git-чекаутом (`git clone` по HTTP обрывается на этом Gitea — `fetch-pack: invalid index-pack output`), поэтому файлы сверены побайтово через git-blob-SHA с деревом HEAD: | файл | git blob (HEAD == хост) | |---|---| | `Makefile` | `638ec381f3d1e57a8ab84f194e40ff6a0c51d8cf` | | `deploy/docker-compose.dev.yml` | `6bed89a5ea2a197aa9f6fd6bcda6d8131303114d` | | `deploy/docker-compose.stub.yml` | `936efb38dfff4ba8b942c9757acd4ba9c39c4064` | | `deploy/docker-compose.test.yml` | `b628c129ae4de9b33875503b2c8273675b235a84` | | `deploy/verify-dev-stand.sh` | `c4e13f11f9023700be531ebb312ad33d7beea4d8` | | `deploy/api_stub.py` | `93663113f118676a7e896e0e9d32bd20a21f6a0e` | | `tests/deploy/validate_compose.py` | `0fa8b3c02675a364f4f5b447220680a7818faa5a` | | `tests/deploy/pytest.ini` | `7299b6b45487dd4f214f0444e2f0f5c237eb2fac` | | `tests/deploy/.coveragerc` | `9b41183e29a155b5498c3722cf4639e9c69268c6` | | `tests/deploy/test_api_stub.py` | `1d5cec3a72b02da6cb48a6ca433e7699b768f631` | | `tests/deploy/test_validate_compose.py` | `fb045949848cb70a1880614201930c2d4ecd9d0a` | | `tests/netns/lib.sh` | `806375299b7c3932d20b275d98b72f65e48940ec` | | `tests/netns/run.sh` | `c6797bf9c42abe7c4f72cf5402af82e2192ee867` | | `tests/netns/topology.sh` | `16aa764264d4697750bf02c53f11d3e2a5a2dcc5` | | `tests/netns/netns_common.py` | `9d3bfd94cd8e8b321af92274835b3f0ebc48c842` | | `tests/netns/README.md` | `845863188ffb26c1aae7e475addc0c511e0d8f1f` | | остальные файлы `tests/netns/` (5) | без изменений с раунда 1 | ## 1. Предварительные пакеты на стенд-хосте ```bash apt-get install -y make python3-yaml python3-pytest python3-pytest-cov shellcheck ``` (`make` на хосте отсутствовал — без него гейты `make *` невоспроизводимы; пакеты добавлены сверх существующего набора, ничего не удалялось и не переконфигурировалось.) ## 2. Гейты (реальные прогоны) Лог целиком: `/opt/awg-dash-dev/verify-final-1792617226.log` (на хосте). | гейт | команда | результат | |---|---|---| | манифесты | `make test-manifests` | exit 0, `validate-compose: ok (1 предупреждений)`¹ | | статический анализ шелла | `make lint-shell` | exit 0 | | unit-тесты манифестов | `python3 -m pytest tests/deploy -q --no-cov` | exit 0 | | netns-харнесс (AC-3) | `make test-netns` | exit 0, `8 ok / 0 fail / 0 err`, отчёт `/tmp/awg-netns/report.json` | | AC-1 (health 200) | `make dev-up` + `curl :8180/api/v1/health` | HTTP 200, контейнер `healthy` | | AC-2 (состояние переживает рестарт) | `make dev-verify` | `[ ok ] AC-1`, `[ ok ] AC-2` ×2, exit 0 | | логи | `make dev-logs` | стрим логов, есть `[api-stub] ... GET /api/v1/health 200` | ¹ предупреждение: `build.dockerfile deploy/Dockerfile отсутствует в дереве (ожидаемо до мержа #18)`. Образ `awg-dash:dev` для стенда собран из Dockerfile PR #49 (ветка `feat/18-docker-runtime`), в дерево этой ветки он не входит: `make build` вернёт внятную ошибку до мержа #18. Валидатор переключается в режим ошибки флагом `make test-manifests MANIFEST_ARGS=--require-dockerfile` — этот режим нужно включить в CI гейте после мержа #18. ### AC-1 — детали ``` $ make dev-up # (авто-режим: backend/awg_api/app.py отсутствует (#31) → + deploy/docker-compose.stub.yml) $ curl -s -w '\nHTTP=%{http_code}\n' http://127.0.0.1:8180/api/v1/health {"issue": "#31", "note": "заглушка dev-стенда: реальный API появится в #31", "status": "ok", "stub": true, "stub_env": "1", "uptime_s": 14.909} HTTP=200 $ docker ps --filter name=awg-dash-dev awg-dash-dev Up 15 seconds (healthy) 0.0.0.0:8180->8080/tcp, [::]:8180->8080/tcp ``` Почему с заглушкой: FastAPI-каркас (#31) ещё не реализован, сам образ HTTP не поднимает (entrypoint пишет `awg_api/app.py ещё не реализован` и ждёт). Чтобы AC-1 #21 проверялся на живом compose-стенде уже в Sprint 0, `make dev-up` добавляет оверрайд `docker-compose.stub.yml` **только пока `backend/awg_api/app.py` отсутствует**; ответ явно помечен `"stub": true`. После мержа #31 оверрайд не добавляется, `make dev-up` поднимает реальный API без правок Makefile. ### AC-2 — детали ``` $ make dev-verify [verify][ ok ] AC-1: http://127.0.0.1:8180/api/v1/health -> 200 (api=stub) [verify] AC-2: пишу маркеры в /config и /data и перезапускаю контейнер [verify][ ok ] AC-2: /config и /data пережили restart и пересоздание контейнера [verify][ ok ] AC-2: маркеры видны на хосте (/opt/awg-dash-dev/{config,data}) — это bind mount, а не слой контейнера $ ls /opt/awg-dash-dev/config /opt/awg-dash-dev/data /opt/awg-dash-dev/config/.ac2-marker (24 байта) /opt/awg-dash-dev/data/.ac2-marker (24 байта) ``` Проверяется два сценария: `docker compose restart` (как в AC) и `up -d --force-recreate` (сильнее). ### AC-3 (netns) — детали ``` [netns] создаю netns: client=awgt21-client gw=awgt21-gw uplink=awgt21-uplink [ ok ] стенд поднят: awgt21-client(10.213.0.2) -> awgt21-gw(10.213.0.1/10.214.0.1) -> awgt21-uplink(10.214.0.2) [ ok ] policy routing: fwmark 51820 -> table 51820 (default via 10.214.0.2) [ ok ] mock endpoint: udp/51820 в awgt21-uplink; детектор утечки: 198.51.100.9:9999/udp Ran 8 tests in 5.447s — OK test_01_namespaces_and_links ... test_08_killswitch_blocks_direct_egress — все ok [ ok ] топология снесена, namespace'ы удалены (ip netns list пусто) ``` `report.json`: `{"suite": "tests/netns", "tests": 8, "passed": 8, "failures": 0, "errors": 0, "skipped": 0, "returncode": 0, "duration_s": 5.447}` ## 3. Что чинилось в этой итерации | проблема | причина | правка | |---|---|---| | `make dev-up` давал `unhealthy` и AC-1 не выполнялся | API (#31) не реализован | `Makefile`: авто-подключение stub-оверрайда, пока нет `backend/awg_api/app.py`; `dev-up-app` / `dev-up-stub` для явных режимов | | `make dev-verify` печатал `[verify][fail] нет монтирования /config` при реально сохранённых томах и падал | `verify-dev-stand.sh` кэшировал id контейнера и делал `docker inspect` по нему после `--force-recreate` → `no such object` | перезапрос id (`cid()`) после пересоздания и явный `fatal_env` вместо ложного fail | | `dev-up` падал без `deploy/Dockerfile` (`--build`) | Dockerfile живёт в #18, ветка не смёржена | `--build` форсируется только при наличии `deploy/Dockerfile` | ## 4. Ограничения и что делать дальше 1. **CI-гейты (DoD) не автоматизированы:** Gitea Actions runner для `embeddevio/awg-dash` не зарегистрирован (см. отчёт в #17, run #1 остаётся `queued`). Все гейты в таблице §2 прогнаны **вручную** на стенд-хосте; после регистрации runner'а в #17 они переносятся в workflow без изменений команд. 2. **Порт:** на `192.168.1.147` хост-порт 8080 занят (`drawio`), поэтому стенд публикуется как `8180:8080` (контейнерный порт приложения — 8080, `AWG_DASH_PORT` переопределяет хост-порт). 3. **Сборка образа** — только после мержа #18; до этого образ `awg-dash:dev` собирается из ветки #18 вручную. 4. **Prod/staging:** stub-оверрайд и `deploy/api_stub.py` — DEV-ONLY. В staging/prod поднимается только `docker-compose.dev.yml` (или прод-манифест) без stub-файла. ## 5. Откат / очистка ```bash make dev-down # остановить стенд, каталоги /opt/awg-dash-dev/{config,data} сохраняются make dev-reset # + удалить осиротевшие контейнеры/сети проекта make test-netns # netns-харнесс сносит топологию сам; аварийно: bash tests/netns/topology.sh down ``` ## 6. Раунд 2 (issue #56): netns в контейнере и покрытие тестов Дата: 2026-10-28 · Хост: `192.168.1.147` · Ветка: `feat/21-dev-stand` · Ref: #21, #15, #17, #56 Ревью qa-engineer (раунд 1) нашло два блокера: `make test-netns-ci` падал в контейнере (`mount of /sys failed: Operation not permitted`, exit 255 вместо 0/1/3/4) и гейт покрытия `pytest tests/deploy` обходился флагом `--no-cov` (0.00 %, `deploy/api_stub.py` 0 %). ### 6.1 Причина падения netns в контейнере Воспроизведено на образе `awg-dash:dev` (iproute2 6.11.0): | команда в контейнере | результат | |---|---| | `ip netns add ` | rc=0 | | `ip netns exec true` | `mount of /sys failed: Operation not permitted`, rc=255 | | `ip -n link set lo up` | `mount of /sys failed: Operation not permitted`, rc=255 | | `nsenter --net=/var/run/netns/ true` | rc=0 | `ip netns exec` и `ip -n` перемонтируют `/sys` внутри namespace; в контейнере это падает. Поэтому харнесс больше не использует эти формы: вход в namespace идёт через `ns_exec()` / `ns_prefix()` (`tests/netns/lib.sh`) и `netns_common.ns_sh()`, которые один раз пробуют `ip netns exec`, а при отказе переключаются на `nsenter --net=...` (`AWG_NS_EXEC_MODE=auto|ip|nsenter`). Все `ip -n "$NS"` заменены на `ns_exec "$NS" ip ...`. Коды возврата приведены к контракту `0/1/3/4` (`run.sh`, `topology.sh`): неожиданный код утилиты (255) нормализуется в `4` («ошибка стенда»), нет прав — `3`, упавшие тесты — `1`. ### 6.2 Реальные прогоны | прогон | команда | результат | |---|---|---| | стенд на хосте | `make test-netns` | exit 0, 8 ok / 0 fail, 5.46 s, `ip netns list` пуст, мок и sink не остались в процессах | | стенд в контейнере (путь CI) | `make test-netns-ci` | exit 0, `[netns] вход в netns: nsenter`, backend `iptables`, 8 ok / 0 fail, 5.50 s, `[ ok ] топология снесена`, отчёт `/artifacts/netns-report.json` | | нет прав | `docker run` без `--privileged` | exit **3**, `[blocked] не удалось создать netns — нет CAP_NET_ADMIN/SYS_ADMIN` | | форс нерабочего режима | `AWG_NS_EXEC_MODE=ip` в контейнере | exit **4**, `mount of /sys failed` → `[warn] run.sh: непредвиденный код 255 — трактую как ошибку стенда (4)` | | упавшие тесты | синтетический проваленный тест в контейнере | exit **1**, `[warn] netns-стенд: тесты/очистка упали (код 1)` | | гейт покрытия | `make test-deploy` | exit 0, 51 passed, coverage **98.86 %** (`deploy/api_stub.py` 98 %, `tests/deploy/validate_compose.py` 99 %) | | манифесты / shellcheck | `make test-manifests`, `make lint-shell` | exit 0 / exit 0 | ### 6.3 Что изменено для гейта покрытия * `tests/deploy/pytest.ini` — отдельный профиль: `--cov=deploy --cov=tests/deploy --cov-config=tests/deploy/.coveragerc --cov-fail-under=70`. Раньше `pytest tests/deploy` наследовал addopts корневого `pyproject.toml` (`--cov=awg_agent --cov=awg_api`), поэтому покрытие всегда было 0.00 % и RC=1 — гейт гоняли с `--no-cov` в обход порога. * `tests/deploy/.coveragerc` — из покрытия исключены сами тестовые модули (`tests/deploy/test_*.py`), иначе «покрытие» измеряет тестовый код. * `tests/deploy/test_api_stub.py` — 9 тестов на живой HTTP-сервер заглушки: 200 и `"stub": true` на `/api/v1/health`, алиас `/healthz`, игнор query-string, 404 `not_implemented` на прочие пути, HEAD, отказ на POST, разбор аргументов `main()`. * `tests/deploy/test_validate_compose.py` — 39 негативных/граничных тестов валидатора: каждая инварианта AC #21 ломается по отдельности (порт, тома, healthcheck, NET_ADMIN, `/dev/net/tun`, `ip_forward`, privileged netns-сервис, DEV-ONLY-метка, `--require-dockerfile`). * `deploy/api_stub.py` — `make_server()` выделен из `main()` (тесты поднимают сервер на порту 0). * `make test-deploy` — гейт покрытия; `make test-netns-ci` прокидывает `AWG_NS_EXEC_MODE`. ### 6.4 Что закрыто и что осталось Закрыты оба блокера раунда 1 (`make test-netns-ci` работает в контейнере, покрытие измеряется и ≥70 %). `CI green` по-прежнему недостижим: Gitea Actions runner не зарегистрирован (#17), все прогоны в таблице 6.2 — ручные на `192.168.1.147`; команды `make test-netns-ci` и `make test-deploy` — ровно те, что должны попасть в workflow #17. Порт и stub-оверрайд (см. §4) без изменений: `privileged: true` и `cap_add SYS_MODULE` из `deploy/docker-compose.test.yml` — только DEV/TEST, для staging/prod непригодны.