diff --git a/README.md b/README.md index 73b57aa..dbd095c 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,14 @@ # VPNaaS-миграция: Neutron → Sprut (`ipsec_migrator`) -Инструмент для миграции VPNaaS (IPsec site-to-site) с Neutron-роутера OpenStack на «продвинутый» роутер (DC Router) во внешней SDN **Sprut** (`infra.mail.ru:9696`). +Инструмент для миграции VPNaaS (IPsec site-to-site) с Neutron-роутера OpenStack на «продвинутый» роутер (DC Router) в SDN **Sprut**. -Инструмент роутероцентричен: каждый Neutron-роутер из входного CSV сопоставлен с advanced-роутером в Sprut. Переносятся все IPsec-туннели, IKE/IPsec policy и endpoint groups, связанные с этим Neutron-роутером; объекты других роутеров того же OpenStack-проекта не затрагиваются. - -В репозитории две реализации с идентичным CLI, логикой и форматом `audit.json`: - -- **`ipsec_migrator_v2.sh`** — исходный bash-скрипт. Требует бинари `openstack`/`jq`/`curl`. -- **`ipsec_migrator/`** — Python-порт, актуальная реализация. Не использует бинарь `openstack` (ходит в Keystone/Neutron напрямую по REST) и не требует `jq`/`curl`. См. раздел [Python-порт](#python-порт-ipsec_migrator) ниже. - -## Статус проекта - -Python-порт — рекомендуемая реализация для новых миграций. История: - -1. Faithful-порт bash-скрипта на Python через subprocess-обёртку над `openstack` CLI (см. `port_summary.md`). -2. Переход с `openstack` CLI на прямые REST-вызовы к Keystone/Neutron (см. `session_2026-07-21_summary.md`). -3. Добавлено автосоздание DC Router в Sprut, когда `advanced_router_id` в CSV пуст (см. [Авто-создание целевого роутера](#авто-создание-целевого-advanced-роутера) ниже). - -Пункт 3 прошёл боевую проверку 2026-07-21: полный цикл (`--audit-only` → ревью → `--from-audit`) выполнен против настоящего OpenStack-тенанта и Sprut — DC Router + публичный интерфейс, IKE/IPsec policy, endpoint groups, VPN service и IPsec site connection созданы успешно для одного роутера. В ходе этого прогона найдены и исправлены три бага, не проявлявшиеся в офлайн-тестах (подробности и точные фиксы — в `session_2026-07-21_summary.md` и в разделе про авто-создание ниже): - -- Задвоенный `/v2.0` в пути запроса списка сетей Sprut (`.../v2.0/v2.0/networks`) — 404 при любом не-`--audit-only` прогоне. -- Публичная сеть в реальном тенанте называется `internet` (нижний регистр), а сравнение было регистрозависимым и искало `Internet`. -- Sprut отклоняет `subnet_id`/`ip_address` при подключении интерфейса к внешней сети (`400 Bad dc_interface request`) — `subnet_id` для такого интерфейса отправлять не нужно, Sprut назначает его сам. - -## Требования (bash-скрипт `ipsec_migrator_v2.sh`) - -- `bash` ≥ 4.3 (используются ассоциативные массивы и namerefs) -- `openstack` CLI, аутентифицированный в нужном проекте (переменные окружения `OS_*` / `clouds.yaml`) -- `jq` -- `curl` -- Сетевой доступ к `https://infra.mail.ru:9696` - -Скрипт проверяет версию bash и наличие всех трёх бинарей при старте и завершается с понятной ошибкой, если что-то не так. - -Требования для Python-порта — в его собственном разделе ниже; он ничего из списка выше не требует. - -## Формат входного CSV - -``` -neutron_router_id,advanced_router_id -neutron_router_id2,advanced_router_id2 -... -``` - -Пустые строки пропускаются; строки без второй колонки — с предупреждением, но не останавливая скрипт. - -Python-порт дополнительно поддерживает пустой `advanced_router_id` (авто-создание DC Router) и необязательные 3-ю/4-ю колонки — см. [Авто-создание целевого роутера](#авто-создание-целевого-advanced-роутера). +Каждый Neutron-роутер из входного CSV сопоставлен с advanced-роутером в Sprut. Переносятся все IPsec-туннели, IKE/IPsec policy и endpoint groups, связанные с этим Neutron-роутером; объекты других роутеров того же OpenStack-проекта не затрагиваются. ## Режимы запуска ### 1. Полный прогон (аудит + настройка Sprut) ```bash -./ipsec_migrator_v2.sh [output.json] [--dry-run] python3 -m ipsec_migrator [output.json] [--dry-run] ``` @@ -61,7 +17,6 @@ python3 -m ipsec_migrator [output.json] [--dry-run] ### 2. Только аудит (без обращений к Sprut на запись) ```bash -./ipsec_migrator_v2.sh --audit-only [output.json] python3 -m ipsec_migrator --audit-only [output.json] ``` @@ -70,109 +25,22 @@ python3 -m ipsec_migrator --audit-only [output.json] ### 3. Dry-run по ранее собранному аудиту ```bash -./ipsec_migrator_v2.sh --from-audit --dry-run python3 -m ipsec_migrator --from-audit --dry-run ``` - Пропускает повторный опрос OpenStack: все данные (IKE/IPsec policy, endpoint groups, VPN service, site connections) берутся из ранее сохранённого `audit.json`. Заново получает Keystone-токен и перепроверяет, что advanced-роутеры из аудита всё ещё существуют в Sprut (аудит мог быть сделан заранее). Дальше выполняются STAGE 2–4 в режиме симуляции: `GET`-запросы к Sprut выполняются по-настоящему (для сверки, что уже существует), но ни один объект не создаётся — вместо `POST` в лог пишется, что было бы отправлено (с маскированным PSK). ### 4. Реальная настройка продвинутого роутера по аудиту ```bash -./ipsec_migrator_v2.sh --from-audit python3 -m ipsec_migrator --from-audit ``` -То же самое, что режим 3, но без `--dry-run` — объекты реально создаются в Sprut (STAGE 2–4 выполняются по-боевому). - -`--audit-only` и `--from-audit` взаимоисключающие — совместное указание завершает скрипт с ошибкой. - **Рекомендуемый порядок для боевого прогона:** `--audit-only` → ревью получившегося `audit.json` → `--from-audit` (без `--dry-run`). Так реальное создание объектов в Sprut — отдельный, осознанный шаг после проверки того, что именно будет создано. -## Что делает каждый STAGE - -| Stage | Действие | -|---|---| -| **STAGE 1** (STEP 1–10) | Читает CSV, проверяет существование роутеров в OpenStack и Sprut (STEP 3; для Python-порта здесь же — авто-создание DC Router, если `advanced_router_id` пуст), собирает все IKE/IPsec policy, endpoint groups, VPN services и IPsec site connections по каждому роутеру из CSV, пишет самодостаточный JSON-аудит (атомарная запись, права `600`). | -| **STAGE 2** | Забирает текущий список объектов из Sprut API (`/vpn/ikepolicies`, `/vpn/ipsecpolicies`, `/vpn/endpoint-groups`, `/vpn/vpnservices`, `/vpn/ipsec-site-connections`) — основа для идемпотентной сверки. | -| **STAGE 3** | Сравнивает Neutron-объекты с уже существующими в Sprut (по имени / router_id / набору endpoints) и создаёт недостающие: IKE policy → IPsec policy → Endpoint Groups → VPN service. | -| **STAGE 4** | Создаёт IPsec site connections в Sprut, транслируя все связанные ID через карты соответствия из STAGE 3. Идемпотентно по полю `name`. | - -## Формат audit.json - -```json -{ - "audit_metadata": { "generated_at": "...", "input_file": "routers.csv", "stage": "STAGE1_AUDIT" }, - "subnets": { "": "" }, - "routers": [ - { - "neutron_router_id": "...", - "advanced_router_id": "...", - "pending_dc_router": { "name": "...", "description": "...", "availability_zone": "...", "flavor": "..." }, - "vpn_service": { "...": "полный объект vpnservice из Neutron API, либо null" }, - "ipsec_site_connections": [ - { - "id": "...", - "raw": { "...": "полный объект ipsec_site_connection из Neutron API" }, - "ike_policy": { "...": "полный объект ikepolicy" }, - "ipsec_policy": { "...": "полный объект ipsecpolicy" }, - "local_endpoint_group": { "id": "...", "raw": {}, "resolved_endpoints": ["10.0.0.0/24"], "already_migrated": false }, - "peer_endpoint_group": { "...": "та же структура" } - } - ] - } - ] -} -``` - -`advanced_router_id` равен `null`, а `pending_dc_router` заполнен, только когда CSV-строка не задавала advanced-роутер и он ещё не создан (Python-порт, `--audit-only`) — см. [Авто-создание целевого роутера](#авто-создание-целевого-advanced-роутера). - -Это тот же файл, что пишет режим `--audit-only` / полный прогон, и его же читает `--from-audit`. `--from-audit` проверяет `audit_metadata.stage == "STAGE1_AUDIT"` перед использованием. - -## Безопасность - -- **Файл аудита содержит PSK в открытом виде** (`ipsec_site_connections[].raw.psk` в Python-порте / `..."Pre-shared Key"` в bash-выводе). Пишется атомарно с правами `600` (только владелец). Храните и передавайте его как секрет. -- В stdout-логах PSK всегда маскируется (`redact_psk`), в т.ч. в теле запросов к Sprut и в ответах, содержащих `psk`. -- В `--dry-run` не-`GET` запросы к Sprut не отправляются — в лог пишется только предполагаемый метод/URL/тело (с маскированным PSK). `GET`-запросы (в т.ч. проверка существования advanced-роутера и поиск публичной сети для авто-создания DC Router) выполняются по-настоящему даже в `--dry-run`. -- HTTP-код ответа Sprut/Neutron всегда проверяется; не-2xx останавливает скрипт с ошибкой вместо тихого продолжения. - -## Известные ограничения - -- Токен Keystone не обновляется в течение STAGE 2–4 — для очень долгих миграций возможен `401` в середине прогона. -- Сравнение endpoint group'ов чувствительно к порядку адресов в массиве. -- Совпадающие имена IKE/IPsec policy у разных объектов в Neutron могут сломать сопоставление в Sprut. То же верно и для DC Router: авто-создание идемпотентно по имени Neutron-роутера — два разных роутера с одинаковым именем будут ошибочно сведены к одному DC Router. -- Между проверкой существования объекта (`GET`) и его созданием (`POST`) есть окно гонки — не запускайте миграцию параллельно против одного проекта. - -## Python-порт (`ipsec_migrator/`) - -Порт `ipsec_migrator_v2.sh` на Python, запуск `python3 -m ipsec_migrator `, тот же CLI и формат `audit.json`, что описаны выше. Ключевое отличие от bash-версии — способ обращения к OpenStack и Sprut: порт **не использует бинарь `openstack`** (и не требует `jq`/`curl`), а ходит в Keystone/Neutron/Sprut напрямую по REST через `requests`. - -Требования: - -- `python3`, `requests` — больше ничего не нужно. -- Аутентификация — только переменные окружения `OS_*` (password auth, - `clouds.yaml` не поддерживается): `OS_AUTH_URL`, `OS_USERNAME`, - `OS_PASSWORD`, `OS_USER_DOMAIN_NAME` (или `OS_USER_DOMAIN_ID`), - `OS_PROJECT_ID` (либо `OS_PROJECT_NAME` + `OS_PROJECT_DOMAIN_NAME`/`_ID`); - опционально `OS_REGION_NAME`, `OS_INTERFACE` (по умолчанию `public`) — - см. `test_openrc.sh` для примера. Эндпоинт Neutron резолвится из каталога - сервисов Keystone, а не хардкодится (с нормализацией хвостового `/v2.0`/`/v2` — - разные облака отдают его в каталоге по-разному). -- Списки Neutron-объектов (routers, subnets, ike/ipsec policies, endpoint groups, - vpn services, ipsec site connections) собираются с пагинацией — порт идёт по - `_links` (`rel: next`), пока не соберёт все страницы, а не только - первую. - -**Несовместимость `--from-audit`:** внутри `audit.json` вложенные объекты -(`raw`, `ike_policy`, `ipsec_policy`, endpoint group'ы) хранят имена полей -как их отдаёт Neutron API (`id`, `name`, `admin_state_up`, ...), а не -человекочитаемые заголовки колонок `openstack ... -f json` (`ID`, `Name`, -`State`, ...). Audit-файл, сгенерированный более ранней версией порта (той, -что ходила в OpenStack через `openstack` CLI), нельзя скормить в -`--from-audit` этой версии — нужно перегенерировать аудит заново. - ### Авто-создание целевого (advanced) роутера +Если `advanced_router_id` пуст, порт сам создаёт DC Router в Sprut. + В `input.csv` для порта вторая колонка (`advanced_router_id`) может быть пустой: @@ -182,50 +50,4 @@ neutron_router_id2,,MS1,standard neutron_router_id3 ``` -Если `advanced_router_id` пуст, порт сам создаёт DC Router в Sprut -(`POST /direct_connect/dc_routers`, `enable_snat: true`, `name`/`description` -берутся из самого Neutron-роутера) и подключает к нему публичный интерфейс -на внешнюю сеть Sprut (`POST /direct_connect/dc_interfaces`; сеть ищется по -имени `internet`/`Internet` без учёта регистра — реальное имя в проверенном -тенанте оказалось строчным `internet`). `subnet_id` для этого интерфейса не -указывается: Sprut отклоняет `subnet_id`/`ip_address` для интерфейса на -внешнюю сеть (`400 Bad dc_interface request: Specifying subnet_id or -ip_address for external network is restricted`) и назначает адрес сам. -Обе операции (DC Router, DC Interface) идемпотентны — по имени и по паре -`(dc_router_id, network_id)` соответственно — повторный прогон не создаёт -дублей. Точные пути и схемы запросов сверены с `neutron-sprut-api.json` -в корне репозитория. -- `availability_zone` и `flavor` (обязательные поля Sprut, `flavor` — одно из - `basic`/`standard`/`advanced`) берутся из 3-й/4-й колонки CSV; если их там - нет — порт спросит интерактивно (`input()`). Для неинтерактивных запусков - (cron/CI) указывайте их в CSV заранее — иначе скрипт завершится ошибкой - вместо зависания на `stdin`. -- В режиме `--audit-only` DC Router **не создаётся** (режим по-прежнему - ничего не пишет в Sprut) — вместо этого в `audit.json` записывается - `"advanced_router_id": null` и блок `"pending_dc_router"` с именем, - описанием, AZ и flavor. Реальное создание происходит при последующем - `--from-audit` (без `--dry-run`) или в полном прогоне. -- Если в Sprut найдено несколько сетей с именем `internet`/`Internet` без - учёта регистра, используется первая с предупреждением в stderr — сознательный - упрощённый выбор (без дополнительной проверки `router:external`), см. - «Известные ограничения» выше про аналогичный риск коллизии имён. -- Реальное (не `--dry-run`) создание роутера и интерфейса — необратимое - изменение инфраструктуры Sprut, выполняется как часть обычного потока - STEP 3 (внутри STAGE 1). - -Статус: боевой цикл (`--audit-only` → `--from-audit`) успешно прогнан против -реального Sprut 2026-07-21 — детали в `session_2026-07-21_summary.md`. - -## Файлы в репозитории - -| Файл/директория | Назначение | -|---|---| -| `ipsec_migrator_v2.sh` | Исходный bash-скрипт миграции | -| `ipsec_migrator/` | Python-порт (пакет), запуск `python3 -m ipsec_migrator` | -| `input.csv` | Пример/рабочий входной CSV для текущей миграции | -| `test_openrc.sh` | Пример переменных окружения `OS_*` для аутентификации в OpenStack | -| `neutron-sprut-api.json` | OpenAPI-спека Sprut API — источник истины для путей/схем запросов (онлайн-документация `cloud.vk.com` недоступна из окружения разработки) | -| `port_summary.md` | Как и с какими оговорками бэш-скрипт был портирован на Python (2026-07-20) | -| `session_2026-07-21_summary.md` | Переход Python-порта на прямые Keystone/Neutron REST-вызовы и добавление авто-создания DC Router, включая боевую проверку и найденные баги (2026-07-21) | -| `vpnaas_audit_*.json` | Результаты прогонов (`--audit-only`/полный прогон) — содержат PSK, права `600` |