From 98f9f502ce4a8d13c1aee0da53bcec44b830f678 Mon Sep 17 00:00:00 2001 From: ayurishchev Date: Thu, 3 Sep 2026 14:14:04 +0300 Subject: [PATCH] Add Ansible playbook for Site A FRR configuration on VMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Разворачивает FRR на a1/a2/a3 (площадка A) через Ansible вместо Docker-лабы, для реальных ВМ. Provisioning ВМ (3 NIC: внешний + 2 приватных) — Terraform, вне рамок этой задачи; playbook создаёт только dummy0 (netplan) и конфигурирует FRR. - ansible/site-a.env — единственный источник site-A-специфичных значений (ASN, anycast-префикс, router-id/BGP-соседи по узлам, имена интерфейсов); роль frr_anycast полностью параметризована - ansible/roles/frr_anycast — задачи, handlers (netplan apply перед restart frr), шаблоны frr.conf.j2/daemons.j2/dummy0.netplan.yml.j2 - ansible/tests/render-check.sh — офлайн golden-diff проверка рендера конфигов против уже провалидированных в Docker-лабе frr/a{1,2,3}/ (ВМ ещё не подняты, функциональный тест недоступен) - README.md — раздел «Развёртывание площадки A на ВМ (Ansible)» - docs/ — план и summary изменения Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF --- .gitignore | 2 + README.md | 44 ++++++++++++ ansible/ansible.cfg | 9 +++ ansible/group_vars/site_a.yml | 8 +++ ansible/host_vars/a1.yml | 6 ++ ansible/host_vars/a2.yml | 6 ++ ansible/host_vars/a3.yml | 6 ++ ansible/inventory/site-a.yml | 13 ++++ ansible/roles/frr_anycast/defaults/main.yml | 15 ++++ ansible/roles/frr_anycast/handlers/main.yml | 13 ++++ ansible/roles/frr_anycast/tasks/main.yml | 68 +++++++++++++++++++ .../roles/frr_anycast/templates/daemons.j2 | 25 +++++++ .../templates/dummy0.netplan.yml.j2 | 6 ++ .../roles/frr_anycast/templates/frr.conf.j2 | 20 ++++++ ansible/run.sh | 27 ++++++++ ansible/site-a.env | 42 ++++++++++++ ansible/site-a.yml | 6 ++ ansible/tests/render-check.sh | 49 +++++++++++++ ansible/tests/render-test.yml | 32 +++++++++ docs/2026-09-03-ansible-site-a-plan.md | 22 ++++++ docs/2026-09-03-ansible-site-a-summary.md | 37 ++++++++++ 21 files changed, 456 insertions(+) create mode 100644 .gitignore create mode 100644 ansible/ansible.cfg create mode 100644 ansible/group_vars/site_a.yml create mode 100644 ansible/host_vars/a1.yml create mode 100644 ansible/host_vars/a2.yml create mode 100644 ansible/host_vars/a3.yml create mode 100644 ansible/inventory/site-a.yml create mode 100644 ansible/roles/frr_anycast/defaults/main.yml create mode 100644 ansible/roles/frr_anycast/handlers/main.yml create mode 100644 ansible/roles/frr_anycast/tasks/main.yml create mode 100644 ansible/roles/frr_anycast/templates/daemons.j2 create mode 100644 ansible/roles/frr_anycast/templates/dummy0.netplan.yml.j2 create mode 100644 ansible/roles/frr_anycast/templates/frr.conf.j2 create mode 100755 ansible/run.sh create mode 100644 ansible/site-a.env create mode 100644 ansible/site-a.yml create mode 100755 ansible/tests/render-check.sh create mode 100644 ansible/tests/render-test.yml create mode 100644 docs/2026-09-03-ansible-site-a-plan.md create mode 100644 docs/2026-09-03-ansible-site-a-summary.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c58777c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv/ +*.retry diff --git a/README.md b/README.md index d772391..ff966b6 100644 --- a/README.md +++ b/README.md @@ -82,3 +82,47 @@ docker compose exec -T c1 curl -s http://10.200.200.1/ - Линки между A и B заведены как `/29` (не `/30`, как в hld.md буквально) — Docker резервирует первый адрес подсети под gateway моста даже для `internal: true`-сетей, поэтому реальным peering-адресам (`.1`/`.2`, как в hld.md) нужно место рядом с ним. IP-адресация роутеров и логика BGP при этом полностью соответствуют hld.md. - ECMP-балансировка на b1/b2 требует `net.ipv4.fib_multipath_hash_policy=1` (в `docker-compose.yml`) — по умолчанию (`0`) Linux хеширует next-hop только по src/dst IP, без учёта портов, из-за чего один клиент всегда попадал бы в один и тот же anycast-узел. + +## Развёртывание площадки A на ВМ (Ansible) + +`ansible/` — playbook для конфигурации FRR на реальных ВМ площадки A (a1/a2/a3), продолжение того же HLD, но не в Docker, а «в проде». Provisioning ВМ (создание, 3 сетевых интерфейса: 1 внешний/управляющий + 2 приватных к B1/B2) выполняется отдельным Terraform-сценарием (вне рамок этого репозитория) — приватные интерфейсы уже сконфигурированы к моменту запуска playbook; Ansible создаёт `dummy0` (anycast) и разворачивает FRR. + +Все специфичные для площадки 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). + +### Структура + +``` +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 офлайн-проверка шаблонов без реальных ВМ (см. ниже) +``` + +### Запуск + +```bash +cd ansible +./run.sh site-a.yml # применить конфигурацию к a1/a2/a3 +./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`, если он есть. + +### Проверка без живых ВМ + +На момент подготовки playbook ВМ площадки A ещё не подняты (Terraform-провижининг — отдельный шаг), поэтому функциональная проверка (`vtysh`, реальный apply) не выполнялась и не входит в текущую поставку. Доступна и выполнена офлайн-проверка: + +```bash +ansible/run.sh site-a.yml --syntax-check # синтаксис +ansible/tests/render-check.sh # golden-diff: рендер frr.conf/daemons для a1/a2/a3 + # сравнивается с уже работавшими в Docker-лабе frr/a{1,2,3}/ +``` + +После того как 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'"`). diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..8a51803 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,9 @@ +[defaults] +inventory = inventory/site-a.yml +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml + +[privilege_escalation] +become = True +become_method = sudo diff --git a/ansible/group_vars/site_a.yml b/ansible/group_vars/site_a.yml new file mode 100644 index 0000000..bb0dcc2 --- /dev/null +++ b/ansible/group_vars/site_a.yml @@ -0,0 +1,8 @@ +# Site-wide переменные площадки A — тонкий адаптер над ansible/site-a.env. +# Ничего site-специфичного не хардкодится: только имена ключей env-файла. +site_a_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') }}" +priv2_iface: "{{ lookup('env', 'PRIV2_IFACE') }}" +frr_apt_series: "{{ lookup('env', 'FRR_APT_SERIES') | default('frr-stable', true) }}" diff --git a/ansible/host_vars/a1.yml b/ansible/host_vars/a1.yml new file mode 100644 index 0000000..bdfbd70 --- /dev/null +++ b/ansible/host_vars/a1.yml @@ -0,0 +1,6 @@ +# 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') }}" diff --git a/ansible/host_vars/a2.yml b/ansible/host_vars/a2.yml new file mode 100644 index 0000000..465c445 --- /dev/null +++ b/ansible/host_vars/a2.yml @@ -0,0 +1,6 @@ +# 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') }}" diff --git a/ansible/host_vars/a3.yml b/ansible/host_vars/a3.yml new file mode 100644 index 0000000..618332b --- /dev/null +++ b/ansible/host_vars/a3.yml @@ -0,0 +1,6 @@ +# 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') }}" diff --git a/ansible/inventory/site-a.yml b/ansible/inventory/site-a.yml new file mode 100644 index 0000000..9277d42 --- /dev/null +++ b/ansible/inventory/site-a.yml @@ -0,0 +1,13 @@ +all: + children: + site_a: + hosts: + a1: + ansible_host: "{{ lookup('env', 'A1_MGMT_IP') }}" + a2: + ansible_host: "{{ lookup('env', 'A2_MGMT_IP') }}" + a3: + ansible_host: "{{ lookup('env', 'A3_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 new file mode 100644 index 0000000..c2d9c1b --- /dev/null +++ b/ansible/roles/frr_anycast/defaults/main.yml @@ -0,0 +1,15 @@ +--- +# Не site-специфичные дефолты роли (пакеты, пути). Все site-A-специфичные +# значения приходят из group_vars/host_vars (см. ansible/site-a.env). +frr_packages: + - frr + - frr-pythontools + +frr_daemons_enabled: + - zebra + - bgpd + - staticd + +frr_conf_path: /etc/frr/frr.conf +frr_daemons_path: /etc/frr/daemons +netplan_dummy_config_path: /etc/netplan/90-anycast-dummy0.yaml diff --git a/ansible/roles/frr_anycast/handlers/main.yml b/ansible/roles/frr_anycast/handlers/main.yml new file mode 100644 index 0000000..491c2bf --- /dev/null +++ b/ansible/roles/frr_anycast/handlers/main.yml @@ -0,0 +1,13 @@ +--- +# Порядок важен: Ansible исполняет handlers в порядке ИХ ОБЪЯВЛЕНИЯ здесь, +# а не в порядке notify. "Apply netplan" должен отработать раньше "Restart +# frr", иначе dummy0 (и anycast-адрес на нём) ещё не будет существовать к +# моменту старта zebra/bgpd. +- name: Apply netplan + ansible.builtin.command: netplan apply + changed_when: true + +- name: Restart frr + ansible.builtin.systemd: + name: frr + state: restarted diff --git a/ansible/roles/frr_anycast/tasks/main.yml b/ansible/roles/frr_anycast/tasks/main.yml new file mode 100644 index 0000000..c0fc460 --- /dev/null +++ b/ansible/roles/frr_anycast/tasks/main.yml @@ -0,0 +1,68 @@ +--- +- name: Pre-flight | приватные интерфейсы должны быть уже сконфигурированы Terraform-слоем + ansible.builtin.assert: + that: + - priv1_iface in ansible_facts.interfaces + - priv2_iface in ansible_facts.interfaces + - ansible_facts[priv1_iface]['active'] | default(false) + - ansible_facts[priv2_iface]['active'] | default(false) + fail_msg: >- + Интерфейсы {{ priv1_iface }}/{{ priv2_iface }} не найдены или не активны. + Ожидается, что Terraform-провижининг уже создал и поднял их до запуска + этого playbook. + success_msg: "Приватные интерфейсы {{ priv1_iface }} и {{ priv2_iface }} на месте." + +- name: FRR APT-репозиторий | добавить GPG-ключ + ansible.builtin.get_url: + url: https://deb.frrouting.org/frr/keys.gpg + dest: /usr/share/keyrings/frrouting.gpg + mode: "0644" + +- name: FRR APT-репозиторий | добавить источник пакетов + ansible.builtin.apt_repository: + repo: >- + deb [signed-by=/usr/share/keyrings/frrouting.gpg] + https://deb.frrouting.org/frr {{ ansible_distribution_release }} {{ frr_apt_series }} + filename: frr + state: present + update_cache: true + +- name: Установить пакеты FRR + ansible.builtin.apt: + name: "{{ frr_packages }}" + state: present + +- name: Конфиг | /etc/frr/daemons + ansible.builtin.template: + src: daemons.j2 + dest: "{{ frr_daemons_path }}" + owner: frr + group: frr + mode: "0640" + notify: Restart frr + +- name: Конфиг | /etc/frr/frr.conf + ansible.builtin.template: + src: frr.conf.j2 + dest: "{{ frr_conf_path }}" + owner: frr + group: frr + mode: "0640" + notify: Restart frr + +- name: Сеть | netplan-конфиг для dummy0 (anycast) + ansible.builtin.template: + src: dummy0.netplan.yml.j2 + dest: "{{ netplan_dummy_config_path }}" + owner: root + group: root + mode: "0600" + notify: + - Apply netplan + - Restart frr + +- name: FRR | сервис включён и запущен + ansible.builtin.systemd: + name: frr + enabled: true + state: started diff --git a/ansible/roles/frr_anycast/templates/daemons.j2 b/ansible/roles/frr_anycast/templates/daemons.j2 new file mode 100644 index 0000000..5d76c77 --- /dev/null +++ b/ansible/roles/frr_anycast/templates/daemons.j2 @@ -0,0 +1,25 @@ +zebra=yes +bgpd=yes +ospfd=no +ospf6d=no +ripd=no +ripngd=no +isisd=no +pimd=no +pim6d=no +ldpd=no +nhrpd=no +eigrpd=no +babeld=no +sharpd=no +pbrd=no +bfdd=no +fabricd=no +vrrpd=no +pathd=no +staticd=yes + +vtysh_enable=yes +zebra_options=" -A 127.0.0.1 -s 90000000" +bgpd_options=" -A 127.0.0.1" +staticd_options="-A 127.0.0.1" diff --git a/ansible/roles/frr_anycast/templates/dummy0.netplan.yml.j2 b/ansible/roles/frr_anycast/templates/dummy0.netplan.yml.j2 new file mode 100644 index 0000000..323431d --- /dev/null +++ b/ansible/roles/frr_anycast/templates/dummy0.netplan.yml.j2 @@ -0,0 +1,6 @@ +network: + version: 2 + dummy-devices: + {{ dummy_iface }}: + addresses: + - {{ anycast_prefix }} diff --git a/ansible/roles/frr_anycast/templates/frr.conf.j2 b/ansible/roles/frr_anycast/templates/frr.conf.j2 new file mode 100644 index 0000000..e1eff0e --- /dev/null +++ b/ansible/roles/frr_anycast/templates/frr.conf.j2 @@ -0,0 +1,20 @@ +frr version 8.5 +frr defaults traditional +hostname {{ inventory_hostname }} +log stdout informational +no ipv6 forwarding +! +router bgp {{ site_a_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 }} + ! + address-family ipv4 unicast + network {{ anycast_prefix }} + neighbor {{ neighbor_b1_ip }} activate + neighbor {{ neighbor_b2_ip }} activate + exit-address-family +! +line vty +! diff --git a/ansible/run.sh b/ansible/run.sh new file mode 100755 index 0000000..a9dd326 --- /dev/null +++ b/ansible/run.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# Загружает site-a.env в environment и вызывает ansible-playbook. +# Использование: ./run.sh site-a.yml [доп. аргументы ansible-playbook] +set -e + +cd "$(dirname "$0")" + +ENV_FILE="${SITE_A_ENV_FILE:-site-a.env}" +if [ ! -f "$ENV_FILE" ]; then + echo "Не найден файл переменных окружения: $ENV_FILE" >&2 + exit 1 +fi + +set -a +# shellcheck disable=SC1090 +. "./$ENV_FILE" +set +a + +PLAYBOOK="${1:-site-a.yml}" +shift 2>/dev/null || true + +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" "$@" diff --git a/ansible/site-a.env b/ansible/site-a.env new file mode 100644 index 0000000..38b6578 --- /dev/null +++ b/ansible/site-a.env @@ -0,0 +1,42 @@ +# Все специфичные для площадки A параметры разворачивания (hld.md). +# Источник истины: значения из этого файла и только из него подставляются +# в шаблоны роли frr_anycast — сама роль site-специфичных значений не содержит. +# +# Использование: ./run.sh site-a.yml [доп. аргументы ansible-playbook] +# (run.sh делает `set -a; source site-a.env; set +a` перед вызовом ansible-playbook) + +# --- site-wide --- +SITE_A_ASN=65001 +ANYCAST_PREFIX=10.200.200.1/32 +DUMMY_IFACE=dummy0 +PRIV1_IFACE=ens7 # интерфейс к B1 на всех A-нодах (имя задаёт Terraform-провижининг) +PRIV2_IFACE=ens8 # интерфейс к B2 +FRR_APT_SERIES=frr-8 # ветка репозитория deb.frrouting.org + +# SSH/подключение (пользователь и ключ, которые Terraform выдал ВМ при создании) +SSH_USER=ansible +SSH_PRIVATE_KEY_FILE=~/.ssh/site-a-deploy + +# --- per-node: A1 (bgp router-id 10.0.0.1) --- +A1_MGMT_IP=192.168.100.11 +A1_ROUTER_ID=10.0.0.1 +A1_NEIGHBOR_B1_IP=10.0.1.2 +A1_NEIGHBOR_B1_ASN=65002 +A1_NEIGHBOR_B2_IP=10.0.2.2 +A1_NEIGHBOR_B2_ASN=65003 + +# --- per-node: A2 (bgp router-id 10.0.0.2) --- +A2_MGMT_IP=192.168.100.12 +A2_ROUTER_ID=10.0.0.2 +A2_NEIGHBOR_B1_IP=10.0.3.2 +A2_NEIGHBOR_B1_ASN=65002 +A2_NEIGHBOR_B2_IP=10.0.4.2 +A2_NEIGHBOR_B2_ASN=65003 + +# --- per-node: A3 (bgp router-id 10.0.0.3) --- +A3_MGMT_IP=192.168.100.13 +A3_ROUTER_ID=10.0.0.3 +A3_NEIGHBOR_B1_IP=10.0.5.2 +A3_NEIGHBOR_B1_ASN=65002 +A3_NEIGHBOR_B2_IP=10.0.6.2 +A3_NEIGHBOR_B2_ASN=65003 diff --git a/ansible/site-a.yml b/ansible/site-a.yml new file mode 100644 index 0000000..1790936 --- /dev/null +++ b/ansible/site-a.yml @@ -0,0 +1,6 @@ +--- +- name: Configure Site A FRR anycast routers + hosts: site_a + become: true + roles: + - frr_anycast diff --git a/ansible/tests/render-check.sh b/ansible/tests/render-check.sh new file mode 100755 index 0000000..abb6f16 --- /dev/null +++ b/ansible/tests/render-check.sh @@ -0,0 +1,49 @@ +#!/bin/sh +# Офлайн-проверка шаблонов роли frr_anycast: рендерит frr.conf/daemons для +# a1/a2/a3 (только control node, реальные ВМ не нужны) и сравнивает с уже +# проверенными в Docker-лабе golden-файлами frr/a{1,2,3}/{frr.conf,daemons}. +set -e +cd "$(dirname "$0")/.." # -> ansible/ + +ENV_FILE="${SITE_A_ENV_FILE:-site-a.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-a.yml tests/render-test.yml -e "render_out_dir=$OUT_DIR" 2>&1 | cat + +STATUS=0 +for n in a1 a2 a3; do + for f in frr.conf daemons; do + golden="../frr/$n/$f" + rendered="$OUT_DIR/$n/$f" + # frr.conf в Docker-лабе включает "interface dummy0 / ip address ...": + # там его назначает zebra. В VM-варианте тот же адрес назначается через + # netplan (dummy0.netplan.yml.j2), поэтому в frr.conf.j2 блока нет — + # это осознанное расхождение (см. Context плана), исключаем его перед diff. + if [ "$f" = "frr.conf" ]; then + golden_cmp="$OUT_DIR/.golden-$n-$f" + sed '/^interface dummy0$/,/^!$/d' "$golden" > "$golden_cmp" + else + golden_cmp="$golden" + fi + if diff -u "$golden_cmp" "$rendered" >/tmp/render-check.diff 2>&1; then + echo "OK $n/$f совпадает с $golden (за вычетом дизайн-отличия dummy0, см. выше)" + else + echo "FAIL $n/$f отличается от $golden:" + cat /tmp/render-check.diff + STATUS=1 + fi + done +done + +exit "$STATUS" diff --git a/ansible/tests/render-test.yml b/ansible/tests/render-test.yml new file mode 100644 index 0000000..fab1739 --- /dev/null +++ b/ansible/tests/render-test.yml @@ -0,0 +1,32 @@ +--- +# Рендерит шаблоны роли frr_anycast локально (без SSH и без реальных ВМ) — +# используется render-check.sh для сверки с golden-файлами frr/a{1,2,3}/. +- name: "Render-only test шаблонов frr_anycast" + hosts: site_a + connection: local + gather_facts: false + become: false + tasks: + # group_vars/host_vars авто-подгружаются относительно каталога playbook'а + # или inventory; render-test.yml лежит в tests/, поэтому подключаем явно. + - name: Подключить site-wide переменные (group_vars/site_a.yml) + ansible.builtin.include_vars: "{{ playbook_dir }}/../group_vars/site_a.yml" + + - name: Подключить per-node переменные (host_vars/{{ inventory_hostname }}.yml) + ansible.builtin.include_vars: "{{ playbook_dir }}/../host_vars/{{ inventory_hostname }}.yml" + + - name: Создать выходную директорию для хоста + ansible.builtin.file: + path: "{{ render_out_dir }}/{{ inventory_hostname }}" + state: directory + mode: "0755" + + - name: Рендер frr.conf + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/frr_anycast/templates/frr.conf.j2" + dest: "{{ render_out_dir }}/{{ inventory_hostname }}/frr.conf" + + - name: Рендер daemons + ansible.builtin.template: + src: "{{ playbook_dir }}/../roles/frr_anycast/templates/daemons.j2" + dest: "{{ render_out_dir }}/{{ inventory_hostname }}/daemons" diff --git a/docs/2026-09-03-ansible-site-a-plan.md b/docs/2026-09-03-ansible-site-a-plan.md new file mode 100644 index 0000000..417a7ef --- /dev/null +++ b/docs/2026-09-03-ansible-site-a-plan.md @@ -0,0 +1,22 @@ +# План внедрения: Ansible playbook для настройки площадки A + +**Дата:** 2026-09-03 + +## Цель + +Ansible playbook для конфигурации FRR на ВМ площадки A (a1/a2/a3), полностью параметризованный через файл переменных окружения `ansible/site-a.env`. Provisioning ВМ (3 сетевых интерфейса: 1 внешний + 2 приватных) выполняется отдельным Terraform-сценарием вне рамок этой задачи — приватные интерфейсы и IP на них уже сконфигурированы к моменту запуска playbook; playbook создаёт только `dummy0` (anycast) и разворачивает FRR. + +Полный план (архитектура, структура каталогов, содержимое `site-a.env`, шаблон `frr.conf.j2`, задачи роли, handlers, файлы, проверка) согласован с пользователем в чате — см. историю диалога. Ключевые решения: + +- Сеть: **netplan**, приватные интерфейсы предзаданы Terraform'ом, `dummy0` создаёт Ansible. +- Все site-A-специфичные значения (ASN, anycast-префикс, router-id, BGP-соседи, имена интерфейсов) — в `ansible/site-a.env`, роль `frr_anycast` полностью generic. +- `frr.conf.j2` при рендере для a1/a2/a3 должен побайтово соответствовать уже проверенным в Docker-лабе `frr/a{1,2,3}/frr.conf`. +- **ВМ площадки A не подняты** — функциональная проверка (`vtysh`, реальный apply) невозможна и не входит в эту задачу; проверяется синтаксис + рендеринг шаблонов (golden-file diff против `frr/a{1,2,3}/frr.conf`). +- Ansible уже установлен системно (2.14.18) — venv не потребовался (был бы создан в `venv/` при отсутствии). + +## Definition of done + +- `ansible/` с playbook, ролью `frr_anycast`, inventory, group_vars/host_vars, `site-a.env`, `run.sh`. +- `ansible-playbook --syntax-check` проходит. +- `ansible/tests/render-check.sh` подтверждает: рендер `frr.conf.j2` для a1/a2/a3 совпадает с `frr/a{1,2,3}/frr.conf`. +- README и summary обновлены. diff --git a/docs/2026-09-03-ansible-site-a-summary.md b/docs/2026-09-03-ansible-site-a-summary.md new file mode 100644 index 0000000..3ee312b --- /dev/null +++ b/docs/2026-09-03-ansible-site-a-summary.md @@ -0,0 +1,37 @@ +# Summary: Ansible playbook для настройки площадки A + +**Дата:** 2026-09-03 +**План:** [2026-09-03-ansible-site-a-plan.md](2026-09-03-ansible-site-a-plan.md) + +## Что сделано + +Создан `ansible/` — playbook + роль `frr_anycast` для конфигурации FRR на ВМ площадки A (a1/a2/a3). Provisioning ВМ (3 NIC: 1 внешний + 2 приватных) — Terraform, вне рамок этой задачи; playbook создаёт только `dummy0` (netplan) и разворачивает FRR. + +**Параметризация подтверждена практически, а не только на словах**: единственный источник site-A-специфичных значений — `ansible/site-a.env` (плоский `KEY=VALUE`). Цепочка `env → group_vars/site_a.yml + host_vars/a{1,2,3}.yml (lookup('env', ...)) → роль frr_anycast → шаблоны` — ни один IP/ASN/router-id не захардкожен ни в роли, ни в `frr.conf.j2`/`daemons.j2`/`dummy0.netplan.yml.j2`. + +Ansible оказался уже установлен системно (core 2.14.18) — venv по плану не потребовался (сценарий с `python3 -m venv venv` реализован в `run.sh` как fallback, но не задействован). `ansible-lint` не установлен и не устанавливался — по плану опционален. + +## Отклонения/уточнения в процессе реализации + +1. **`ERROR: Ansible requires blocking IO`** — известное ограничение ansible-playbook в этом sandboxed-окружении при прямом запуске из Bash-тула; решается пайпом через `cat` (`... 2>&1 | cat`). Учтено в `render-check.sh`. +2. **`group_vars`/`host_vars` не подхватились в тестовом плейбуке** — Ansible ищет их относительно каталога *переданного* playbook'а и inventory-файла; `ansible/tests/render-test.yml` лежит в подкаталоге `tests/`, поэтому `ansible/group_vars`/`ansible/host_vars` не попадали в scope автоматически. Продакшн-playbook `ansible/site-a.yml` (лежит прямо в `ansible/`) эту проблему не имеет — переменные подхватываются штатно. Для теста добавлены явные задачи `include_vars`. +3. **`daemons.j2` переписан без Jinja-цикла** — изначальный вариант с `{% for %}` рисковал дать лишние пустые строки при рендере (поведение `trim_blocks`/`lstrip_blocks`), а набор демонов не site-специфичен (одинаков на всех A-нодах) — статичный текст безопаснее и предсказуемее. + +## Результаты проверки (без живых ВМ — они не подняты) + +- `ansible-playbook --syntax-check` — OK. +- **`ansible/tests/render-check.sh`** — golden-diff рендера `frr.conf.j2`/`daemons.j2` для a1/a2/a3 против уже подтверждённых живыми BGP-сессиями (10/10 Established, 3-way ECMP — см. `docs/2026-09-03-anycast-lab-summary.md`) `frr/a{1,2,3}/{frr.conf,daemons}`: + - `daemons` — совпадает побайтово для всех трёх узлов. + - `frr.conf` — совпадает полностью, за вычетом блока `interface dummy0` (осознанно вынесен в netplan, см. Context плана) — единственное различие подтверждено и является дизайн-решением, а не ошибкой. +- Синтаксис Jinja-шаблона netplan (`dummy0.netplan.yml.j2`) проверен рендером с тестовыми значениями и парсингом через `yaml.safe_load` — валиден. + +## Известное ограничение + +Реальный apply (`apt install frr`, `netplan apply`, `systemctl restart frr`) и функциональная проверка (`show bgp summary` и т.д. на живых ВМ) не выполнялись — ВМ площадки A ещё не подняты Terraform-слоем. Это нужно сделать вручную после провижининга (команды — в README, раздел «Развёртывание площадки A на ВМ»). + +## Изменённые/новые файлы + +- `ansible/` — вся структура (site-a.env, run.sh, ansible.cfg, inventory/, group_vars/, host_vars/, site-a.yml, roles/frr_anycast/, tests/). +- `.gitignore` — новый (`venv/`, `*.retry`). +- `README.md` — новый раздел «Развёртывание площадки A на ВМ (Ansible)». +- `docs/2026-09-03-ansible-site-a-plan.md`, `docs/2026-09-03-ansible-site-a-summary.md`.