Files
VPNaaS-Migrator/README.md
T
2026-07-21 15:05:52 +03:00

232 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VPNaaS-миграция: Neutron → Sprut (`ipsec_migrator`)
Инструмент для миграции VPNaaS (IPsec site-to-site) с Neutron-роутера OpenStack на «продвинутый» роутер (DC Router) во внешней SDN **Sprut** (`infra.mail.ru:9696`).
Инструмент роутероцентричен: каждый 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)
```bash
./ipsec_migrator_v2.sh <input.csv> [output.json] [--dry-run]
python3 -m ipsec_migrator <input.csv> [output.json] [--dry-run]
```
Выполняет всё последовательно: аудит конфигурации Neutron (STAGE 1) → сбор существующих объектов в Sprut (STAGE 2) → создание недостающих объектов и site connection'ов (STAGE 34). `output.json` необязателен, по умолчанию — `vpnaas_audit_<YYYYmmdd_HHMMSS>.json` в текущей директории.
### 2. Только аудит (без обращений к Sprut на запись)
```bash
./ipsec_migrator_v2.sh --audit-only <input.csv> [output.json]
python3 -m ipsec_migrator --audit-only <input.csv> [output.json]
```
Выполняет только STAGE 1: проверяет, что Neutron- и advanced-роутеры существуют, собирает полную конфигурацию VPNaaS из Neutron и пишет её в JSON-файл. STAGE 2–4 (создание объектов в Sprut) не запускаются — скрипт завершается сразу после записи аудита.
### 3. Dry-run по ранее собранному аудиту
```bash
./ipsec_migrator_v2.sh --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).
### 4. Реальная настройка продвинутого роутера по аудиту
```bash
./ipsec_migrator_v2.sh --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 — отдельный, осознанный шаг после проверки того, что именно будет создано.
## Что делает каждый 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) роутера
В `input.csv` для порта вторая колонка (`advanced_router_id`) может быть
пустой:
```
neutron_router_id1,advanced_router_id1
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` |