Add Ansible playbook for Site A FRR configuration on VMs
Разворачивает 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bc3HkMiDwooSyLBLoMKNyF
This commit is contained in:
1 parent
eaa577e6b7
commit
98f9f502ce
21 files changed
+456
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
venv/
|
||||||
|
*.retry
|
||||||
@@ -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.
|
- Линки между 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-узел.
|
- 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 <node> vtysh ...` на `ssh <node> vtysh ...` или `ansible site_a -m command -a "vtysh -c 'show bgp summary'"`).
|
||||||
@@ -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
|
||||||
@@ -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) }}"
|
||||||
@@ -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') }}"
|
||||||
@@ -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') }}"
|
||||||
@@ -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') }}"
|
||||||
@@ -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') }}"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
network:
|
||||||
|
version: 2
|
||||||
|
dummy-devices:
|
||||||
|
{{ dummy_iface }}:
|
||||||
|
addresses:
|
||||||
|
- {{ anycast_prefix }}
|
||||||
@@ -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
|
||||||
|
!
|
||||||
Executable
+27
@@ -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" "$@"
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
- name: Configure Site A FRR anycast routers
|
||||||
|
hosts: site_a
|
||||||
|
become: true
|
||||||
|
roles:
|
||||||
|
- frr_anycast
|
||||||
Executable
+49
@@ -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"
|
||||||
@@ -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"
|
||||||
@@ -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 обновлены.
|
||||||
@@ -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`.
|
||||||
Reference in new issue
Block a user