Обновить README.md

This commit is contained in:
2026-07-21 15:15:52 +03:00
parent 6563d755d8
commit cd84eb581a
+4 -182
View File
@@ -1,58 +1,14 @@
# VPNaaS-миграция: Neutron → Sprut (`ipsec_migrator`) # 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-проекта не затрагиваются. Каждый 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-роутера).
## Режимы запуска ## Режимы запуска
### 1. Полный прогон (аудит + настройка Sprut) ### 1. Полный прогон (аудит + настройка Sprut)
```bash ```bash
./ipsec_migrator_v2.sh <input.csv> [output.json] [--dry-run]
python3 -m ipsec_migrator <input.csv> [output.json] [--dry-run] python3 -m ipsec_migrator <input.csv> [output.json] [--dry-run]
``` ```
@@ -61,7 +17,6 @@ python3 -m ipsec_migrator <input.csv> [output.json] [--dry-run]
### 2. Только аудит (без обращений к Sprut на запись) ### 2. Только аудит (без обращений к Sprut на запись)
```bash ```bash
./ipsec_migrator_v2.sh --audit-only <input.csv> [output.json]
python3 -m ipsec_migrator --audit-only <input.csv> [output.json] python3 -m ipsec_migrator --audit-only <input.csv> [output.json]
``` ```
@@ -70,109 +25,22 @@ python3 -m ipsec_migrator --audit-only <input.csv> [output.json]
### 3. Dry-run по ранее собранному аудиту ### 3. Dry-run по ранее собранному аудиту
```bash ```bash
./ipsec_migrator_v2.sh --from-audit <audit.json> --dry-run
python3 -m ipsec_migrator --from-audit <audit.json> --dry-run python3 -m ipsec_migrator --from-audit <audit.json> --dry-run
``` ```
Пропускает повторный опрос OpenStack: все данные (IKE/IPsec policy, endpoint groups, VPN service, site connections) берутся из ранее сохранённого `audit.json`. Заново получает Keystone-токен и перепроверяет, что advanced-роутеры из аудита всё ещё существуют в Sprut (аудит мог быть сделан заранее). Дальше выполняются STAGE 2–4 в режиме симуляции: `GET`-запросы к Sprut выполняются по-настоящему (для сверки, что уже существует), но ни один объект не создаётся — вместо `POST` в лог пишется, что было бы отправлено (с маскированным PSK). Пропускает повторный опрос OpenStack: все данные (IKE/IPsec policy, endpoint groups, VPN service, site connections) берутся из ранее сохранённого `audit.json`. Заново получает Keystone-токен и перепроверяет, что advanced-роутеры из аудита всё ещё существуют в Sprut (аудит мог быть сделан заранее). Дальше выполняются STAGE 2–4 в режиме симуляции: `GET`-запросы к Sprut выполняются по-настоящему (для сверки, что уже существует), но ни один объект не создаётся — вместо `POST` в лог пишется, что было бы отправлено (с маскированным PSK).
### 4. Реальная настройка продвинутого роутера по аудиту ### 4. Реальная настройка продвинутого роутера по аудиту
```bash ```bash
./ipsec_migrator_v2.sh --from-audit <audit.json>
python3 -m ipsec_migrator --from-audit <audit.json> python3 -m ipsec_migrator --from-audit <audit.json>
``` ```
То же самое, что режим 3, но без `--dry-run` — объекты реально создаются в Sprut (STAGE 2–4 выполняются по-боевому).
`--audit-only` и `--from-audit` взаимоисключающие — совместное указание завершает скрипт с ошибкой.
**Рекомендуемый порядок для боевого прогона:** `--audit-only` → ревью получившегося `audit.json``--from-audit` (без `--dry-run`). Так реальное создание объектов в Sprut — отдельный, осознанный шаг после проверки того, что именно будет создано. **Рекомендуемый порядок для боевого прогона:** `--audit-only` → ревью получившегося `audit.json``--from-audit` (без `--dry-run`). Так реальное создание объектов в Sprut — отдельный, осознанный шаг после проверки того, что именно будет создано.
## Что делает каждый STAGE
| Stage | Действие |
|---|---|
| **STAGE 1** (STEP 110) | Читает 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": { "<subnet_id>": "<cidr>" },
"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 <args>`, тот же 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) собираются с пагинацией — порт идёт по
`<collection>_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) роутера
Если `advanced_router_id` пуст, порт сам создаёт DC Router в Sprut.
В `input.csv` для порта вторая колонка (`advanced_router_id`) может быть В `input.csv` для порта вторая колонка (`advanced_router_id`) может быть
пустой: пустой:
@@ -182,50 +50,4 @@ neutron_router_id2,,MS1,standard
neutron_router_id3 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` |