Обновить README.md
This commit is contained in:
@@ -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 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": { "<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` |
|
|
||||||
|
|||||||
Reference in New Issue
Block a user