--- - name: Start spines and leafs and add them to inventory hosts: localhost vars: leafs_list: - leaf-0 - leaf-1 - leaf-2 - leaf-3 - leaf-4 - leaf-5 spines_list: - spine-0 - spine-1 routers_list: - router-0 fabric_list: "{{ leafs_list + spines_list + routers_list }}" tasks: - name: Start spine and leaf VMs delegate_to: hypervisor become: true community.libvirt.virt: name: "cifmw-{{ item }}" state: running loop: "{{ fabric_list }}" - name: Add leafs group to inventory when: '"leafs" not in groups' ansible.builtin.add_host: name: "{{ item }}.utility" group: leafs loop: "{{ leafs_list }}" - name: Add spines group to inventory when: '"spines" not in groups' ansible.builtin.add_host: name: "{{ item }}.utility" group: spines loop: "{{ spines_list }}" - name: Add routers group to inventory when: '"routers" not in groups' ansible.builtin.add_host: name: "{{ item }}.utility" group: routers loop: "{{ routers_list }}" - name: Check SSH connectivity delegate_to: hypervisor ansible.builtin.wait_for: port: 22 host: "{{ item }}.utility" search_regex: OpenSSH delay: 10 timeout: 120 loop: "{{ fabric_list }}" - name: Common spines and leaves configuration hosts: "spines,leafs{{ router_bool | default(false) | ternary(',routers', '') }}" tasks: - name: Workaround router advertisement packets polluting routing tables become: true ansible.builtin.shell: cmd: | for i in $(ls /proc/sys/net/ipv6/conf/*/forwarding); do echo 1 > $i; done changed_when: false - name: Register interfaces ansible.builtin.shell: cmd: "set -o pipefail && ls -1 /proc/sys/net/ipv4/conf/*/rp_filter | cut -d/ -f7" register: interfaces changed_when: false - name: Disable reverse path forwarding validation become: true ansible.posix.sysctl: name: "net.ipv4.conf.{{ item }}.rp_filter" value: "0" sysctl_set: true sysctl_file: /etc/sysctl.d/sysctl.conf state: present reload: true loop: "{{ interfaces.stdout_lines }}" register: result retries: 3 timeout: 60 until: result is not failed - name: Disable reverse path forwarding validation become: true ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" sysctl_set: true sysctl_file: /etc/sysctl.d/sysctl.conf state: present reload: true with_dict: net.ipv4.conf.all.rp_filter: '0' net.ipv4.conf.default.rp_filter: '0' register: result retries: 3 timeout: 60 until: result is not failed - name: Set IPv4 forwarding become: true ansible.posix.sysctl: name: net.ipv4.ip_forward value: '1' sysctl_set: true sysctl_file: /etc/sysctl.d/90-network.conf state: present reload: true - name: Set IPv6 forwarding become: true ansible.posix.sysctl: name: net.ipv6.conf.all.forwarding value: '1' sysctl_set: true sysctl_file: /etc/sysctl.d/90-network.conf state: present reload: true - name: Check installed packages ansible.builtin.package_facts: manager: auto - name: Install FRR when: '"frr" not in ansible_facts.packages' block: - name: Install RHOS Release tool become: true ansible.builtin.package: name: "{{ cifmw_repo_setup_rhos_release_rpm }}" state: present disable_gpg_check: true - name: Enable RHOS release repos. become: true ansible.builtin.command: cmd: "rhos-release rhel" changed_when: false - name: Install frr become: true ansible.builtin.package: name: frr state: present register: frr_present retries: 10 delay: 2 until: frr_present is success - name: Enable FRR BGP daemon become: true ansible.builtin.lineinfile: path: /etc/frr/daemons regexp: "^bgpd=" line: "bgpd=yes" owner: frr group: frr mode: '640' - name: Enable FRR BFD daemon become: true ansible.builtin.lineinfile: path: /etc/frr/daemons regexp: "^bfdd=" line: "bfdd=yes" owner: frr group: frr mode: '640' - name: Enable retain option of zebra become: true ansible.builtin.lineinfile: path: /etc/frr/daemons regexp: "^zebra_options=" line: "zebra_options=\" -A 127.0.0.1 -s 90000000 -r \"" owner: frr group: frr mode: '640' # Router play - name: Configure router hosts: "{{ router_bool | default(false) | ternary('routers', 'localhost') }}" vars: _ip_version: "{{ ip_version | default(4) | int }}" tasks: - name: Early end if no router defined ansible.builtin.meta: end_play when: not (router_bool | default(false)) - name: Obtain the connection for the eth0 interface ansible.builtin.command: cmd: > nmcli -g GENERAL.CONNECTION device show eth0 register: router_eth0_conn changed_when: false # When eth0 connection name is "Wired connection 1", then the rest of the # connection names corresponding to the interfaces will follow this pattern: # eth1 -> "Wired connection 2" # eth2 -> "Wired connection 3" # When eth0 connection name is different from "Wired connection 1", then the # rest of the connection names corresponding to the interfaces will follow # this pattern: # eth1 -> "Wired connection 1" # eth2 -> "Wired connection 2" - name: Set router_conn_name_offset ansible.builtin.set_fact: router_conn_name_offset: >- {{ 1 if "Wired connection 1" == (router_eth0_conn.stdout | trim) else 0 }} - name: Build downlink connection list vars: connection_name: "Wired connection {{ (item | int) + (router_conn_name_offset | int) }}" interface_name: "eth{{ item }}" ansible.builtin.set_fact: router_downlink_conns: "{{ router_downlink_conns | default([]) + [connection_name] }}" router_downlink_ifs: "{{ router_downlink_ifs | default([]) + [interface_name] }}" loop: "{{ range(1, 3) | list }}" # the number of spines is always 2 - name: Build uplink connection vars: len_router_downlink_conns: "{{ router_downlink_conns | length }}" ansible.builtin.set_fact: router_uplink_conn: "Wired connection {{ 1 + (len_router_downlink_conns | int) + (router_conn_name_offset | int) }}" router_uplink_if: "eth{{ 1 + (len_router_downlink_conns | int) }}" - name: Configure downlink router connections with nmcli become: true community.general.nmcli: autoconnect: true conn_name: "{{ item }}" type: ethernet ifname: "{{ router_downlink_ifs[loop_index | int] }}" method4: disabled method6: link-local state: present loop: "{{ router_downlink_conns }}" loop_control: index_var: loop_index # uplink router IPv4 is configured for both IPv4 and IPv6 jobs - name: Configure uplink router connections with nmcli when IPv4 become: true community.general.nmcli: autoconnect: true conn_name: "{{ router_uplink_conn }}" # mask changed to /24 due to https://github.com/openstack-k8s-operators/architecture/pull/466 ip4: "{{ router_uplink_ip }}/24" method4: manual method6: link-local state: present when: _ip_version == 4 - name: Configure uplink router connections with nmcli when IPv6 become: true community.general.nmcli: autoconnect: true conn_name: "{{ router_uplink_conn }}" ip4: "{{ router_uplink_ip }}/24" ip6: "{{ router_uplink_ipv6 }}/126" method4: manual method6: manual state: present when: _ip_version == 6 - name: Add provider network gateway IP to router loopback become: true community.general.nmcli: autoconnect: true conn_name: lo ip4: - 127.0.0.1/8 - 192.168.133.1/32 method4: manual ip6: "::1/128" method6: manual state: present - name: Configure FRR vars: _router_id: "{{ '' if _ip_version == 4 else '1.1.1.1' }}" become: true ansible.builtin.template: src: templates/router-frr.conf.j2 dest: /etc/frr/frr.conf owner: frr group: frr mode: '640' - name: Enable and start FRR become: true ansible.builtin.service: name: frr enabled: true state: restarted - name: Masquerade mortacci block: - name: Install iptables become: true ansible.builtin.package: name: iptables state: present - name: Masquerade outgoing traffic vars: router_ext_if: eth0 become: true ansible.builtin.shell: cmd: > {% if _ip_version == 4 %} iptables -t nat -A POSTROUTING -s 99.99.0.0/16 -o {{ router_ext_if }} -j MASQUERADE && iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -o {{ router_ext_if }} -j MASQUERADE {% else %} ip6tables -t nat -A POSTROUTING -s f00d:f00d:f00d:f00d:99:99::/96 -o {{ router_ext_if }} -j MASQUERADE {% endif %} changed_when: false - name: Add route to RH intranet from router via HV when IPv6 when: _ip_version == 6 block: - name: Obtain the router default IPv6 route ansible.builtin.shell: cmd: > set -o pipefail && ip -6 r show default | grep "proto ra" | head -1 register: router_default_ra_route changed_when: false # NOTE: This route is not persistent, but it is ok because the router will not be rebooted. # Adding this route NM is a bit overkill (a config file has to be created for it) - name: Add route from router to RH intranet via HV when IPv6 vars: router_default_ra_route_list: "{{ router_default_ra_route.stdout | trim | split | list }}" become: true ansible.builtin.shell: cmd: | ip r del 10.0.0.0/8 || true ip r add 10.0.0.0/8 via inet6 {{ router_default_ra_route_list[2] }} dev {{ router_default_ra_route_list[4] }} changed_when: false - name: Restart NetworkManager become: true ansible.builtin.systemd: name: NetworkManager.service state: restarted # Spines play - name: Configure spines hosts: spines vars: _ip_version: "{{ ip_version | default(4) | int }}" tasks: - name: Obtain the connection for the eth0 interface ansible.builtin.command: cmd: > nmcli -g GENERAL.CONNECTION device show eth0 register: spine_eth0_conn changed_when: false - name: Set spine_conn_name_offset ansible.builtin.set_fact: spine_conn_name_offset: >- {{ 1 if "Wired connection 1" == (spine_eth0_conn.stdout | trim) else 0 }} - name: Build downlink connection list vars: num_conns: "{{ (num_racks | default(4) | int) * 2 }}" connection_name: "Wired connection {{ (item | int) + (spine_conn_name_offset | int) }}" interface_name: "eth{{ item }}" ansible.builtin.set_fact: spine_downlink_conns: "{{ spine_downlink_conns | default([]) + [connection_name] }}" spine_downlink_ifs: "{{ spine_downlink_ifs | default([]) + [interface_name] }}" loop: "{{ range(1, 1 + (num_conns | int)) | list }}" - name: Build uplink connection vars: len_spine_downlink_conns: "{{ spine_downlink_conns | length }}" ansible.builtin.set_fact: spine_uplink_conn: "Wired connection {{ 1 + (len_spine_downlink_conns | int) + (spine_conn_name_offset | int) }}" spine_uplink_if: "eth{{ 1 + (len_spine_downlink_conns | int) }}" - name: Configure spine connections with nmcli become: true vars: spine_conns: >- {{ router_bool | default(false) | ternary(spine_downlink_conns + [spine_uplink_conn], spine_downlink_conns) }} community.general.nmcli: autoconnect: true conn_name: "{{ item }}" type: ethernet method4: disabled method6: link-local state: present loop: "{{ spine_conns }}" - name: Configure FRR vars: _router_id: "{{ '' if _ip_version == 4 else '1.1.1.10' + ansible_hostname.split('-')[-1]}}" become: true ansible.builtin.template: src: templates/spine-frr.conf.j2 dest: /etc/frr/frr.conf owner: frr group: frr mode: '640' - name: Enable and start FRR become: true ansible.builtin.service: name: frr enabled: true state: restarted - name: Masquerade mortacci when: not (router_bool | default(false)) block: - name: Install iptables become: true ansible.builtin.package: name: iptables state: present - name: Masquerade outgoing traffic become: true ansible.builtin.shell: cmd: > iptables -t nat -A POSTROUTING -s 99.99.0.0/16 -o {{ spine_uplink_if }} -j MASQUERADE && iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -o {{ spine_uplink_if }} -j MASQUERADE changed_when: false # Leaves play - name: Configure leaves hosts: leafs vars: leaf_id: "{{ (ansible_hostname.split('-')[-1] | int) % 2 }}" # always 2 leaves per rack rack_id: "{{ (ansible_hostname.split('-')[-1] | int) // 2 }}" # always 2 leaves per rack _ip_version: "{{ ip_version | default(4) | int }}" tasks: - name: Obtain the connection for the eth0 interface ansible.builtin.command: cmd: > nmcli -g GENERAL.CONNECTION device show eth0 register: leaf_eth0_conn changed_when: false - name: Set leaf_conn_name_offset ansible.builtin.set_fact: leaf_conn_name_offset: >- {{ 1 if "Wired connection 1" == (leaf_eth0_conn.stdout | trim) else 0 }} - name: Build uplink connection list vars: connection_name: "Wired connection {{ (item | int) + (leaf_conn_name_offset | int) }}" interface_name: "eth{{ item }}" ansible.builtin.set_fact: uplink_conns: "{{ uplink_conns | default([]) + [connection_name] }}" uplink_ifs: "{{ uplink_ifs | default([]) + [interface_name] }}" loop: "{{ range(1, 3) | list }}" # the number of spines is always 2 - name: Build downlink connection list vars: num_conns: "{{ (edpm_nodes_per_rack | default(1) | int) + (ocp_nodes_per_rack | default(0) | int) }}" connection_name: "Wired connection {{ (item | int) + (leaf_conn_name_offset | int) }}" interface_name: "eth{{ item }}" ansible.builtin.set_fact: leaf_downlink_conns: "{{ leaf_downlink_conns | default([]) + [connection_name] }}" leaf_downlink_ifs: "{{ leaf_downlink_ifs | default([]) + [interface_name] }}" loop: "{{ range(3, 3 + (num_conns | int)) | list }}" - name: Build downlink connection list for rack3 vars: connection_name: "Wired connection {{ (item | int) + (leaf_conn_name_offset | int) }}" interface_name: "eth{{ item }}" ansible.builtin.set_fact: downlink_conns_rack3: "{{ downlink_conns_rack3 | default([]) + [connection_name] }}" downlink_ifs_rack3: "{{ downlink_ifs_rack3 | default([]) + [interface_name] }}" loop: "{{ range(3, 6) | list }}" # number of OCP nodes on rack3 is always 3 - name: Configure downlink leaf connections IPv4 when: - _ip_version == 4 block: # rack3 is special because only OCP nodes are deployed on it when it exists - name: Configure downlink leaf connections on rack3 become: true vars: leaf_ds_ip4: >- 100.{{ 64 + (leaf_id | int) }}.{{ rack_id }}.{{ 1 + 4 * (loop_index | int) }} when: (rack_id | int) == 3 community.general.nmcli: autoconnect: true conn_name: "{{ item }}" ip4: "{{ leaf_ds_ip4 }}/30" type: ethernet ifname: "{{ downlink_ifs_rack3[loop_index | int] }}" method4: manual method6: link-local state: present loop: "{{ downlink_conns_rack3 }}" loop_control: index_var: loop_index - name: Configure downlink leaf connections on racks 0, 1 and 2 become: true vars: leaf_ds_ip4: >- 100.{{ 64 + (leaf_id | int) }}.{{ rack_id }}.{{ 1 + 4 * (loop_index | int) }} when: (rack_id | int) != 3 community.general.nmcli: autoconnect: true conn_name: "{{ item }}" ip4: "{{ leaf_ds_ip4 }}/30" type: ethernet ifname: "{{ leaf_downlink_ifs[loop_index | int] }}" method4: manual method6: link-local state: present loop: "{{ leaf_downlink_conns }}" loop_control: index_var: loop_index - name: Configure FRR become: true vars: downlink_interfaces: "{{ downlink_ifs_rack3 if (rack_id | int) == 3 else leaf_downlink_ifs }}" _router_id: '' ansible.builtin.template: src: templates/leaf-frr.conf.j2 dest: /etc/frr/frr.conf owner: frr group: frr mode: '640' - name: Configure downlink leaf connections IPv6 when: - _ip_version == 6 block: - name: Fail if num_racks > 3 ansible.builtin.assert: that: - num_racks | default(4) | int <= 3 fail_msg: "num_racks must be lower than 3 when IPv6 is used" changed_when: false - name: Configure downlink leaf connections on racks 0, 1 and 2 become: true vars: _end_byte: "{{ '{:x}'.format(1 + 4 * (loop_index | int)) }}" _leaf_ds_ip6: >- 2620:cf::100:{{ 64 + (leaf_id | int) }}:{{ rack_id }}:{{ _end_byte }} _leaf_ds_ip4: >- 100.{{ 64 + (leaf_id | int) }}.{{ rack_id }}.{{ 1 + 4 * (loop_index | int) }} community.general.nmcli: autoconnect: true conn_name: "{{ item }}" ip4: "{{ _leaf_ds_ip4 }}/30" ip6: "{{ _leaf_ds_ip6 }}/126" type: ethernet ifname: "{{ leaf_downlink_ifs[loop_index | int] }}" method4: manual method6: manual state: present loop: "{{ leaf_downlink_conns }}" loop_control: index_var: loop_index - name: Create list of IPv6 downstream peers per leaf vars: _end_byte: "{{ '{:x}'.format(2 + 4 * (loop_index | int)) }}" _leaf_ds_ip6_peer: >- 2620:cf::100:{{ 64 + (leaf_id | int) }}:{{ rack_id }}:{{ _end_byte }} ansible.builtin.set_fact: leaf_ds_ip6_peer_list: "{{ leaf_ds_ip6_peer_list | default([]) + [_leaf_ds_ip6_peer] }}" loop: "{{ leaf_downlink_conns }}" loop_control: index_var: loop_index - name: Configure FRR become: true vars: _router_id: "{{ '1.1.1.20' + ansible_hostname.split('-')[-1] }}" ansible.builtin.template: src: templates/leaf-frr.conf.j2 dest: /etc/frr/frr.conf owner: frr group: frr mode: '640' - name: Configure uplink leaf connections become: true community.general.nmcli: autoconnect: true conn_name: "{{ item }}" method4: disabled method6: link-local type: ethernet ifname: "{{ uplink_ifs[loop_index | int] }}" state: present loop: "{{ uplink_conns }}" loop_control: index_var: loop_index - name: Enable FRR Zebra daemon become: true ansible.builtin.lineinfile: path: /etc/frr/daemons regexp: "^zebra=" line: "zebra=yes" owner: frr group: frr mode: '640' - name: Enable and start FRR become: true ansible.builtin.service: name: frr enabled: true state: restarted # Final play to remove DHCP default routes - name: Remove DHCP default routes and use BGP instead hosts: "leafs{{ router_bool | default(false) | ternary(',spines', '') }}" vars: _dash_six: "{{ '' if (ip_version | default(4) | int) == 4 else '-6' }}" _proto: "{{ 'dhcp' if (ip_version | default(4) | int) == 4 else 'ra' }}" tasks: - name: Check default route corresponds with BGP ansible.builtin.command: cmd: > ip {{ _dash_six }} route show default register: _initial_default_ip_route_result changed_when: false - name: Early end if default route is already based on BGP ansible.builtin.meta: end_play when: "'proto bgp' in _initial_default_ip_route_result.stdout" - name: Apply the BGP default routes ansible.builtin.include_tasks: tasks/apply_bgp_default_routes.yaml