#!/bin/bash # Open vSwitch fabric for labsim — the "switch" the whole sim hangs off. # # Why OVS and not a Linux bridge: a Linux bridge cannot do LACP at all, and its # VLAN support is awkward to drive from libvirt. OVS gives real 802.1Q access # and trunk ports plus real LACP bonds, so a router VM can run the SAME bond0 + # vif config as the production VP2440s instead of an approximation. # # Layout: # ovs-labsim the switch # ├─ vm ports access ports, tag= (micro VM per VLAN) # ├─ hostv internal ports, tag= (host leg, for SSH) # └─ lag-vyos LACP bond, trunk of all VLANs (router under test) # shellcheck disable=SC2034 OVS_BR="${OVS_BR:-ovs-labsim}" OVS_NET="${OVS_NET:-labsim-ovs}" # libvirt network wrapping the bridge LAG_NAME="${LAG_NAME:-lag-vyos}" ovs() { sudo ovs-vsctl "$@"; } ovs_require() { command -v ovs-vsctl >/dev/null 2>&1 || die "openvswitch not installed (dnf install openvswitch)" systemctl is-active --quiet openvswitch || sudo systemctl start openvswitch \ || die "could not start openvswitch" } # All VLAN ids from the config, comma separated — used for trunk ports. vlan_id_list() { local ids=() for entry in "${SELECTED[@]}"; do ids+=("${entry%%:*}"); done (IFS=,; echo "${ids[*]}") } ovs_up() { ovs_require ovs --may-exist add-br "$OVS_BR" # Host leg per VLAN: an OVS internal port carrying that VLAN's tag, given the # .2 address. This is how you SSH to the VMs. It is deliberately NOT their # default route (.1 is), so inter-VLAN tests exercise the router, not the # host's routing table. for entry in "${SELECTED[@]}"; do parse_vlan_entry "$entry" local port="hostv${V_VID}" ovs --may-exist add-port "$OVS_BR" "$port" tag="$V_VID" \ -- set interface "$port" type=internal sudo ip link set "$port" up 2>/dev/null || true # Drop any address from a previous mask/octet so a changed vlans.conf does # not leave a stale second address on the port. sudo ip -4 addr flush dev "$port" 2>/dev/null || true sudo ip addr replace "${V_PREFIX}.${V_HOST}/${V_MASK}" dev "$port" done ovs_define_libvirt_net } # A libvirt network that hands out OVS ports: one portgroup per VLAN (access) # plus a trunk portgroup for the router. ovs_define_libvirt_net() { local pg="" ids for entry in "${SELECTED[@]}"; do IFS=: read -r vid name _p _r <<<"$entry" pg+=" " done # Trunk: VLAN 1 native/untagged, everything else tagged — the production # shape. libvirt expresses this declaratively via nativeMode='untagged' # (see libvirt formatnetwork.html), so it does not need fixing up by hand. # It also matters functionally: LACPDUs are untagged, and a trunk with no # native VLAN has nowhere to put them. local trunk=" " for entry in "${SELECTED[@]}"; do IFS=: read -r vid _n _p _r <<<"$entry" if [ "$vid" = "1" ]; then trunk+=" " else trunk+=" " fi done trunk+=" " local xml=" ${OVS_NET} ${pg}${trunk}" if virsh_q net-info "$OVS_NET" >/dev/null 2>&1; then virsh_q net-destroy "$OVS_NET" >/dev/null 2>&1 || true virsh_q net-undefine "$OVS_NET" >/dev/null 2>&1 || true fi echo "$xml" | virsh_q net-define /dev/stdin >/dev/null virsh_q net-start "$OVS_NET" >/dev/null log "libvirt network $OVS_NET bound to $OVS_BR (access portgroups + trunk)" } # Replace the router VM's two individual OVS ports with a single LACP bond. # libvirt attaches each NIC separately; only ovs-vsctl can bond them, and the # taps only exist once the VM is running — so this runs post-start. ovs_bond_router() { local vm="$1" local taps # NB: domiflist indents its rows, so anchor on the FIELD not the line — # /^vnet/ silently matches nothing and the bond never gets built. taps="$(virsh_q domiflist "$vm" 2>/dev/null | awk '$1 ~ /^vnet/ {print $1}')" local count; count="$(echo "$taps" | grep -c .)" [ "$count" -eq 2 ] || { warn "router $vm has $count tap(s), expected 2 — skipping bond"; return 1; } # Already bonded? (idempotent re-runs) if ovs list-ports "$OVS_BR" 2>/dev/null | grep -qx "$LAG_NAME"; then log "LACP bond $LAG_NAME already present" return 0 fi local t1 t2; t1="$(echo "$taps" | sed -n 1p)"; t2="$(echo "$taps" | sed -n 2p)" log "bonding $t1 + $t2 into $LAG_NAME (LACP active, balance-tcp)" ovs del-port "$OVS_BR" "$t1" 2>/dev/null || true ovs del-port "$OVS_BR" "$t2" 2>/dev/null || true # bond_mode=balance-tcp is REQUIRED: OVS defaults a bond to active-backup, # which does not speak LACP at all (confirmed on ovs-discuss). It is also the # equivalent of VyOS's 802.3ad + layer2+3 hashing. # # lacp-fallback-ab breaks a genuine deadlock: OVS keeps members disabled # until LACP negotiates, while the partner needs carrier before it will send # LACPDUs. Falling back to active-backup brings the links up so negotiation # can start. # # native-untagged + tag=1 carries the untagged LACPDUs and the management # VLAN, matching production. libvirt's portgroup VLAN config does NOT apply # here — the bond is a port libvirt never created — so set it inline. local tagged; tagged="$(vlan_id_list | tr ',' '\n' | grep -vx 1 | paste -sd, -)" ovs add-bond "$OVS_BR" "$LAG_NAME" "$t1" "$t2" \ lacp=active bond_mode=balance-tcp \ vlan_mode=native-untagged tag=1 trunks="$tagged" \ -- set port "$LAG_NAME" other_config:lacp-time=fast \ -- set port "$LAG_NAME" other_config:lacp-fallback-ab=true } ovs_bond_status() { echo "--- ovs bond ---" sudo ovs-appctl bond/show "$LAG_NAME" 2>/dev/null | grep -E "bond_mode|lacp_status|^member|may_enable" || echo "(no bond)" echo "--- lacp ---" sudo ovs-appctl lacp/show "$LAG_NAME" 2>/dev/null | grep -E "status|aggregation key|^member|attached" || true } ovs_down() { virsh_q net-destroy "$OVS_NET" >/dev/null 2>&1 || true virsh_q net-undefine "$OVS_NET" >/dev/null 2>&1 || true if command -v ovs-vsctl >/dev/null 2>&1; then ovs --if-exists del-br "$OVS_BR" 2>/dev/null || true fi }