162 lines
6.1 KiB
Bash
162 lines
6.1 KiB
Bash
|
|
#!/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=<vlan> (micro VM per VLAN)
|
||
|
|
# ├─ hostv<vlan> internal ports, tag=<vlan> (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
|
||
|
|
IFS=: read -r vid _name prefix _real <<<"$entry"
|
||
|
|
local port="hostv${vid}"
|
||
|
|
ovs --may-exist add-port "$OVS_BR" "$port" tag="$vid" \
|
||
|
|
-- set interface "$port" type=internal
|
||
|
|
sudo ip link set "$port" up 2>/dev/null || true
|
||
|
|
sudo ip addr replace "${prefix}.2/24" 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+=" <portgroup name='vlan${vid}'>
|
||
|
|
<vlan><tag id='${vid}'/></vlan>
|
||
|
|
</portgroup>
|
||
|
|
"
|
||
|
|
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=" <portgroup name='trunk'>
|
||
|
|
<vlan trunk='yes'>
|
||
|
|
"
|
||
|
|
for entry in "${SELECTED[@]}"; do
|
||
|
|
IFS=: read -r vid _n _p _r <<<"$entry"
|
||
|
|
if [ "$vid" = "1" ]; then
|
||
|
|
trunk+=" <tag id='1' nativeMode='untagged'/>
|
||
|
|
"
|
||
|
|
else
|
||
|
|
trunk+=" <tag id='${vid}'/>
|
||
|
|
"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
trunk+=" </vlan>
|
||
|
|
</portgroup>
|
||
|
|
"
|
||
|
|
|
||
|
|
local xml="<network>
|
||
|
|
<name>${OVS_NET}</name>
|
||
|
|
<forward mode='bridge'/>
|
||
|
|
<bridge name='${OVS_BR}'/>
|
||
|
|
<virtualport type='openvswitch'/>
|
||
|
|
${pg}${trunk}</network>"
|
||
|
|
|
||
|
|
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
|
||
|
|
}
|