diff --git a/README.md b/README.md index ff966b6..fde3c6e 100644 --- a/README.md +++ b/README.md @@ -89,32 +89,38 @@ docker compose exec -T c1 curl -s http://10.200.200.1/ Все специфичные для площадки A значения (ASN, anycast-префикс, router-id и BGP-соседи каждой ноды, имена интерфейсов, SSH-доступ) вынесены в единый файл переменных окружения `ansible/site-a.env` — сама роль `frr_anycast` полностью параметризована и не содержит захардкоженных адресов/ASN. `frr.conf.j2` при рендере воспроизводит уже проверенную в Docker-лабе BGP-конфигурацию (`frr/a{1,2,3}/frr.conf`), за вычетом блока `interface dummy0` — в VM-варианте этот адрес назначается через netplan (`dummy0.netplan.yml.j2`), а не через frr.conf. -Детали: [docs/2026-09-03-ansible-site-a-plan.md](docs/2026-09-03-ansible-site-a-plan.md) / [docs/2026-09-03-ansible-site-a-summary.md](docs/2026-09-03-ansible-site-a-summary.md). +Роль `frr_anycast` обобщена: список BGP-соседей узла — не 2 жёстко именованных (`neighbor_b1`/`neighbor_b2`), а `bgp_neighbors` произвольной длины, каждый с флагом `prepend` (as-path prepend на исходящих анонсах). Опционально (`enable_snat: true`) роль также поднимает sNAT через `nftables` (masquerade на внешнем интерфейсе) — оба узла ниже используют эту одну и ту же роль с разными `group_vars`. + +Детали: [docs/2026-09-03-ansible-site-a-plan.md](docs/2026-09-03-ansible-site-a-plan.md) / [docs/2026-09-03-ansible-site-a-summary.md](docs/2026-09-03-ansible-site-a-summary.md) — исходная реализация; [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md) — доработка под второе окружение (см. ниже). ### Структура ``` ansible/ -├── site-a.env источник всех site-A-специфичных значений -├── run.sh source site-a.env + ansible-playbook -├── inventory/site-a.yml a1/a2/a3, ansible_host из lookup('env', 'A{N}_MGMT_IP') -├── group_vars/site_a.yml site-wide переменные (ASN, anycast-префикс, интерфейсы) -├── host_vars/{a1,a2,a3}.yml per-node переменные (router-id, BGP-соседи) -├── site-a.yml playbook (role: frr_anycast) -├── roles/frr_anycast/ задачи, handlers, шаблоны (daemons.j2, frr.conf.j2, dummy0.netplan.yml.j2) -└── tests/render-check.sh офлайн-проверка шаблонов без реальных ВМ (см. ниже) +├── site-a.env / site-cloud.env источники site-специфичных значений (2 окружения) +├── run.sh общий для всех окружений (SITE_ENV_FILE + имя playbook'а) +├── inventory/{site-a,site-cloud}.yml ansible_host из lookup('env', ...) +├── group_vars/{site_a,site_cloud}.yml site-wide переменные (ASN, anycast, интерфейсы, bgp_neighbors) +├── host_vars/{a1,a2,a3,vr01..vr04}.yml per-node переменные (router-id, [bgp_neighbors]) +├── site-a.yml / site-cloud.yml playbook'и (role: frr_anycast — общая для обоих) +├── roles/frr_anycast/ задачи, handlers, шаблоны (daemons.j2, frr.conf.j2, +│ dummy0.netplan.yml.j2, nftables.conf.j2) +└── tests/ render-check.sh (площадка A, golden-diff), + render-check-cloud.sh (cloud, grep-инварианты) ``` ### Запуск ```bash cd ansible -./run.sh site-a.yml # применить конфигурацию к a1/a2/a3 +./run.sh site-a.yml # применить конфигурацию к a1/a2/a3 (env по умолчанию: site-a.env) ./run.sh site-a.yml --check --diff # сухой прогон ``` Если `ansible-playbook` не установлен в системе — поставить локально в venv, не трогая системный Python: `python3 -m venv venv && venv/bin/pip install ansible` (каталог `venv/` в `.gitignore`, не коммитится); `run.sh` сам подхватит `venv/bin/ansible-playbook`, если он есть. +`run.sh` универсален для всех окружений в этом репозитории: инвентарь берётся по имени playbook'а (`X.yml` → `inventory/X.yml`), env-файл — из `SITE_ENV_FILE` (по умолчанию `site-a.env`). + ### Проверка без живых ВМ На момент подготовки playbook ВМ площадки A ещё не подняты (Terraform-провижининг — отдельный шаг), поэтому функциональная проверка (`vtysh`, реальный apply) не выполнялась и не входит в текущую поставку. Доступна и выполнена офлайн-проверка: @@ -126,3 +132,33 @@ ansible/tests/render-check.sh # golden-diff: рендер frr.conf ``` После того как Terraform поднимет ВМ — реальный прогон `./run.sh site-a.yml`, затем те же `vtysh`-команды, что и в разделе «[Проверка окружения](#проверка-окружения)» выше (заменить `docker compose exec -T vtysh ...` на `ssh vtysh ...` или `ansible site_a -m command -a "vtysh -c 'show bgp summary'"`). + +## Второе окружение: «Облачная площадка» (anycast + sNAT, 2 канала) + +Второе, независимое от площадки A окружение (та же роль `frr_anycast`, другие `group_vars`/`host_vars`): 4 узла `vr01`…`vr04`, один AS `65200`, общий anycast-адрес `172.31.255.10/32`. У каждого узла — 2 приватных интерфейса-сегмента (не point-to-point, а общий L2 с обоими пирами стороны): + +- **Основной канал** — сегмент к «Основная площадка» (внешняя сторона, ASN `65104`, пиры M1/M2) — анонсы **без** as-path prepend. +- **Резервный канал** — сегмент к «Резервная площадка» (внешняя сторона, ASN `65103`, пиры B1/B2) — анонсы **с** as-path prepend (на всех 4 узлах, `AS_PATH_PREPEND_COUNT` из env, по умолчанию 3), чтобы при исправном основном канале трафик шёл через него. +- **sNAT** (nftables, masquerade) на внешнем интерфейсе — на всех 4 узлах: трафик на anycast-адрес транслируется для выхода в интернет. + +Все значения — в `ansible/site-cloud.env` (ASN, anycast-префикс, IP всех 4 пиров, имена интерфейсов, per-node router-id). Т.к. пиры общие для всех 4 узлов, список `bgp_neighbors` задан один раз в `group_vars/site_cloud.yml`, а не per-node. + +```bash +cd ansible +SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml # применить +SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml --check --diff # сухой прогон +``` + +### Проверка без живых ВМ + +ВМ этого окружения тоже не подняты — golden-файла для сравнения тоже нет (окружение новое), поэтому проверка через grep-инварианты вместо byte-diff: + +```bash +SITE_ENV_FILE=site-cloud.env ansible/run.sh site-cloud.yml --syntax-check +ansible/tests/render-check.sh # регресс: площадка A рендерится как прежде +ansible/tests/render-check-cloud.sh # 4 соседа, prepend только на резервном канале, sNAT +``` + +`render-check-cloud.sh` проверяет: у каждого из 4 узлов ровно 4 `neighbor ... remote-as` (2×65104 без route-map, 2×65103 с `route-map RM-PREPEND-OUT out`), `route-map` содержит `set as-path prepend 65200 65200 65200`, `nftables.conf` содержит `masquerade` на заданном внешнем интерфейсе. + +Детали и разбор диаграммы: [docs/2026-09-09-ansible-site-cloud-plan.md](docs/2026-09-09-ansible-site-cloud-plan.md) / [docs/2026-09-09-ansible-site-cloud-summary.md](docs/2026-09-09-ansible-site-cloud-summary.md). diff --git a/ansible/group_vars/site_a.yml b/ansible/group_vars/site_a.yml index bb0dcc2..2a7a144 100644 --- a/ansible/group_vars/site_a.yml +++ b/ansible/group_vars/site_a.yml @@ -1,6 +1,6 @@ # Site-wide переменные площадки A — тонкий адаптер над ansible/site-a.env. # Ничего site-специфичного не хардкодится: только имена ключей env-файла. -site_a_asn: "{{ lookup('env', 'SITE_A_ASN') }}" +local_asn: "{{ lookup('env', 'SITE_A_ASN') }}" anycast_prefix: "{{ lookup('env', 'ANYCAST_PREFIX') }}" dummy_iface: "{{ lookup('env', 'DUMMY_IFACE') | default('dummy0', true) }}" priv1_iface: "{{ lookup('env', 'PRIV1_IFACE') }}" diff --git a/ansible/group_vars/site_cloud.yml b/ansible/group_vars/site_cloud.yml new file mode 100644 index 0000000..d9dc7de --- /dev/null +++ b/ansible/group_vars/site_cloud.yml @@ -0,0 +1,29 @@ +# Site-wide переменные окружения "Облачная площадка" — тонкий адаптер над +# ansible/site-cloud.env. Ничего site-специфичного не хардкодится: только +# имена ключей env-файла. +local_asn: "{{ lookup('env', 'LOCAL_ASN') }}" +anycast_prefix: "{{ lookup('env', 'ANYCAST_PREFIX') }}" +dummy_iface: "{{ lookup('env', 'DUMMY_IFACE') | default('dummy0', true) }}" +priv1_iface: "{{ lookup('env', 'PRIV_MAIN_IFACE') }}" +priv2_iface: "{{ lookup('env', 'PRIV_BACKUP_IFACE') }}" +ext_iface: "{{ lookup('env', 'EXT_IFACE') }}" +frr_apt_series: "{{ lookup('env', 'FRR_APT_SERIES') | default('frr-stable', true) }}" + +enable_snat: true +as_path_prepend_count: "{{ lookup('env', 'AS_PATH_PREPEND_COUNT') }}" + +# M1/M2/B1/B2 сидят на общих для всех 4 узлов L2-сегментах, поэтому список +# соседей — один на всё окружение, а не per-node (см. host_vars/vr0N.yml). +bgp_neighbors: + - ip: "{{ lookup('env', 'MAIN1_NEIGHBOR_IP') }}" + asn: "{{ lookup('env', 'MAIN_ASN') }}" + prepend: false + - ip: "{{ lookup('env', 'MAIN2_NEIGHBOR_IP') }}" + asn: "{{ lookup('env', 'MAIN_ASN') }}" + prepend: false + - ip: "{{ lookup('env', 'BACKUP1_NEIGHBOR_IP') }}" + asn: "{{ lookup('env', 'BACKUP_ASN') }}" + prepend: true + - ip: "{{ lookup('env', 'BACKUP2_NEIGHBOR_IP') }}" + asn: "{{ lookup('env', 'BACKUP_ASN') }}" + prepend: true diff --git a/ansible/host_vars/a1.yml b/ansible/host_vars/a1.yml index bdfbd70..ea1720f 100644 --- a/ansible/host_vars/a1.yml +++ b/ansible/host_vars/a1.yml @@ -1,6 +1,9 @@ # Per-node адаптер над ansible/site-a.env (префикс A1_*). router_id: "{{ lookup('env', 'A1_ROUTER_ID') }}" -neighbor_b1_ip: "{{ lookup('env', 'A1_NEIGHBOR_B1_IP') }}" -neighbor_b1_asn: "{{ lookup('env', 'A1_NEIGHBOR_B1_ASN') }}" -neighbor_b2_ip: "{{ lookup('env', 'A1_NEIGHBOR_B2_IP') }}" -neighbor_b2_asn: "{{ lookup('env', 'A1_NEIGHBOR_B2_ASN') }}" +bgp_neighbors: + - ip: "{{ lookup('env', 'A1_NEIGHBOR_B1_IP') }}" + asn: "{{ lookup('env', 'A1_NEIGHBOR_B1_ASN') }}" + prepend: false + - ip: "{{ lookup('env', 'A1_NEIGHBOR_B2_IP') }}" + asn: "{{ lookup('env', 'A1_NEIGHBOR_B2_ASN') }}" + prepend: false diff --git a/ansible/host_vars/a2.yml b/ansible/host_vars/a2.yml index 465c445..65ab37a 100644 --- a/ansible/host_vars/a2.yml +++ b/ansible/host_vars/a2.yml @@ -1,6 +1,9 @@ # Per-node адаптер над ansible/site-a.env (префикс A2_*). router_id: "{{ lookup('env', 'A2_ROUTER_ID') }}" -neighbor_b1_ip: "{{ lookup('env', 'A2_NEIGHBOR_B1_IP') }}" -neighbor_b1_asn: "{{ lookup('env', 'A2_NEIGHBOR_B1_ASN') }}" -neighbor_b2_ip: "{{ lookup('env', 'A2_NEIGHBOR_B2_IP') }}" -neighbor_b2_asn: "{{ lookup('env', 'A2_NEIGHBOR_B2_ASN') }}" +bgp_neighbors: + - ip: "{{ lookup('env', 'A2_NEIGHBOR_B1_IP') }}" + asn: "{{ lookup('env', 'A2_NEIGHBOR_B1_ASN') }}" + prepend: false + - ip: "{{ lookup('env', 'A2_NEIGHBOR_B2_IP') }}" + asn: "{{ lookup('env', 'A2_NEIGHBOR_B2_ASN') }}" + prepend: false diff --git a/ansible/host_vars/a3.yml b/ansible/host_vars/a3.yml index 618332b..8a0082d 100644 --- a/ansible/host_vars/a3.yml +++ b/ansible/host_vars/a3.yml @@ -1,6 +1,9 @@ # Per-node адаптер над ansible/site-a.env (префикс A3_*). router_id: "{{ lookup('env', 'A3_ROUTER_ID') }}" -neighbor_b1_ip: "{{ lookup('env', 'A3_NEIGHBOR_B1_IP') }}" -neighbor_b1_asn: "{{ lookup('env', 'A3_NEIGHBOR_B1_ASN') }}" -neighbor_b2_ip: "{{ lookup('env', 'A3_NEIGHBOR_B2_IP') }}" -neighbor_b2_asn: "{{ lookup('env', 'A3_NEIGHBOR_B2_ASN') }}" +bgp_neighbors: + - ip: "{{ lookup('env', 'A3_NEIGHBOR_B1_IP') }}" + asn: "{{ lookup('env', 'A3_NEIGHBOR_B1_ASN') }}" + prepend: false + - ip: "{{ lookup('env', 'A3_NEIGHBOR_B2_IP') }}" + asn: "{{ lookup('env', 'A3_NEIGHBOR_B2_ASN') }}" + prepend: false diff --git a/ansible/host_vars/vr01.yml b/ansible/host_vars/vr01.yml new file mode 100644 index 0000000..ae0938b --- /dev/null +++ b/ansible/host_vars/vr01.yml @@ -0,0 +1,2 @@ +# Per-node адаптер над ansible/site-cloud.env (префикс VR01_*). +router_id: "{{ lookup('env', 'VR01_ROUTER_ID') }}" diff --git a/ansible/host_vars/vr02.yml b/ansible/host_vars/vr02.yml new file mode 100644 index 0000000..0c1e910 --- /dev/null +++ b/ansible/host_vars/vr02.yml @@ -0,0 +1,2 @@ +# Per-node адаптер над ansible/site-cloud.env (префикс VR02_*). +router_id: "{{ lookup('env', 'VR02_ROUTER_ID') }}" diff --git a/ansible/host_vars/vr03.yml b/ansible/host_vars/vr03.yml new file mode 100644 index 0000000..ba1be39 --- /dev/null +++ b/ansible/host_vars/vr03.yml @@ -0,0 +1,2 @@ +# Per-node адаптер над ansible/site-cloud.env (префикс VR03_*). +router_id: "{{ lookup('env', 'VR03_ROUTER_ID') }}" diff --git a/ansible/host_vars/vr04.yml b/ansible/host_vars/vr04.yml new file mode 100644 index 0000000..604dcc2 --- /dev/null +++ b/ansible/host_vars/vr04.yml @@ -0,0 +1,2 @@ +# Per-node адаптер над ansible/site-cloud.env (префикс VR04_*). +router_id: "{{ lookup('env', 'VR04_ROUTER_ID') }}" diff --git a/ansible/inventory/site-cloud.yml b/ansible/inventory/site-cloud.yml new file mode 100644 index 0000000..ad0654a --- /dev/null +++ b/ansible/inventory/site-cloud.yml @@ -0,0 +1,15 @@ +all: + children: + site_cloud: + hosts: + vr01: + ansible_host: "{{ lookup('env', 'VR01_MGMT_IP') }}" + vr02: + ansible_host: "{{ lookup('env', 'VR02_MGMT_IP') }}" + vr03: + ansible_host: "{{ lookup('env', 'VR03_MGMT_IP') }}" + vr04: + ansible_host: "{{ lookup('env', 'VR04_MGMT_IP') }}" + vars: + ansible_user: "{{ lookup('env', 'SSH_USER') }}" + ansible_ssh_private_key_file: "{{ lookup('env', 'SSH_PRIVATE_KEY_FILE') }}" diff --git a/ansible/roles/frr_anycast/defaults/main.yml b/ansible/roles/frr_anycast/defaults/main.yml index c2d9c1b..71fc4fa 100644 --- a/ansible/roles/frr_anycast/defaults/main.yml +++ b/ansible/roles/frr_anycast/defaults/main.yml @@ -13,3 +13,9 @@ frr_daemons_enabled: frr_conf_path: /etc/frr/frr.conf frr_daemons_path: /etc/frr/daemons netplan_dummy_config_path: /etc/netplan/90-anycast-dummy0.yaml + +# Опциональные возможности роли — выключены по умолчанию, чтобы окружения, +# которые их не используют (площадка A), рендерились без изменений. +as_path_prepend_count: 0 # >0 включает route-map с этим числом prepend'ов +enable_snat: false # true включает nftables-задачи и sysctl ip_forward +nftables_conf_path: /etc/nftables.conf diff --git a/ansible/roles/frr_anycast/handlers/main.yml b/ansible/roles/frr_anycast/handlers/main.yml index 491c2bf..6e802d5 100644 --- a/ansible/roles/frr_anycast/handlers/main.yml +++ b/ansible/roles/frr_anycast/handlers/main.yml @@ -11,3 +11,12 @@ ansible.builtin.systemd: name: frr state: restarted + +- name: Reload sysctl + ansible.builtin.command: sysctl --system + changed_when: true + +- name: Reload nftables + ansible.builtin.systemd: + name: nftables + state: reloaded diff --git a/ansible/roles/frr_anycast/tasks/main.yml b/ansible/roles/frr_anycast/tasks/main.yml index c0fc460..1809776 100644 --- a/ansible/roles/frr_anycast/tasks/main.yml +++ b/ansible/roles/frr_anycast/tasks/main.yml @@ -12,6 +12,17 @@ этого playbook. success_msg: "Приватные интерфейсы {{ priv1_iface }} и {{ priv2_iface }} на месте." +- name: Pre-flight | внешний интерфейс должен быть на месте (нужен для sNAT egress) + ansible.builtin.assert: + that: + - ext_iface in ansible_facts.interfaces + - ansible_facts[ext_iface]['active'] | default(false) + fail_msg: >- + Внешний интерфейс {{ ext_iface }} не найден или не активен — он нужен + как egress для sNAT (enable_snat: true). + success_msg: "Внешний интерфейс {{ ext_iface }} на месте." + when: enable_snat | bool + - name: FRR APT-репозиторий | добавить GPG-ключ ansible.builtin.get_url: url: https://deb.frrouting.org/frr/keys.gpg @@ -66,3 +77,35 @@ name: frr enabled: true state: started + +- name: sNAT | включить net.ipv4.ip_forward персистентно + ansible.builtin.lineinfile: + path: /etc/sysctl.d/99-frr-anycast-forward.conf + line: net.ipv4.ip_forward=1 + create: true + mode: "0644" + notify: Reload sysctl + when: enable_snat | bool + +- name: sNAT | установить nftables + ansible.builtin.apt: + name: nftables + state: present + when: enable_snat | bool + +- name: sNAT | конфиг /etc/nftables.conf (masquerade на внешнем интерфейсе) + ansible.builtin.template: + src: nftables.conf.j2 + dest: "{{ nftables_conf_path }}" + owner: root + group: root + mode: "0640" + notify: Reload nftables + when: enable_snat | bool + +- name: sNAT | сервис nftables включён и запущен + ansible.builtin.systemd: + name: nftables + enabled: true + state: started + when: enable_snat | bool diff --git a/ansible/roles/frr_anycast/templates/frr.conf.j2 b/ansible/roles/frr_anycast/templates/frr.conf.j2 index e1eff0e..8b20e5f 100644 --- a/ansible/roles/frr_anycast/templates/frr.conf.j2 +++ b/ansible/roles/frr_anycast/templates/frr.conf.j2 @@ -4,16 +4,26 @@ hostname {{ inventory_hostname }} log stdout informational no ipv6 forwarding ! -router bgp {{ site_a_asn }} +{% if bgp_neighbors | selectattr('prepend') | list | length > 0 %} +route-map RM-PREPEND-OUT permit 10 + set as-path prepend {{ ((local_asn ~ ' ') * (as_path_prepend_count | default(0) | int)) | trim }} +! +{% endif %} +router bgp {{ local_asn }} bgp router-id {{ router_id }} no bgp ebgp-requires-policy - neighbor {{ neighbor_b1_ip }} remote-as {{ neighbor_b1_asn }} - neighbor {{ neighbor_b2_ip }} remote-as {{ neighbor_b2_asn }} +{% for n in bgp_neighbors %} + neighbor {{ n.ip }} remote-as {{ n.asn }} +{% endfor %} ! address-family ipv4 unicast network {{ anycast_prefix }} - neighbor {{ neighbor_b1_ip }} activate - neighbor {{ neighbor_b2_ip }} activate +{% for n in bgp_neighbors %} + neighbor {{ n.ip }} activate +{% endfor %} +{% for n in bgp_neighbors if n.prepend %} + neighbor {{ n.ip }} route-map RM-PREPEND-OUT out +{% endfor %} exit-address-family ! line vty diff --git a/ansible/roles/frr_anycast/templates/nftables.conf.j2 b/ansible/roles/frr_anycast/templates/nftables.conf.j2 new file mode 100644 index 0000000..7292379 --- /dev/null +++ b/ansible/roles/frr_anycast/templates/nftables.conf.j2 @@ -0,0 +1,9 @@ +#!/usr/sbin/nft -f +flush ruleset + +table inet nat { + chain postrouting { + type nat hook postrouting priority srcnat; policy accept; + oifname "{{ ext_iface }}" masquerade + } +} diff --git a/ansible/run.sh b/ansible/run.sh index a9dd326..dfb5eba 100755 --- a/ansible/run.sh +++ b/ansible/run.sh @@ -1,11 +1,16 @@ #!/bin/sh -# Загружает site-a.env в environment и вызывает ansible-playbook. -# Использование: ./run.sh site-a.yml [доп. аргументы ansible-playbook] +# Загружает env-файл окружения в environment и вызывает ansible-playbook. +# Использование: ./run.sh [доп. аргументы ansible-playbook] +# SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml +# ./run.sh site-a.yml # SITE_ENV_FILE по умолчанию site-a.env +# Инвентарь берётся по имени плейбука: playbook X.yml -> inventory/X.yml +# (соглашение имён между *.yml playbook'ами и inventory/*.yml в этом репозитории). set -e cd "$(dirname "$0")" -ENV_FILE="${SITE_A_ENV_FILE:-site-a.env}" +# SITE_A_ENV_FILE — устаревший алиас, сохранён для обратной совместимости. +ENV_FILE="${SITE_ENV_FILE:-${SITE_A_ENV_FILE:-site-a.env}}" if [ ! -f "$ENV_FILE" ]; then echo "Не найден файл переменных окружения: $ENV_FILE" >&2 exit 1 @@ -19,9 +24,15 @@ set +a PLAYBOOK="${1:-site-a.yml}" shift 2>/dev/null || true +INVENTORY="inventory/$PLAYBOOK" +if [ ! -f "$INVENTORY" ]; then + echo "Не найден инвентарь: $INVENTORY (ожидается рядом с playbook'ом, то же имя)" >&2 + exit 1 +fi + ANSIBLE_PLAYBOOK_BIN="ansible-playbook" if [ -x "../venv/bin/ansible-playbook" ]; then ANSIBLE_PLAYBOOK_BIN="../venv/bin/ansible-playbook" fi -exec "$ANSIBLE_PLAYBOOK_BIN" -i inventory/site-a.yml "$PLAYBOOK" "$@" +exec "$ANSIBLE_PLAYBOOK_BIN" -i "$INVENTORY" "$PLAYBOOK" "$@" diff --git a/ansible/site-cloud.env b/ansible/site-cloud.env new file mode 100644 index 0000000..43cda04 --- /dev/null +++ b/ansible/site-cloud.env @@ -0,0 +1,56 @@ +# Все специфичные для окружения "Облачная площадка" параметры разворачивания +# (updated_dia.png). Источник истины: значения из этого файла и только из +# него подставляются в шаблоны роли frr_anycast — сама роль site-специфичных +# значений не содержит. +# +# Топология: 4 узла (vR01..vR04), один AS 65200, общий anycast-адрес на всех. +# У каждого узла 2 приватных интерфейса-сегмента: +# - "основной канал" (синий) — общий L2-сегмент с Client Edge Router-M1/M2 +# (площадка "Основная", ASN 65104) — анонсы БЕЗ as-path prepend; +# - "резервный канал" (розовый) — общий L2-сегмент с Client Edge Router-B1/B2 +# (площадка "Резервная", ASN 65103) — анонсы С as-path prepend. +# Т.к. M1/M2/B1/B2 сидят на общих для всех vR сегментах, bgp_neighbors — +# ОДИН список на всё окружение (см. group_vars/site_cloud.yml), а не per-node. +# +# Использование: SITE_ENV_FILE=site-cloud.env ./run.sh site-cloud.yml [...] + +# --- site-wide --- +LOCAL_ASN=65200 +ANYCAST_PREFIX=172.31.255.10/32 +DUMMY_IFACE=dummy0 +PRIV_MAIN_IFACE=ens7 # сегмент к "Основная площадка" (M1/M2), синий/основной канал +PRIV_BACKUP_IFACE=ens8 # сегмент к "Резервная площадка" (B1/B2), розовый/резервный канал +EXT_IFACE=ens9 # внешний интерфейс — интернет-egress для sNAT +FRR_APT_SERIES=frr-8 # ветка репозитория deb.frrouting.org + +# SSH/подключение (пользователь и ключ, которые Terraform выдал ВМ при создании) +SSH_USER=ansible +SSH_PRIVATE_KEY_FILE=~/.ssh/site-cloud-deploy + +# --- Основная площадка (синий/основной канал, БЕЗ as-path prepend) --- +MAIN_ASN=65104 +MAIN1_NEIGHBOR_IP=192.168.200.10 # Client Edge Router-M1 (main) +MAIN2_NEIGHBOR_IP=192.168.200.11 # Client Edge Router-M2 (main) + +# --- Резервная площадка (розовый/резервный канал, С as-path prepend) --- +BACKUP_ASN=65103 +BACKUP1_NEIGHBOR_IP=192.168.201.10 # Client Edge Router-B1 (backup) +BACKUP2_NEIGHBOR_IP=192.168.201.11 # Client Edge Router-B2 (backup) +AS_PATH_PREPEND_COUNT=3 + +# --- per-node (только то, что реально уникально на узел) --- +VR01_MGMT_IP=192.168.100.21 +VR01_ROUTER_ID=172.31.0.1 +VR01_PLATFORM=PA2 # информационно, роль это значение не использует + +VR02_MGMT_IP=192.168.100.22 +VR02_ROUTER_ID=172.31.0.2 +VR02_PLATFORM=ME1 + +VR03_MGMT_IP=192.168.100.23 +VR03_ROUTER_ID=172.31.0.3 +VR03_PLATFORM=PA2 + +VR04_MGMT_IP=192.168.100.24 +VR04_ROUTER_ID=172.31.0.4 +VR04_PLATFORM=ME1 diff --git a/ansible/site-cloud.yml b/ansible/site-cloud.yml new file mode 100644 index 0000000..1160beb --- /dev/null +++ b/ansible/site-cloud.yml @@ -0,0 +1,6 @@ +--- +- name: Configure Cloud site anycast/sNAT routers (vR01-vR04) + hosts: site_cloud + become: true + roles: + - frr_anycast diff --git a/ansible/tests/render-check-cloud.sh b/ansible/tests/render-check-cloud.sh new file mode 100755 index 0000000..6ae039f --- /dev/null +++ b/ansible/tests/render-check-cloud.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# Офлайн-проверка шаблонов роли frr_anycast для окружения site_cloud (4 узла, +# 2 канала, as-path prepend только на резервном, sNAT). Golden-файлов для +# этого окружения нет (оно новое, живых ВМ нет) — проверяем grep'ом ожидаемые +# инварианты вместо byte-diff. +set -e +cd "$(dirname "$0")/.." # -> ansible/ + +ENV_FILE="${SITE_ENV_FILE:-site-cloud.env}" +if [ ! -f "$ENV_FILE" ]; then + echo "Не найден файл переменных окружения: $ENV_FILE" >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +. "./$ENV_FILE" +set +a + +OUT_DIR="$(mktemp -d)" +trap 'rm -rf "$OUT_DIR"' EXIT + +ansible-playbook -i inventory/site-cloud.yml tests/render-test-cloud.yml -e "render_out_dir=$OUT_DIR" 2>&1 | cat + +STATUS=0 +check() { + # check