20 KiB
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-порт — рекомендуемая реализация для новых миграций. История:
- Faithful-порт bash-скрипта на Python через subprocess-обёртку над
openstackCLI (см.port_summary.md). - Переход с
openstackCLI на прямые REST-вызовы к Keystone/Neutron (см.session_2026-07-21_summary.md). - Добавлено автосоздание DC Router в Sprut, когда
advanced_router_idв CSV пуст (см. Авто-создание целевого роутера ниже).
Пункт 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)openstackCLI, аутентифицированный в нужном проекте (переменные окруженияOS_*/clouds.yaml)jqcurl- Сетевой доступ к
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-ю колонки — см. Авто-создание целевого роутера.
Режимы запуска
1. Полный прогон (аудит + настройка Sprut)
./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 3–4). output.json необязателен, по умолчанию — vpnaas_audit_<YYYYmmdd_HHMMSS>.json в текущей директории.
2. Только аудит (без обращений к Sprut на запись)
./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 по ранее собранному аудиту
./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. Реальная настройка продвинутого роутера по аудиту
./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 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
{
"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) — см. Авто-создание целевого роутера.
Это тот же файл, что пишет режим --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-onlyDC 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 |