diff --git a/playbooks/site.yml b/playbooks/site.yml index afc0cf9e..a419de73 100644 --- a/playbooks/site.yml +++ b/playbooks/site.yml @@ -15,6 +15,15 @@ - role: repository tags: [preinstall, prometheus] +- hosts: + - "{{ frontend_group | d('frontend') }}" + - "{{ node_group | d('node') }}" + collections: + - opennebula.deploy + roles: + - role: openvswitch + tags: [network, openvswitch] + - hosts: "{{ frontend_group | d('frontend') }}" tags: [frontend, stage1] collections: diff --git a/roles/openvswitch/README.md b/roles/openvswitch/README.md new file mode 100644 index 00000000..cc3154c2 --- /dev/null +++ b/roles/openvswitch/README.md @@ -0,0 +1,110 @@ +Role: opennebula.deploy.openvswitch +=================================== + +A role that **replaces** OS default networking with OVS/DPDK. + +Requirements +------------ + +N/A + +Role Variables +-------------- + +| Name | Type | Default | Example | Description | +|----------------|--------|-----------------------|---------------|------------------------------------------| +| `ovs` | `dict` | (check role defaults) | (check below) | OVS/DPDK config. | +| `ovs_packages` | `dict` | (check role defaults) | | OVS/DPDK packages grouped per OS distro. | + +Dependencies +------------ + +N/A + +Example Playbook +---------------- + + - hosts: node + vars: + kernel_ok_to_reboot: true + kernel_params: + - default_hugepagesz: "1G" + - hugepagesz: "1G" + - hugepages: 3 + - intel_iommu: "on" + kernel_modules: + - load: vfio-pci + - load: vfio_iommu_type1 + options: ["allow_unsafe_interrupts=1"] # for virtio-net-pci devices + opennebula_repo_pre_enable: + AlmaLinux: + extra_rpms: + '10': [centos-release-nfv-openvswitch] + config_manager: + '10': [crb, epel, highavailability, centos-nfv-openvswitch] + RedHat: + subscription_manager: + '9': + - codeready-builder-for-rhel-9-x86_64-rpms + - rhel-9-for-x86_64-highavailability-rpms + - fast-datapath-for-rhel-9-x86_64-rpms + ovs: + set: + - other_config:dpdk-init: 'true' + - other_config:dpdk-socket-mem: '1024,0' + port: + ovsbr0: # "internal" port + set: + - tag: 123 + iface: + ovsbr0: # "internal" port + set: + - mtu_request: 1500 + dpdk-p0: + set: + - type: dpdkvhostuserclient + - mtu_request: 9000 + - options:vhost-server-path: /var/tmp/dpdk-p0 + dpdk-p1: + set: + - type: dpdk + - mtu_request: 9000 + - options:dpdk-devargs: '0000:02:00.0' + dpdk-p2: + set: + - type: dpdk + - mtu_request: 9000 + - options:dpdk-devargs: '0000:03:00.0' + eth3: {} # non-DPDK device + bond: + bond0: + ifaces: [dpdk-p1, dpdk-p2] + set: + - bond_mode: active-backup + br: + ovsbr0: + ports: [dpdk-p0, bond0] + set: + - datapath_type: netdev + addrs: + - cidr: "{{ ansible_default_ipv4.address ~ '/' ~ ansible_default_ipv4.prefix }}" + metric: 400 + gw: "{{ ansible_default_ipv4.gateway }}" + dns: ["{{ ansible_default_ipv4.gateway }}"] + ovsbr1: # non-DPDK bridge + ports: [eth3] + roles: + - role: opennebula.deploy.helper.facts + - role: opennebula.deploy.helper.kernel + - role: opennebula.deploy.repository + - role: opennebula.deploy.openvswitch + +License +------- + +Apache-2.0 + +Author Information +------------------ + +[OpenNebula Systems](https://opennebula.io/) diff --git a/roles/openvswitch/defaults/main.yml b/roles/openvswitch/defaults/main.yml new file mode 100644 index 00000000..b629fdc9 --- /dev/null +++ b/roles/openvswitch/defaults/main.yml @@ -0,0 +1,42 @@ +--- +ovs_defaults: + iface: {} + bond: {} + br: {} + port: {} + +ovs_packages: + AlmaLinux: + - ethtool + - iproute + - iputils + - openvswitch3.5 + Debian: + - ethtool + - iproute2 + - iputils-arping + - openvswitch-switch + RedHat: + - ethtool + - iproute + - iputils + - openvswitch3.6 + +ovs_packages_dpdk: + AlmaLinux: + - dpdk-tools + - ethtool + - iproute + - iputils + - openvswitch3.5 + Debian: + - ethtool + - iproute2 + - iputils-arping + - openvswitch-switch-dpdk + RedHat: + - dpdk-tools + - ethtool + - iproute + - iputils + - openvswitch3.6 diff --git a/roles/openvswitch/meta/main.yml b/roles/openvswitch/meta/main.yml new file mode 100644 index 00000000..e233e79d --- /dev/null +++ b/roles/openvswitch/meta/main.yml @@ -0,0 +1,6 @@ +--- +collections: + - opennebula.deploy + +dependencies: + - role: opennebula.deploy.common diff --git a/roles/openvswitch/tasks/main.yml b/roles/openvswitch/tasks/main.yml new file mode 100644 index 00000000..68855aa1 --- /dev/null +++ b/roles/openvswitch/tasks/main.yml @@ -0,0 +1,270 @@ +--- +- name: Compute facts (OVS) + ansible.builtin.set_fact: + ovs: "{{ ovs_defaults | combine(ovs | d({}), recursive=true) }}" + +- when: ((ovs.iface | count) + (ovs.bond | count) + (ovs.br | count)) > 0 + vars: + # helpers + _pci_addr_regex: >- + ^[0-9a-fA-F]{4}[:][0-9a-fA-F]{2}[:][0-9a-fA-F]{2}[.][0-9a-fA-F]{1,2}$ + # general + _dpdk_enabled: >- + {{ ovs.set | d([]) + | selectattr('other_config:dpdk-init', 'defined') + | selectattr('other_config:dpdk-init', 'in', ['true']) + | count > 0 }} + # iface + _dpdk_iface: >- + {%- set output = [] -%} + {%- for k, v in ovs.iface.items() -%} + {%- for u in v.set | d([]) | selectattr('type', 'defined') -%} + {%- if u.type.startswith('dpdk') -%} + {{- output.append([k, v]) -}} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- dict(output) -}} + _dpdk_pci_addrs_raw: >- + {%- set output = [] -%} + {%- for k, v in _dpdk_iface.items() -%} + {%- for u in v.set | selectattr('options:dpdk-devargs', 'defined') -%} + {%- if u['options:dpdk-devargs'] | string | regex_search(_pci_addr_regex) -%} + {{- output.append(u['options:dpdk-devargs']) -}} + {%- else -%} + {{- output.append(None) -}} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- output -}} + _dpdk_pci_addrs: >- + {{ _dpdk_pci_addrs_raw | select | unique }} + _internal_iface: >- + {{ ovs.iface | dict2items + | selectattr('key', 'in', ovs.br.keys()) + | items2dict }} + _iface: >- + {{ ovs.iface | dict2items + | rejectattr('key', 'in', _dpdk_iface.keys()) + | rejectattr('key', 'in', _internal_iface.keys()) + | items2dict }} + _pci_addrs: >- + {{ command_udevadm_pci_addresses.stdout_lines | d([]) + | select + | map('regex_replace', '^pci-', '') }} + # bond + _dpdk_bond: >- + {{ ovs.bond | dict2items + | selectattr('value.ifaces', 'defined') + | selectattr('value.ifaces', 'subset', _dpdk_iface.keys()) + | items2dict }} + _bond: >- + {{ ovs.bond | dict2items + | selectattr('value.ifaces', 'defined') + | selectattr('value.ifaces', 'subset', _iface.keys()) + | items2dict }} + _bond_ifaces: >- + {{ ovs.bond | dict2items + | selectattr('value.ifaces', 'defined') + | map(attribute='value.ifaces') + | flatten }} + # br + _dpdk_br: >- + {%- set output = [] -%} + {%- for k, v in ovs.br.items() -%} + {%- for u in v.set | d([]) | selectattr('datapath_type', 'defined') -%} + {%- if u.datapath_type in ['netdev'] -%} + {{- output.append([k, v]) -}} + {%- endif -%} + {%- endfor -%} + {%- endfor -%} + {{- dict(output) -}} + _br: >- + {{ ovs.br | dict2items + | rejectattr('key', 'in', _dpdk_br.keys()) + | items2dict }} + _br_ports: >- + {{ ovs.br | dict2items + | selectattr('value.ports', 'defined') + | map(attribute='value.ports') + | flatten }} + block: + - name: Assert DPDK is enabled when DPDK resources are defined + ansible.builtin.assert: + that: (_dpdk_iface | count == 0) + or + (_dpdk_enabled is true) + fail_msg: Please enable DPDK (define other_config:dpdk-init) or remove DPDK resources from OVS config. + + - name: Assert all provided DPDK PCI addresses are valid and unique + ansible.builtin.assert: + that: _dpdk_pci_addrs_raw == _dpdk_pci_addrs # no invalid, no duplicated + fail_msg: Please remove invalid / duplicated DPDK PCI addresses from OVS config. + + - name: Query lspci if DPDK PCI addresses do exist + ansible.builtin.shell: + cmd: | + set -o errexit + {% for v in _dpdk_pci_addrs %} + STDOUT="$(lspci -vmm -nkD -s '{{ v }}')" + if [[ -n "$STDOUT" ]]; then + echo "$STDOUT" + echo + else + echo "Could not find '{{ v }}'" >&2 + exit 1 + fi + {% endfor %} + executable: /bin/bash + changed_when: false + + - name: Query udev for PCI addresses + ansible.builtin.command: + cmd: "udevadm info --query=property --property=ID_PATH --value {{ _paths | join(' ') }}" + when: _paths | count > 0 + vars: + _paths: >- + {{ _iface.keys() | map('regex_replace', '^(.*)$', "-p '/sys/class/net/\g<1>'") }} + register: command_udevadm_pci_addresses + changed_when: false + + - name: Assert DPDK and non-DPDK devices do not share PCI addresses + ansible.builtin.assert: + that: _dpdk_pci_addrs | intersect(_pci_addrs) | count == 0 + fail_msg: Please remove conflicting (PCI address) DPDK vs non-DPDK device definitions from OVS config. + + - name: Assert all bond 'ifaces' are unique subset of ovs.iface devices + ansible.builtin.assert: + that: + - _bond_ifaces == _bond_ifaces | unique # no duplicated + - _bond_ifaces is subset(ovs.iface.keys()) + fail_msg: Please ensure bond 'ifaces' are declared in ovs.iface and uniquely distributed among bond resources. + + - name: Assert each bond resource contains at least 2 'ifaces' + ansible.builtin.assert: + that: + - 0 not in _ifaces_counts + - 1 not in _ifaces_counts + fail_msg: Please ensure each bond resource contains at least 2 'ifaces'. + vars: + _ifaces_counts: >- + {{ ovs.bond | dict2items + | selectattr('value.ifaces', 'defined') + | map(attribute='value.ifaces') + | map('count') + | unique }} + + - name: Assert each bond 'ifaces' are not of mixed DPDK / non-DPDK types + ansible.builtin.assert: + that: + - _bond_keys == _bond_keys | unique # there are no mixed 'ifaces' + - _bond_keys == _all_bond_keys # every bond resource has some valid-ish 'ifaces' + fail_msg: Please ensure each declared bond resource has 'ifaces' of either DPDK or non-DPDK types (never both). + vars: + _bond_keys: >- + {{ _dpdk_bond.keys() | list + _bond.keys() | list }} + _all_bond_keys: >- + {{ ovs.bond.keys() }} + + - name: Assert ovs.port is a subset of available ports (including internal) + ansible.builtin.assert: + that: + - ovs.port.keys() is subset(ovs.br.keys() | list + _br_ports) + fail_msg: Please ensure ovs.port is a subset of available ports. + + - name: Assert br 'ports' are unique subset of ovs.bond + ovs.iface resources + ansible.builtin.assert: + that: + - _br_ports == _br_ports | unique # no duplicated + - _br_ports is subset(ovs.bond.keys() | list + ovs.iface.keys() | list) + fail_msg: Please ensure br 'ports' are declared in ovs.bond and ovs.iface and uniquely distributed among br resources. + + - name: Assert each non-DPDK br 'ports' do not contain DPDK resources + ansible.builtin.assert: + that: _keys | intersect(_dpdk_keys) | count == 0 + fail_msg: Please ensure no DPDK resource is inserted into non-DPDK br resources. + vars: + _keys: >- + {{ _br | dict2items + | map(attribute='value.ports') + | flatten }} + _dpdk_keys: >- + {{ _dpdk_bond.keys() | list + _dpdk_iface.keys() | list }} + + - name: Assert addrs/gw/dns are not declared for br 'ports' and bond 'ifaces' + ansible.builtin.assert: + that: _matched | count == 0 + fail_msg: Please remove addrs/gw/dns attributes from br 'ports' and bond 'ifaces'. + vars: + _keys: >- + {{ (_bond_ifaces + _br_ports) | difference(ovs.bond) }} + _matched: >- + {{ _keys | map('extract', ovs.iface) + | map('dict2items') + | flatten + | selectattr('key', 'in', ['addrs', 'gw', 'dns']) }} + + - name: Install OVS packages + ansible.builtin.package: + name: "{{ _specific[ansible_distribution] | d(_specific[ansible_os_family]) }}" + vars: + _specific: "{{ _dpdk_enabled | ternary(ovs_packages_dpdk, ovs_packages) }}" + register: package + until: package is success + retries: 12 + delay: 5 + + - when: + - ansible_os_family in ['Debian'] + - _dpdk_enabled is true + block: + - name: Use DPDK version of ovs-vswitchd + community.general.alternatives: + name: ovs-vswitchd + path: /usr/lib/openvswitch-switch-dpdk/ovs-vswitchd-dpdk + register: alternatives_ovs_vswitchd + + - name: Enable / (Re)Start OVS (NOW) + ansible.builtin.systemd_service: + name: "{{ _specific[ansible_os_family] }}" + state: >- + {{ 'restarted' if _changed else 'started' }} + enabled: true + vars: + _specific: + Debian: openvswitch-switch.service + RedHat: openvswitch.service + _changed: >- + {{ (alternatives_ovs_vswitchd is defined) + and + (alternatives_ovs_vswitchd is changed) }} + + - name: Install OVS-related scripts + ansible.builtin.template: + src: "{{ item.src }}" + dest: "{{ item.dest }}" + mode: "{{ item.mode }}" + owner: 0 + group: 0 + loop: + - src: opennebula-ovs.sh.jinja + dest: /usr/local/sbin/opennebula-ovs.sh + mode: u=rwx,go=rx + - src: opennebula-ovs.service.jinja + dest: /etc/systemd/system/opennebula-ovs.service + mode: u=rw,go=r + register: template + + - name: Switch to OVS networking + ansible.builtin.systemd_service: + daemon_reload: "{{ item.daemon_reload | d(omit) }}" + name: "{{ item.name | d(omit) }}" + state: "{{ item.state | d(omit) }}" + enabled: "{{ item.enabled | d(omit) }}" + loop: + - daemon_reload: "{{ _changed }}" + - name: opennebula-ovs.service + state: "{{ 'restarted' if _changed else 'started' }}" + enabled: true + vars: + _changed: "{{ template is changed }}" diff --git a/roles/openvswitch/templates/opennebula-ovs.service.jinja b/roles/openvswitch/templates/opennebula-ovs.service.jinja new file mode 100644 index 00000000..c931d683 --- /dev/null +++ b/roles/openvswitch/templates/opennebula-ovs.service.jinja @@ -0,0 +1,30 @@ +[Unit] +Description=OVS Bridge Interface Network configuration +{% if ansible_os_family == 'Debian' %} +After=openvswitch-switch.service network-pre.target +Wants=network-pre.target +Before=network.target +Requires=openvswitch-switch.service +{% endif %} +{% if ansible_os_family == 'RedHat' %} +After=openvswitch.service network-pre.target +Wants=network-pre.target +Before=network.target +Requires=openvswitch.service +{% endif %} + +[Service] +Type=oneshot +RemainAfterExit=yes +EnvironmentFile= +ExecStart=/usr/local/sbin/opennebula-ovs.sh +TimeoutStartSec=120 +StandardOutput=journal +StandardError=journal +# Ensure the service runs in a clean environment +Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +# Network operations require root +User=root + +[Install] +WantedBy=multi-user.target diff --git a/roles/openvswitch/templates/opennebula-ovs.sh.jinja b/roles/openvswitch/templates/opennebula-ovs.sh.jinja new file mode 100644 index 00000000..bdb2f0e7 --- /dev/null +++ b/roles/openvswitch/templates/opennebula-ovs.sh.jinja @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +set -o errexit -o pipefail + +# --- Helper functions + +log() { echo "$*" >&2; } +die() { log "ERROR: $*"; exit 1; } + +# --- Basic assertions + +log 'Asserting required binaries and scripts are available' +type -p arping find ip jq ovs-vsctl &>/dev/null +{% if _dpdk_enabled %} +type -p dpdk-devbind.py ethtool &>/dev/null +{% endif %} + +{% for iface in _iface %} +# {{ iface }} +log 'Asserting {{ iface }} exists and is not used outside OVS' +ip --json link show dev '{{ iface }}' | jq -re '.[0].master as $m | [null, "ovs-system"] | any(. == $m)' &>/dev/null +{% endfor %} + +# --- OVS / DPDK initialization + +{% for addr in _dpdk_pci_addrs %} +# {{ addr }} +log 'Calling dpdk-devbind.py on {{ addr }}' +dpdk-devbind.py --force --bind=vfio-pci '{{ addr }}' || die 'Failed to bind PCI device {{ addr }}' +{% endfor %} + +{% for v in ovs.set | d([]) | map('dict2items') | flatten %} +log 'Setting {{ v.key }}={{ v.value }}' +ovs-vsctl --no-wait set Open_vSwitch . '{{ v.key }}={{ v.value }}' || die 'Failed to set {{ v.key }}={{ v.value }}' +{% endfor %} + +# --- Bridge / Port cleanup + +declare -A _BOND_IFACES=({{ _bond_ifaces | map('regex_replace', '^(.*)$', '[\g<1>]=1') | join(' ') }}) + +ovs-vsctl --no-headings --columns=name find Interface 'external_ids:one-deploy-bond!=""' | while read -r IFACE; do + if [[ -n "$IFACE" ]] && [[ ! -v _BOND_IFACES[$IFACE] ]]; then + log "Deleting leftover iface $IFACE" + ovs-vsctl --with-iface del-port "$IFACE" || die "Failed to delete iface $IFACE" + fi +done + +declare -A _BR_PORTS=({{ _br_ports | map('regex_replace', '^(.*)$', '[\g<1>]=1') | join(' ') }}) + +ovs-vsctl --no-headings --columns=name find Port 'external_ids:one-deploy-bond=""' | while read -r PORT; do + if [[ -n "$PORT" ]] && [[ ! -v _BR_PORTS[$PORT] ]]; then + log "Deleting leftover port $PORT" + ovs-vsctl del-port "$PORT" || die "Failed to delete port $PORT" + fi +done + +declare -A _BR=({{ ovs.br.keys() | map('regex_replace', '^(.*)$', '[\g<1>]=1') | join(' ') }}) + +ovs-vsctl --no-headings --columns=name find Bridge 'external_ids:one-deploy-br!=""' | while read -r BR; do + if [[ -n "$BR" ]] && [[ ! -v _BR[$BR] ]]; then + log "Deleting leftover br $BR" + ovs-vsctl del-br "$BR" || die "Failed to delete br $BR" + fi +done + +# --- Networking cleanup + +if type -p systemctl &>/dev/null && systemctl is-active --quiet NetworkManager; then + log 'Stopping and disabling NetworkManager' + systemctl disable NetworkManager || log 'WARNING: Failed to disable NetworkManager' + systemctl stop NetworkManager || log 'WARNING: Failed to stop NetworkManager' +fi + +if type -p netplan &>/dev/null && netplan status -f json | jq -re '."netplan-global-state"."online"' &>/dev/null; then + log 'Disabling Netplan' + find /etc/netplan/ -type f -name '*.yaml' -exec mv {} {}.bak \; + netplan apply || log 'WARNING: Failed to disable Netplan' +fi + +# --- Bridge creation + +{% for br in ovs.br %} +# {{ br }} +log 'Creating OVS bridge {{ br }}' +ovs-vsctl --may-exist add-br '{{ br }}' \ + -- set Bridge '{{ br }}' 'external_ids:one-deploy-br={{ br }}' \ +{% for v in ovs.br[br].set | d([]) | map('dict2items') | flatten %} + -- set Bridge '{{ br }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} + || die 'Failed to configure bridge {{ br }}' +{% endfor %} + +# --- Port creation + +{% for br in ovs.br %} +# {{ br }} +{% for port in ovs.br[br].ports | select('in', ovs.iface) %} +log 'Adding port {{ port }} to {{ br }}' +ovs-vsctl --may-exist add-port '{{ br }}' '{{ port }}' \ + 'external_ids:one-deploy-br={{ br }}' \ + 'external_ids:one-deploy-bond=""' \ + -- set Interface '{{ port }}' 'external_ids:one-deploy-br={{ br }}' \ + -- set Interface '{{ port }}' 'external_ids:one-deploy-bond=""' \ +{% for v in ovs.iface[port].set | d([]) | map('dict2items') | flatten %} + -- set Interface '{{ port }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} + || die 'Failed to add port {{ port }}' +{% endfor %} +{% endfor %} + +# --- Bond creation + +{% for br in ovs.br %} +# {{ br }} +{% for port in ovs.br[br].ports | select('in', ovs.bond) %} +log 'Adding bond {{ port }} to {{ br }}' +ovs-vsctl --if-exists del-port '{{ port }}' -- add-bond '{{ br }}' '{{ port }}' \ +{% for iface in ovs.bond[port].ifaces %} + '{{ iface }}' \ +{% endfor %} + 'external_ids:one-deploy-br={{ br }}' \ + 'external_ids:one-deploy-bond={{ port }}' \ +{% for v in ovs.bond[port].set | d([]) | map('dict2items') | flatten %} + '{{ v.key }}={{ v.value }}' \ +{% endfor %} +{% for iface in ovs.bond[port].ifaces %} + -- set Interface '{{ iface }}' 'external_ids:one-deploy-br={{ br }}' \ + -- set Interface '{{ iface }}' 'external_ids:one-deploy-bond={{ port }}' \ +{% for v in ovs.iface[iface].set | d([]) | map('dict2items') | flatten %} + -- set Interface '{{ iface }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} +{% endfor %} + || die 'Failed to add bond {{ port }}' +{% endfor %} +{% endfor %} + +# --- Bridge / Port / Interface reconfiguration + +{% for br in ovs.br | dict2items | selectattr('value.set', 'defined') | map(attribute='key') %} +# {{ br }} +log 'Reconfiguring OVS bridge {{ br }}' +ovs-vsctl \ +{% for v in ovs.br[br].set | d([]) | map('dict2items') | flatten %} + -- --if-exists set Bridge '{{ br }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} + || die 'Failed to reconfigure {{ br }}' +{% endfor %} + +{% for port in ovs.port | dict2items | selectattr('value.set', 'defined') | map(attribute='key') %} +# {{ port }} +log 'Reconfiguring port {{ port }}' +ovs-vsctl \ +{% for v in ovs.port[port].set | d([]) | map('dict2items') | flatten %} + -- --if-exists set Port '{{ port }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} + || die 'Failed to reconfigure {{ port }}' +{% endfor %} + +{% for iface in ovs.iface | dict2items | selectattr('value.set', 'defined') | map(attribute='key') %} +# {{ iface }} +log 'Reconfiguring interface {{ iface }}' +ovs-vsctl \ +{% for v in ovs.iface[iface].set | d([]) | map('dict2items') | flatten %} + -- --if-exists set Interface '{{ iface }}' '{{ v.key }}={{ v.value }}' \ +{% endfor %} + || die 'Failed to reconfigure {{ iface }}' +{% endfor %} + +# --- Checksum offload (kernel) + +{% for br in _dpdk_br %} +log 'Disabling checksum offloading for {{ br }}' +ethtool --offload '{{ br }}' tx off rx off || log 'WARNING: Failed to disable checksum offloading for {{ br }}' +{% endfor %} + +# --- IP configuration + +{% for dev, v in (_iface.items() | list + ovs.br.items() | list) %} +# {{ dev }} +log 'Flushing {{ dev }}' +ip addr flush dev '{{ dev }}' || die 'Failed to flush {{ dev }}' +{% for a in v.addrs | d([]) %} +log 'Adding IP address {{ a.cidr }} to {{ dev }}' +ip addr add '{{ a.cidr }}' dev '{{ dev }}' \ + {{ "metric '{}'".format(a.metric) if a.metric is defined else "" }} || die 'Failed to add IP address to {{ dev }}' +{% endfor %} +log 'Bringing up {{ dev }}' +ip link set dev '{{ dev }}' up || die 'Failed to bring up {{ dev }}' +{% endfor %} + +# --- Default route + +{% set gw = ((ovs.iface.values() | list + ovs.br.values() | list) | selectattr('gw', 'defined') | first).gw | d(None) %} +{% if gw is not none %} +log 'Replacing default route to via {{ gw }}' +ip route replace default via '{{ gw }}' || die 'Failed to replace default route {{ gw }}' +{% endif %} + +# --- Nameserver configuration + +if type -p systemctl resolvectl &>/dev/null && systemctl is-active --quiet systemd-resolved; then +{% for dev, v in (ovs.iface.items() | list + ovs.br.items() | list) %} + # {{ dev }} +{% if v.dns is defined and v.dns is sequence %} + log 'Setting nameservers for {{ dev }}' + resolvectl dns '{{ dev }}' \ + {{ v.dns | map('regex_replace', '^(.*)$', "'\g<1>'") | join(' ') }} \ + || die 'Failed to setup nameservers' +{% endif %} +{% endfor %} +fi + +# --- Networking refresh + +{% for br, v in ovs.br.items() %} +# {{ br }} +{% for a in v.addrs | d([]) %} +log 'Sending gratuitous ARP for {{ a.cidr }} on {{ br }}' +arping -c 3 -A -I '{{ br }}' '{{ a.cidr.split('/') | first }}' || log 'WARNING: arping failed' +{% endfor %} +{% endfor %} + +# --- + +log 'OVS network configuration completed successfully' +exit 0