2026-07-21 15:05:52 +03:00
2026-07-21 15:05:52 +03:00
2026-07-21 15:05:52 +03:00
2026-07-21 15:05:52 +03:00
2026-07-21 15:05:52 +03:00

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-порт — рекомендуемая реализация для новых миграций. История:

  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 пуст (см. Авто-создание целевого роутера ниже).

Пункт 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-ю колонки — см. Авто-создание целевого роутера.

Режимы запуска

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 34). 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 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

{
  "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-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
S
Description
Сценарий по миграции VPNaaS сервиса с Neutron на Sprut с полным сохранением всех настроек IPsec туннеля.
Readme
103 KiB
Languages
Python 100%