Cisco 9800-CL WLC HA SSO Deployment
Deploy redundant Cisco Catalyst 9800-CL virtual wireless controllers with SSO (Stateful Switchover) for zero-downtime failover. This maintains your CCNP ENWLSI skills while providing production-grade wireless infrastructure.
1. Executive Summary
| Item | Value |
|---|---|
Active WLC |
9800-WLC-01 (10.50.1.40) on kvm-01 |
Standby WLC |
9800-WLC-02 (10.50.1.41) on kvm-02 |
HA Mode |
SSO (Stateful Switchover) - 1+1 Redundancy |
Failover Time |
< 1 second (sub-second with SSO) |
CCNP Relevance |
ENWLSI 300-430 - WLC HA, RF Management, Security |
2. Architecture
2.1. SSO HA Topology
┌─────────────────────────────────────┐
│ Redundancy Port (RP) │
│ VLAN 100 - Port 9800 │
└─────────────────────────────────────┘
▲ ▲
│ │
┌───────────────┴───┐ ┌─────┴───────────────┐
│ 9800-WLC-01 │ │ 9800-WLC-02 │
│ 10.50.1.40 │ │ 10.50.1.41 │
│ kvm-01 │ │ kvm-02 │
│ │ │ │
│ ACTIVE │◄─────►│ HOT STANDBY │
│ (owns SSID/AP) │ SSO │ (ready takeover) │
│ │ sync │ │
│ 4 vCPU │ │ 4 vCPU │
│ 16GB RAM │ │ 16GB RAM │
└───────────────────┘ └───────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────┐
│ Management VLAN 100 │
│ 10.50.1.0/24 │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ APs │
│ Catalyst 9120AX │ Cisco OEAP │
│ CAPWAP to Active WLC (auto-failover) │
└─────────────────────────────────────────────────┘
2.2. What SSO Provides
| Feature | Without SSO | With SSO |
|---|---|---|
Failover Time |
2-5 minutes (AP re-join) |
< 1 second (stateful) |
Client Impact |
Full re-authentication |
Seamless (session preserved) |
AP Impact |
Full CAPWAP re-establishment |
Instant switchover |
Config Sync |
Manual (or Prime) |
Automatic real-time |
License |
Separate per WLC |
Shared (Network Advantage) |
3. Prerequisites
3.1. VM Resources
|
9800-CL requires 3 NICs for HA SSO. Single-NIC VMs cannot support HA - port 9800 (Redundancy Port) will never listen.
|
| Resource | WLC-01 | WLC-02 | Notes |
|---|---|---|---|
vCPU |
4 |
4 |
Minimum for production |
RAM |
16GB |
16GB |
Minimum for 1000 APs |
Disk |
16GB |
16GB |
IOS-XE + config |
vNIC1 (Gi1) |
br-mgmt (or VLAN 100 bridge) |
br-mgmt |
Service port - SSH/HTTPS management |
vNIC2 (Gi2) |
br-mgmt (or VLAN 100 bridge) |
br-mgmt |
WMI trunk - CAPWAP, AP management |
vNIC3 (Gi3) |
br-mgmt (or VLAN 100 bridge) |
br-mgmt |
HA Redundancy Port - port 9800 (MUST be same L2 segment as WLC-02/Gi3) |
|
Do NOT use Both WLCs' Gi3 interfaces MUST be on the same broadcast domain for link-local HA communication. |
3.2. HA Interface Options
There are three approaches for HA interface configuration, depending on your VM NIC count:
| Approach | Interface | IPs | Use When |
|---|---|---|---|
3-NIC (Dedicated HA) |
GigabitEthernet3 |
169.254.1.1/2 (link-local) |
Fresh 3-NIC VM deployment |
2-NIC (Gi2 HA) |
GigabitEthernet2 |
169.254.1.1/2 (link-local) |
VMs with only Gi1/Gi2 available |
1-NIC (Shared Mgmt) |
GigabitEthernet1 |
10.50.1.40/41 (management) |
Single-NIC VMs, simplest setup |
3.2.1. Option 1: 3-NIC with Gi3 (Recommended for New Deployments)
| WLC | Gi3 IP | Notes |
|---|---|---|
WLC-01 |
169.254.1.1/24 |
HA local IP |
WLC-02 |
169.254.1.2/24 |
HA remote IP |
3.2.2. Option 2: 2-NIC with Gi2 (Validated 2026-03-08)
If your VMs only have Gi1 and Gi2 (no Gi3), use Gi2 for HA:
| WLC | Gi2 IP | Notes |
|---|---|---|
WLC-01 |
169.254.1.1/24 |
HA local IP |
WLC-02 |
169.254.1.2/24 |
HA remote IP |
Commands:
On WLC-01:
chassis redundancy ha-interface GigabitEthernet 2 local-ip 169.254.1.1 /24 remote-ip 169.254.1.2
write memory
On WLC-02:
chassis redundancy ha-interface GigabitEthernet 2 local-ip 169.254.1.2 /24 remote-ip 169.254.1.1
write memory
3.2.3. Option 3: 1-NIC with Gi1 (Management IPs)
Uses existing management network for HA traffic:
| WLC | Gi1 IP | Notes |
|---|---|---|
WLC-01 |
10.50.1.40/24 |
Same as management |
WLC-02 |
10.50.1.41/24 |
Same as management |
|
Link-local 169.254.x.x is recommended by Cisco. Both HA interfaces must be on the same L2 segment (same VLAN across kvm-01 and kvm-02). |
3.3. Network Requirements
| Requirement | Value | Purpose |
|---|---|---|
RP Port |
TCP/UDP 9800 |
Redundancy heartbeat + sync |
Management |
VLAN 100 |
AP CAPWAP, admin access |
Latency |
< 80ms RTT |
Between Active/Standby |
MTU |
1500 (or jumbo) |
RP communication |
3.4. Licensing
SSO requires Network Advantage tier on BOTH controllers.
# Verify license on each WLC
show license summary
If no licenses show, enable Network Advantage:
configure terminal
license air level air-network-advantage
end
write memory
reload
After reload, verify:
show license summary
License Usage: License Entitlement Tag Count Status ----------------------------------------------------------------------------- air-network-advantage (AIR-DNA-A) 1 IN USE
|
Home Enterprise Licensing:
|
4. Phase 0: Configure kvm-02 br-mgmt VLAN Trunking
|
kvm-02’s |
4.1. Linux Bridge = Virtual Switch (Cisco Mental Model)
Understanding Linux bridges from a Cisco switch perspective:
┌─────────────────────────────────────────────────────────────────────────────┐
│ CISCO SWITCH vs LINUX BRIDGE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ CISCO SWITCH LINUX BRIDGE (br-mgmt) │
│ ═══════════ ══════════════════════ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Gi1/0/1 │ ◄─── trunk ───► │ eno8 │ Physical uplink │
│ │ (uplink) │ │ (uplink) │ to physical switch │
│ └─────────────┘ └─────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │
│ │ VLAN DATABASE │ │ BRIDGE VLAN TABLE │ │
│ │ vlan 1,10,20,30,40,100, │ │ vid 1,10,20,30,40,100, │ │
│ │ 110,120 │ │ 110,120 │ │
│ └─────────────────────────────┘ └─────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Gi1/0/2 │ ◄─── access ───► │ vnet0 │ VM interface │
│ │ (host) │ │ (WLC-02) │ (virtual NIC) │
│ └─────────────┘ └─────────────┘ │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ CISCO COMMANDS LINUX COMMANDS │
│ ══════════════ ══════════════ │
│ │
│ vlan 10,20,30,40,100,110,120 bridge vlan add vid 10 dev br-mgmt │
│ │
│ interface Gi1/0/1 # For physical uplink: │
│ switchport mode trunk bridge vlan add vid 10 dev eno8 │
│ switchport trunk allowed vlan │
│ 1,10,20,30,40,100,110,120 │
│ │
│ interface Gi1/0/2 # For VM interface: │
│ switchport mode trunk bridge vlan add vid 10 dev vnet0 │
│ switchport trunk allowed vlan │
│ 1,10,20,30,40,100,110,120 │
│ │
│ vtp mode transparent vlan_filtering = 0 (OFF) │
│ (VLANs pass but not filtered) (all traffic passes unfiltered) │
│ │
│ vtp mode server vlan_filtering = 1 (ON) │
│ (VLANs enforced) (only configured VLANs pass) │
│ │
├─────────────────────────────────────────────────────────────────────────────┤
│ DANGER ZONE (what locked you out) │
│ ═════════════════════════════════ │
│ │
│ CISCO: LINUX: │
│ switchport trunk allowed vlan 10 ip link set br-mgmt type bridge │
│ (REMOVES all other VLANs!) vlan_filtering 1 │
│ (without adding VLANs first!) │
│ │
│ FIX: switchport trunk allowed FIX: Add VLANs BEFORE enabling │
│ vlan add 1,20,30... vlan_filtering │
└─────────────────────────────────────────────────────────────────────────────┘
4.2. VLAN Configuration Flow
CORRECT ORDER (what we're doing)
═════════════════════════════════
Step 1: Add VLANs to bridge Step 2: Add VLANs to uplink
┌─────────────────────────┐ ┌─────────────────────────┐
│ bridge vlan add vid X │ │ bridge vlan add vid X │
│ dev br-mgmt self │ ──► │ dev eno8 │
│ │ │ │
│ "self" = the bridge │ │ (no self) = the port │
│ itself, like VLAN DB │ │ like trunk allowed │
└─────────────────────────┘ └─────────────────────────┘
│ │
▼ ▼
Step 3: Verify before enabling Step 4: Enable filtering
┌─────────────────────────┐ ┌─────────────────────────┐
│ bridge vlan show │ │ ip link set br-mgmt │
│ │ ──► │ type bridge │
│ Confirm ALL VLANs │ │ vlan_filtering 1 │
│ appear on br-mgmt │ │ │
│ AND eno8 │ │ NOW SAFE because VLANs │
└─────────────────────────┘ │ are already configured │
└─────────────────────────┘
4.3. 0.1 Verify Current State
ssh kvm-02.inside.domusdigitalis.dev
cat /sys/class/net/br-mgmt/bridge/vlan_filtering
0
bridge vlan show dev br-mgmt
port vlan-id br-mgmt 1 PVID Egress Untagged
4.4. 0.2 Find Physical Interface
bridge link show master br-mgmt | awk '/state forwarding/ && !/vnet/ {print $2}' | tr -d ':'
# Set variable for remaining commands
PHYS_IF="eno8" # Replace with actual output from above
echo "Physical interface: $PHYS_IF"
echo "PHYS_IF is set to: $PHYS_IF"
4.5. 0.3 Add VLANs to Bridge (BEFORE enabling filtering)
|
ADD VLANs FIRST, then enable filtering. Enabling filtering without VLANs configured will drop ALL traffic and lock you out. This is like running |
# Add VLAN 100 as PVID (native) - matches WLC trunk native VLAN 100
sudo bridge vlan add vid 100 dev br-mgmt self pvid untagged
# Add tagged VLANs for WLC and infrastructure trunking
# VLAN names MUST match switch/WLC for ISE policy consistency
# Client VLANs
sudo bridge vlan add vid 10 dev br-mgmt self # DATA_VLAN
sudo bridge vlan add vid 20 dev br-mgmt self # VOICE_VLAN
sudo bridge vlan add vid 30 dev br-mgmt self # GUEST_VLAN
sudo bridge vlan add vid 40 dev br-mgmt self # IOT_VLAN
# Infrastructure VLANs
sudo bridge vlan add vid 110 dev br-mgmt self # SECURITY_VLAN (ISE, Vault)
sudo bridge vlan add vid 120 dev br-mgmt self # SERVICES_VLAN (k3s)
# Special VLANs
sudo bridge vlan add vid 666 dev br-mgmt self # NATIVE_VLAN_UNUSED
sudo bridge vlan add vid 999 dev br-mgmt self # CRITICAL_AUTH_VLAN
bridge vlan show dev br-mgmt
port vlan-id
br-mgmt 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
4.6. 0.4 Add VLANs to Physical Interface (Uplink)
# VLAN 100 as PVID (native) - matches WLC trunk native VLAN 100
sudo bridge vlan add vid 100 dev $PHYS_IF pvid untagged
# Client VLANs (names match switch for ISE policy)
sudo bridge vlan add vid 10 dev $PHYS_IF # DATA_VLAN
sudo bridge vlan add vid 20 dev $PHYS_IF # VOICE_VLAN
sudo bridge vlan add vid 30 dev $PHYS_IF # GUEST_VLAN
sudo bridge vlan add vid 40 dev $PHYS_IF # IOT_VLAN
# Infrastructure VLANs
sudo bridge vlan add vid 110 dev $PHYS_IF # SECURITY_VLAN
sudo bridge vlan add vid 120 dev $PHYS_IF # SERVICES_VLAN
# Special VLANs
sudo bridge vlan add vid 666 dev $PHYS_IF # NATIVE_VLAN_UNUSED
sudo bridge vlan add vid 999 dev $PHYS_IF # CRITICAL_AUTH_VLAN
bridge vlan show dev $PHYS_IF
port vlan-id
eno8 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
4.7. 0.5 Verify ALL VLANs Configured (Pre-Flight Check)
bridge vlan show
port vlan-id
eno8 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
br-mgmt 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
|
DO NOT proceed to 0.6 unless BOTH br-mgmt AND eno8 show all VLANs. Missing VLANs = lockout when filtering is enabled. |
4.8. 0.6 Enable VLAN Filtering (NOW safe)
sudo ip link set br-mgmt type bridge vlan_filtering 1
cat /sys/class/net/br-mgmt/bridge/vlan_filtering
1
# Use VyOS gateway (VRRP VIP 10.50.1.1 pending Phase 15 of VyOS deployment)
ping -c2 10.50.1.3
64 bytes from 10.50.1.3: icmp_seq=1 ttl=64 time=0.3 ms 64 bytes from 10.50.1.3: icmp_seq=2 ttl=64 time=0.2 ms
4.9. 0.7 Make Persistent (NetworkManager)
The bridge vlan commands are runtime only. NetworkManager persists them across reboots.
sudo nmcli connection modify br-mgmt bridge.vlan-filtering yes
sudo nmcli connection modify br-mgmt bridge.vlans "100 pvid untagged, 10, 20, 30, 40, 110, 120, 666, 999"
nmcli connection show | grep -i eno8
br-mgmt-port ebd955da-b7e8-495e-b0f1-3c7f852a75d8 ethernet eno8
# Add VLANs to the port connection (use actual name from above)
sudo nmcli connection modify "br-mgmt-port" bridge-port.vlans "100 pvid untagged, 10, 20, 30, 40, 110, 120, 666, 999"
nmcli connection show br-mgmt | grep -i vlan
bridge.vlan-filtering: yes bridge.vlans: 100 pvid untagged, 10, 20, 30, 40, 110, 120, 666, 999
nmcli connection show br-mgmt-port | grep -i vlan
bridge-port.vlans: 100 pvid untagged, 10, 20, 30, 40, 110, 120, 666, 999
4.10. 0.8 Final Verification
cat /sys/class/net/br-mgmt/bridge/vlan_filtering
1
bridge vlan show | awk '/br-mgmt|eno8/ {found=1} found && /^[a-z]/ && !/br-mgmt|eno8/ {found=0} found'
eno8 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
br-mgmt 10
20
30
40
100 PVID Egress Untagged
110
120
666
999
4.11. Phase 0 Complete
kvm-02’s br-mgmt bridge is now a VLAN-aware trunk, equivalent to:
interface GigabitEthernet1/0/1
description kvm-02-uplink
switchport mode trunk
switchport trunk native vlan 100
switchport trunk allowed vlan 10,20,30,40,100,110,120,666,999
WLC-02 VMs connected to br-mgmt can now trunk all configured VLANs.
|
VLAN Names for ISE Policy Consistency: | VLAN ID | Name | Purpose | |---------|------|---------| | 10 | DATA_VLAN | Corporate data | | 20 | VOICE_VLAN | VoIP phones | | 30 | GUEST_VLAN | Guest wireless | | 40 | IOT_VLAN | IoT devices | | 100 | MGMT_VLAN | Management/Infrastructure | | 110 | SECURITY_VLAN | ISE, Vault | | 120 | SERVICES_VLAN | k3s services | | 666 | NATIVE_VLAN_UNUSED | Unused native (security) | | 999 | CRITICAL_AUTH_VLAN | 802.1X auth failure | These names MUST match on switch, WLC, and ISE authorization profiles. |
5. Upgrade Existing Single-NIC WLCs to 3-NIC
If you have existing single-NIC WLC VMs, add the required NICs for HA SSO.
5.1. Prerequisites
-
WLC VM is shut down
-
Existing qcow2 disk preserves all config (hostname, IPs, VLANs)
-
Bridge must support VLAN trunking
5.2. Add NICs to WLC-02 (kvm-02)
# SSH to kvm-02
ssh kvm-02
# Verify VM is running
sudo virsh list --all | grep -i wlc
# Shutdown WLC-02
sudo virsh shutdown 9800-WLC-02
# Wait for shutdown (check status)
sudo virsh list --all | grep -i wlc
# Add vNIC2 (Gi2 - WMI trunk)
sudo virsh attach-interface 9800-WLC-02 --type bridge --source br-mgmt --model virtio --persistent
# Add vNIC3 (Gi3 - HA Redundancy Port)
sudo virsh attach-interface 9800-WLC-02 --type bridge --source br-mgmt --model virtio --persistent
# Verify 3 NICs attached
sudo virsh domiflist 9800-WLC-02
Interface Type Source Model MAC --------------------------------------------------------- vnetX bridge br-mgmt virtio 52:54:00:xx:xx:xx vnetY bridge br-mgmt virtio 52:54:00:yy:yy:yy vnetZ bridge br-mgmt virtio 52:54:00:zz:zz:zz
# Start WLC-02
sudo virsh start 9800-WLC-02
5.3. Add NICs to WLC-01 (kvm-01)
Gi3 (HA port) MUST be on the same L2 segment as WLC-02/Gi3.
If WLC-02/Gi3 is on br-mgmt (VLAN 100), then WLC-01/Gi3 must ALSO be on a bridge connected to VLAN 100.
virbr0 is libvirt’s NAT bridge (192.168.122.x) - it has NO L2 connectivity to br-mgmt. Using virbr0 for Gi3 will cause HA to fail with "Communications = Down".
First, check what bridges exist on kvm-01:
ip link show type bridge
Use whichever bridge provides L2 connectivity to VLAN 100 (typically br-mgmt or similar).
# SSH to kvm-01
ssh kvm-01
# Check existing bridge for WLC-01 Gi1 (the working management interface)
sudo virsh domiflist 9800-WLC-01
Use the SAME bridge for Gi2 and Gi3 to ensure L2 connectivity.
# Shutdown WLC-01
sudo virsh shutdown 9800-WLC-01
# Add vNIC2 (Gi2 - WMI trunk)
# Replace BRIDGE with actual bridge name (e.g., br-mgmt, NOT virbr0)
sudo virsh attach-interface 9800-WLC-01 --type bridge --source BRIDGE --model virtio --persistent
# Add vNIC3 (Gi3 - HA Redundancy Port)
# CRITICAL: Must be same L2 segment as WLC-02/Gi3
sudo virsh attach-interface 9800-WLC-01 --type bridge --source BRIDGE --model virtio --persistent
# Verify 3 NICs attached
sudo virsh domiflist 9800-WLC-01
# Start WLC-01
sudo virsh start 9800-WLC-01
5.4. Configure IOS-XE Interfaces
After adding NICs, configure the new interfaces on each WLC.
5.4.1. On WLC-02
! Verify all 3 interfaces exist
show ip interface brief | include Gig
GigabitEthernet1 10.50.1.41 YES NVRAM up up GigabitEthernet2 unassigned YES unset down down GigabitEthernet3 unassigned YES unset down down
|
Do NOT use On 9800 WLCs, the HA interface is configured via |
configure terminal
!
interface GigabitEthernet3
description HA-REDUNDANCY-PORT
no shutdown
!
end
write memory
6. Phase 1: Deploy 9800-WLC-02 VM
|
All commands use FULL PATHS. Do NOT use |
6.1. 1.1 Download C9800-CL OVA (from workstation)
# From Cisco software download (CCO account required)
# Product: Catalyst 9800-CL Cloud Wireless Controller
# Version: 17.15.x (match WLC-01 or upgrade both)
# File: C9800-CL-universalk9.17.15.04d.ova
6.2. 1.2 Copy OVA to kvm-02 NAS Storage
# SCP to home first (avoid permission issues)
scp ~/Downloads/C9800-CL-universalk9.17.15.04d.ova kvm-02:~/
# Then move to NAS (requires sudo for NAS write)
ssh kvm-02.inside.domusdigitalis.dev "sudo mv ~/C9800-CL-universalk9.17.15.04d.ova /mnt/nas/isos/"
ssh kvm-02.inside.domusdigitalis.dev "ls -lh /mnt/nas/isos/*.ova"
6.4. 1.4 Extract OVA
sudo tar -xvf /mnt/nas/isos/C9800-CL-universalk9.17.15.04d.ova -C /mnt/nas/isos/
C9800-CL-universalk9_vga.17.15.04d.ovf C9800-CL-universalk9_vga.17.15.04d.mf vwlc_harddisk.vmdk README-OVF.txt C9800-CL-universalk9_vga.17.15.04d.iso
ls -lh /mnt/nas/isos/vwlc_harddisk.vmdk
6.5. 1.5 Create Empty Disk for Installation
|
The VMDK in the OVA is a STUB file (~2.6MB), NOT a real disk image. The actual 9800-CL image is on the ISO (~1.6GB). You must:
This is like how you install Windows - boot from ISO, it installs to the blank hard drive. |
# Verify the VMDK is just a stub (should be ~2.6MB, not 8GB)
ls -lh /mnt/nas/isos/vwlc_harddisk.vmdk
-rw-r--r-- 1 root root 2.6M Mar 4 22:15 /mnt/nas/isos/vwlc_harddisk.vmdk
# Create EMPTY 16GB disk for IOS-XE installation
sudo qemu-img create -f qcow2 /mnt/nas/vms/9800-WLC-02.qcow2 16G
Formatting '/mnt/nas/vms/9800-WLC-02.qcow2', fmt=qcow2 cluster_size=65536 extended_l2=off compression_type=zlib size=17179869184 lazy_refcounts=off refcount_bits=16
ls -lh /mnt/nas/vms/9800-WLC-02.qcow2
-rw-r--r-- 1 root root 193K Mar 4 22:30 /mnt/nas/vms/9800-WLC-02.qcow2
6.6. 1.6 Fix NFS Permissions for libvirt (REQUIRED)
|
libvirt runs as uid:107 (qemu user), not root. NFS mounts typically don’t allow the qemu user to traverse directories. Without this fix, virt-install fails with: ERROR Cannot access storage file '/mnt/nas/...' (as uid:107, gid:107): Permission denied This is like a user needing |
# Grant others read+execute on ALL NFS paths used by VMs
# setfacl doesn't work on NFS ("Operation not supported"), use chmod instead
sudo chmod o+rx /mnt/nas
sudo chmod o+rx /mnt/nas/vms
sudo chmod o+rx /mnt/nas/isos
# Grant qemu user ownership of the disk file
sudo chown qemu:qemu /mnt/nas/vms/9800-WLC-02.qcow2
Path traversal requires execute (x) permission on EVERY directory:
/mnt ← qemu needs x here
└── nas ← qemu needs x here
├── vms ← qemu needs x here (for disk images)
│ └── 9800-WLC-02.qcow2 ← qemu needs r here
└── isos ← qemu needs x here (for ISO/CDROM)
└── C9800-CL-...iso ← qemu needs r here
NFS default: root creates dirs → mode 755 → others have rx ✓
NFS gotcha: sometimes restricted → mode 750 → others blocked ✗
setfacl fails on NFS because NFS doesn't support POSIX ACLs.
chmod o+rx is the portable fix.
|
Run this ONCE on kvm-02 after NAS mount. It persists across reboots (stored on NAS, not local). |
6.7. 1.7 Create VM (Boot from ISO)
sudo virt-install \
--name 9800-WLC-02 \
--memory 16384 \
--vcpus 4 \
--disk path=/mnt/nas/vms/9800-WLC-02.qcow2,format=qcow2 \
--cdrom /mnt/nas/isos/C9800-CL-universalk9_vga.17.15.04d.iso \
--network bridge=br-mgmt,model=virtio \
--os-variant generic \
--graphics vnc,listen=0.0.0.0 \
--boot cdrom,hd \
--noautoconsole
|
Key options explained:
|
Starting install... Domain is still running. Installation may be in progress.
sudo virsh list --all | grep -i wlc
- 9800-WLC-02 running
6.8. 1.7.1 Convert to Serial Console (Preferred)
Remove VNC graphics and use serial console for installation:
sudo virsh dumpxml 9800-WLC-02 > /tmp/wlc02.xml
# Remove VNC graphics section with sed
sed -i '/<graphics type=.vnc/,/<\/graphics>/d' /tmp/wlc02.xml
# Verify graphics removed
grep -c graphics /tmp/wlc02.xml
0
# Redefine and restart VM
sudo virsh define /tmp/wlc02.xml
sudo virsh destroy 9800-WLC-02
sudo virsh start 9800-WLC-02
# Connect via serial console
sudo virsh console 9800-WLC-02
Press Enter to wake console. You should see the GRUB boot menu.
Ctrl + ]
6.9. 1.7.2 VNC Console (Alternative)
If serial console doesn’t work, use VNC:
sudo virsh vncdisplay 9800-WLC-02
:1
This means VNC port 5901. Connect with: vncviewer kvm-02:5901
6.10. 1.7.3 IOS-XE Installation from ISO
At the GRUB boot menu, select vWLC - GOLDEN IMAGE and press Enter.
+----------------------------------------------------------------------------+
| vWLC - GOLDEN IMAGE |
| |
+----------------------------------------------------------------------------+
Use the UP and DOWN arrow keys to select which entry is highlighted.
The installer will:
-
Boot the IOS-XE kernel
-
Format the virtual disk
-
Install IOS-XE to disk (~5-10 minutes)
-
Reboot automatically
Installing IOS-XE... Formatting disk... Copying files... Installation complete. Rebooting...
After reboot, you’ll see the IOS-XE initial configuration dialog:
--- System Configuration Dialog --- Would you like to enter the initial configuration dialog? [yes/no]:
Type no and press Enter - we’ll configure manually.
6.11. 1.7.4 Configure vnet Interface PVID (CRITICAL)
|
libvirt creates vnet10 with PVID 1 by default. The WLC sends management traffic untagged (native VLAN 100), but without PVID 100 on vnet10, the bridge tags it as VLAN 1 and drops it. This is the #1 cause of "WLC boots but no connectivity" issues. |
# Find the vnet interface for WLC-02
sudo virsh domiflist 9800-WLC-02 | awk '/br-mgmt/ {print $1}'
vnet10
# Check current VLAN config (likely shows PVID 1)
bridge vlan show dev vnet10
port vlan-id
vnet10 1 PVID Egress Untagged
100
# Set PVID to 100 (matches WLC native VLAN)
sudo bridge vlan del vid 1 dev vnet10 pvid untagged
sudo bridge vlan add vid 100 dev vnet10 pvid untagged
# Verify PVID is now 100
bridge vlan show dev vnet10
port vlan-id vnet10 100 PVID Egress Untagged
# Test connectivity
ping -c2 10.50.1.41
|
Why PVID matters:
This is equivalent to setting |
6.12. 1.8 Remove CDROM After Installation
After IOS-XE installs and the WLC boots normally, remove the CDROM so it boots from disk:
# Check current CDROM attachment
sudo virsh domblklist 9800-WLC-02 | grep -i cdrom
# Eject the ISO
sudo virsh change-media 9800-WLC-02 sda --eject
# Update boot order to disk only (persistent)
sudo virsh dumpxml 9800-WLC-02 > /tmp/wlc02.xml
# Edit XML: Change boot order from "cdrom,hd" to just "hd"
# Or use virt-manager GUI
# Apply changes
sudo virsh define /tmp/wlc02.xml
|
You can also use |
6.13. 1.9 Initial Configuration (Console)
Connect via VNC console (serial doesn’t work with VGA ISO) and configure:
|
Gateway: Use VyOS (10.50.1.2 or 10.50.1.3) until VRRP VIP (10.50.1.1) is configured in Phase 15 of VyOS deployment. For WLC-02 on kvm-02, use 10.50.1.3. |
! Basic setup
hostname 9800-WLC-02
!
! Create ALL VLANs with consistent names (matches switch/ISE)
vlan 10
name DATA_VLAN
vlan 20
name VOICE_VLAN
vlan 30
name GUEST_VLAN
vlan 40
name IOT_VLAN
vlan 100
name MGMT_VLAN
vlan 110
name SECURITY_VLAN
vlan 120
name SERVICES_VLAN
vlan 666
name NATIVE_VLAN_UNUSED
vlan 999
name CRITICAL_AUTH_VLAN
!
! Management SVI on VLAN 100
interface Vlan100
ip address 10.50.1.41 255.255.255.0
no shutdown
!
! Trunk interface - native VLAN 100, all VLANs allowed
interface GigabitEthernet1
switchport mode trunk
switchport trunk native vlan 100
switchport trunk allowed vlan 10,20,30,40,100,110,120,666,999
no shutdown
!
! Gateway - VyOS (VRRP VIP pending)
ip route 0.0.0.0 0.0.0.0 10.50.1.3
ip name-server 10.50.1.90
ip name-server 10.50.1.91
ip domain name inside.domusdigitalis.dev
!
! Enable SSH
crypto key generate rsa modulus 4096
ip ssh version 2
!
line vty 0 15
login local
transport input ssh
!
! Admin user (get password from gopass)
username admin privilege 15 secret <password>
!
! Save
write memory
|
Why trunk with native VLAN 100?
|
7. Phase 2: Configure SSO on Active (WLC-01)
7.1. 2.1 Enable Redundancy
! On 9800-WLC-01 (Active)
configure terminal
!
redundancy
mode sso
exit
!
! Set chassis priority (higher = preferred active)
chassis 1 priority 200
!
! Configure HA interface with local and remote IPs
! WARNING: Do not use management IP - use dedicated HA subnet or same subnet with different IPs
chassis redundancy ha-interface GigabitEthernet 1 local-ip 10.50.1.40 /24 remote-ip 10.50.1.41
!
end
write memory
|
17.15.x Syntax Changes:
|
7.2. 2.2 Verify RP Configuration
show redundancy
Expected output (before standby joins):
Redundant System Information :
Available system uptime = 45 days, 3 hours, 22 minutes
Switchovers system experienced = 0
Standby failures = 0
Last switchover reason = none
Hardware Mode = Simplex
Configured Redundancy Mode = sso
Operating Redundancy Mode = Non-redundant
8. Phase 3: Configure SSO on Standby (WLC-02)
8.1. 3.1 Enable Redundancy
! On 9800-WLC-02 (Standby)
configure terminal
!
redundancy
mode sso
exit
!
! Set chassis priority (lower = standby)
chassis 1 priority 100
!
! Configure HA interface with local and remote IPs
chassis redundancy ha-interface GigabitEthernet 1 local-ip 10.50.1.41 /24 remote-ip 10.50.1.40
!
end
write memory
8.2. 3.2 Verify Redundancy Pair
# On WLC-01 (Active)
show redundancy
Expected output (after sync):
Redundant System Information :
Available system uptime = 45 days, 3 hours, 30 minutes
Switchovers system experienced = 0
Standby failures = 0
Last switchover reason = none
Hardware Mode = Duplex
Configured Redundancy Mode = sso
Operating Redundancy Mode = sso
Maintenance Mode = Disabled
RMI-state = ACTIVE
Auto-sync = Enabled
Current Processor Information :
----------------------------------------------
Active Location = slot 1
Current Software state = ACTIVE
Uptime in current state = 45 days, 3 hours, 30 minutes
Peer Processor Information :
----------------------------------------------
Standby Location = slot 2
Current Software state = STANDBY HOT
Uptime in current state = 0 days, 0 hours, 8 minutes
8.3. 3.3 Verify Sync Status
show redundancy states
Expected:
Unit = Primary
Redundancy Mode (Operational) = sso
Redundancy Mode (Configured) = sso
Redundancy State = ACTIVE
Manual Swact = enabled
Communications = Up
Other Side (Standby) Not Ready or Not Present
^^^^^ This becomes "STANDBY HOT" when synced
9. Phase 4: Validation
9.1. 4.1 Show Commands
# Redundancy summary
show redundancy
# Detailed HA status
show chassis redundancy ha-intf summary
# Config sync status
show redundancy config-sync failures
# AP status (should show connected to Active)
show ap summary
10. Phase 5: ISE Integration
11. Troubleshooting
11.1. RP Communication Issues
# Check RP connectivity
ping 10.50.1.41 source 10.50.1.40
# Check RP port
show chassis redundancy ha-intf summary
# Debug (use sparingly)
debug redundancy ios
11.2. Sync Failures
# Check sync errors
show redundancy config-sync failures
# Force re-sync
redundancy config-sync bulk
11.3. Standby Not Coming Up
Common causes: 1. Version mismatch - Both WLCs must run IDENTICAL IOS-XE version 2. License mismatch - Both need Network Advantage 3. MTU issues - Ensure jumbo frames or 1500 MTU on RP path 4. Firewall blocking - Port 9800 TCP/UDP must be open
# Check version
show version | include Version
# Check license
show license summary
12. CCNP Wireless Study Notes
12.1. Exam Topics Covered (ENWLSI 300-430)
| Topic | Relevance |
|---|---|
2.1 HA Design |
SSO vs N+1, RPO/RTO, AP groups |
2.2 Mobility |
Same-WLC roaming (intra) vs inter-WLC |
3.1 RF |
RRM, DCA, TPC - managed by Active WLC |
4.1 Security |
802.1X auth happens on Active only |
5.1 Troubleshooting |
Show commands, debug redundancy |
12.2. Key Concepts
- SSO (Stateful Switchover)
-
Both WLCs maintain synchronized state. Failover is sub-second because standby already has all client/AP state.
- N+1 Redundancy
-
Alternative to SSO. Multiple WLCs in a mobility group, APs failover to any available WLC. Longer failover (2-5 min).
- Redundancy Port (RP)
-
Dedicated or shared interface for heartbeat and state sync between Active/Standby.
- Chassis Priority
-
Higher value = preferred Active. WLC-01 (200) preferred over WLC-02 (100).
- Preemption
-
If enabled, higher-priority WLC takes over when it recovers. Default: disabled.
13. Appendix A: Session Variables
# WLC-01 (Active)
WLC01_HOSTNAME="9800-WLC-01"
WLC01_IP="10.50.1.40"
WLC01_PRIORITY=200
# WLC-02 (Standby)
WLC02_HOSTNAME="9800-WLC-02"
WLC02_IP="10.50.1.41"
WLC02_PRIORITY=100
# RP Configuration
RP_PORT=9800
RP_VLAN=100
14. Appendix B: VLAN Reference (ISE Policy Consistency)
|
VLAN names MUST match across all devices for ISE authorization profiles to work correctly. ISE uses VLAN names in authorization profiles (e.g., |
| VLAN ID | Name | ISE Policy Use | Purpose |
|---|---|---|---|
10 |
DATA_VLAN |
Corporate users (EAP-TLS) |
Authenticated employee traffic |
20 |
VOICE_VLAN |
VoIP phones |
802.1p QoS marking |
30 |
GUEST_VLAN |
CWA/WebAuth |
Captive portal redirect |
40 |
IOT_VLAN |
MAB/iPSK |
IoT devices, printers |
100 |
MGMT_VLAN |
Infrastructure |
WLC management, AP CAPWAP |
110 |
SECURITY_VLAN |
ISE, Vault (mTLS) |
Security infrastructure |
120 |
SERVICES_VLAN |
k3s workloads |
Container services |
666 |
NATIVE_VLAN_UNUSED |
Security (unused) |
Prevent VLAN hopping |
999 |
CRITICAL_AUTH_VLAN |
Auth failure |
802.1X critical VLAN |
14.1. WLC VLAN Configuration (Add After HA Setup)
After SSO is established, add wireless client VLANs to both WLCs:
! Create VLANs with CONSISTENT names
vlan 10
name DATA_VLAN
vlan 20
name VOICE_VLAN
vlan 30
name GUEST_VLAN
vlan 40
name IOT_VLAN
vlan 110
name SECURITY_VLAN
vlan 120
name SERVICES_VLAN
vlan 666
name NATIVE_VLAN_UNUSED
vlan 999
name CRITICAL_AUTH_VLAN
!
! Add to trunk
interface GigabitEthernet1
switchport trunk allowed vlan add 10,20,30,40,110,120,666,999
!
! Create wireless interfaces (dynamic interfaces)
wireless interface DATA_VLAN vlan 10
wireless interface VOICE_VLAN vlan 20
wireless interface GUEST_VLAN vlan 30
wireless interface IOT_VLAN vlan 40
15. Appendix C: Quick Reference
| Command | Purpose |
|---|---|
|
Overall HA status |
|
Detailed state machine |
|
RP interface status |
|
Sync error log |
|
Manual failover (Active→Standby) |
|
Force full config sync |
|
Verify APs connected post-failover |
16. Appendix D: Troubleshooting
16.1. WLC-01 Gi1 Not Detected (kvm-01/virbr0)
Symptom: WLC-01 shows only Gi2, not Gi1. Or neither Gi1 nor Gi2 after NIC removal.
Root Cause: IOS-XE maps PCI slots to GigabitEthernet interfaces. Two NICs on virbr0 (untagged bridge) causes detection issues.
Fix: Recreate VM with single NIC using existing qcow2:
# 1. Shutdown and destroy old VM definition
sudo virsh shutdown 9800-WLC-01
sudo virsh destroy 9800-WLC-01 2>/dev/null
sudo virsh undefine 9800-WLC-01
# 2. Recreate with single NIC (preserves existing qcow2 config)
sudo virt-install \
--name 9800-WLC-01 \
--memory 16384 \
--vcpus 4 \
--import \
--disk path=/mnt/onboard-ssd/vms/C9800-CL-universalk9.17.15.03.qcow2,format=qcow2 \
--network bridge=virbr0,model=virtio \
--os-variant generic \
--graphics vnc \
--noautoconsole
# 3. Verify single NIC attached
sudo virsh domiflist 9800-WLC-01
Interface Type Source Model MAC
-------------------------------------------------------
vnet0 bridge virbr0 virtio 52:54:00:xx:xx:xx
# 4. Start and verify Gi1 in WLC
sudo virsh start 9800-WLC-01
# Wait 2-3 minutes for IOS-XE boot
ssh admin@10.50.1.40 "show ip int brief | include Gi"
GigabitEthernet1 10.50.1.40 YES NVRAM up up
Key Learning (2026-03-05):
-
kvm-01/virbr0 = untagged bridge, no VLAN filtering → single NIC required
-
kvm-02/br-mgmt = also requires single NIC (despite VLAN-aware bridge)
-
--importpreserves existing qcow2 config (IP, VLANs, hostname) -
Renaming:
virsh domrename <old> <new>(VM must be shutdown)
16.2. WLC-02 Gi1 Not Detected (kvm-02/br-mgmt)
Symptom: WLC-02 shows only Gi2 or no interfaces. Cannot SSH to 10.50.1.41 but ping works from kvm-02.
Root Cause: Same as WLC-01 - IOS-XE PCI slot mapping issue affects both bridges.
Fix: Recreate VM with single NIC using existing qcow2:
# 1. Destroy old VM definition
sudo virsh destroy 9800-WLC-02
sudo virsh undefine 9800-WLC-02
# 2. Recreate with single NIC (preserves existing qcow2 config)
sudo virt-install \
--name 9800-WLC-02 \
--memory 16384 \
--vcpus 4 \
--import \
--disk path=/mnt/nas/vms/9800-WLC-02.qcow2,format=qcow2 \
--network bridge=br-mgmt,model=virtio \
--os-variant generic \
--graphics vnc \
--noautoconsole
# 3. Verify single NIC attached
sudo virsh domiflist 9800-WLC-02
Interface Type Source Model MAC
-------------------------------------------------------
vnet0 bridge br-mgmt virtio 52:54:00:xx:xx:xx
# 4. Verify Gi1 in WLC (after 2-3 min boot)
ssh admin@10.50.1.41 "show ip int brief | include Gi"
GigabitEthernet1 10.50.1.41 YES NVRAM up up
Key Learning (2026-03-06): br-mgmt (VLAN-aware) also requires single NIC for 9800-CL.
16.3. Corrupted Disk Recovery (No Bootable Media)
Symptom: Console shows no bootable media or similar BIOS error. VM won’t boot.
Diagnosis:
# Check if disk is locked (VM may still be "running" in bad state)
sudo qemu-img check /mnt/nas/vms/9800-WLC-02.qcow2
qemu-img: Could not open '/mnt/nas/vms/9800-WLC-02.qcow2': Failed to get "write" lock Is another process using the image [/mnt/nas/vms/9800-WLC-02.qcow2]?
# VM stuck in running state
sudo virsh list --all | grep -i wlc
- 9800-WLC-02 running
Root Cause: NAS I/O failure, storage corruption, or ISO installation failure corrupted the qcow2 filesystem.
Recovery Procedure:
# Step 1: Force destroy the stuck VM
sudo virsh destroy 9800-WLC-02
# Step 2: Undefine the VM (removes libvirt config, keeps disk)
sudo virsh undefine 9800-WLC-02
# Step 3: Delete corrupted disk
sudo rm /mnt/nas/vms/9800-WLC-02.qcow2
# Step 4: Create fresh empty disk (16G minimum)
sudo qemu-img create -f qcow2 /mnt/nas/vms/9800-WLC-02.qcow2 16G
# Step 5: Fix permissions for libvirt
sudo chown qemu:qemu /mnt/nas/vms/9800-WLC-02.qcow2
# Step 6: Redeploy from ISO (boots from CDROM, installs to empty disk)
sudo virt-install \
--name 9800-WLC-02 \
--memory 16384 \
--vcpus 4 \
--disk path=/mnt/nas/vms/9800-WLC-02.qcow2,format=qcow2 \
--cdrom /mnt/nas/isos/C9800-CL-universalk9_vga.17.15.04d.iso \
--network bridge=br-mgmt,model=virtio \
--os-variant generic \
--graphics vnc,listen=0.0.0.0 \
--boot cdrom,hd \
--noautoconsole
# Step 7: Connect via Cockpit VNC (PREFERRED)
# Open browser: https://kvm-02:9090 → Virtual Machines → 9800-WLC-02 → Console
sudo virsh vncdisplay 9800-WLC-02
# Then: vncviewer kvm-02:590X (where X is display number)
At GRUB menu, select vWLC - GOLDEN IMAGE. Wait for installation (~5-10 min).
After reboot, complete initial configuration per Section 1.9.
# Step 8: Configure vnet PVID (CRITICAL - WLC won't ping without this)
VNET=$(sudo virsh domiflist 9800-WLC-02 | awk '/br-mgmt/ {print $1}')
sudo bridge vlan del vid 1 dev $VNET pvid untagged
sudo bridge vlan add vid 100 dev $VNET pvid untagged
# Step 9: Verify connectivity
ping -c2 10.50.1.41
Key Learning (2026-03-05):
-
NAS I/O issues during bridge config changes can corrupt VM disks
-
qcow2 "write lock" error + "no bootable media" = disk is toast, redeploy
-
ISO installation requires empty disk (VMDK in OVA is a stub)
-
PVID 100 on vnet is required for management traffic
17. Troubleshooting HA SSO
17.1. HA Communications Down, Reason: Failure
Symptom:
WLC-01#show redundancy
...
Communications = Down Reason: Failure
Peer (slot: 0) information is not available because it is in 'DISABLED' state
And show ip interface brief shows Gi3 as "unassigned":
GigabitEthernet3 unassigned YES unset up up
Root Cause: Gi3 on WLC-01 and Gi3 on WLC-02 are on different L2 segments.
Link-local addresses (169.254.x.x) are non-routable. They require direct Layer 2 adjacency - both WLCs' Gi3 interfaces MUST be on the same broadcast domain.
17.1.1. Verify Bridge Assignments
On kvm-01:
sudo virsh domiflist 9800-WLC-01
On kvm-02:
sudo virsh domiflist 9800-WLC-02
PROBLEM: If Gi3 bridges don’t match (e.g., virbr0 vs br-mgmt), HA cannot form.
| Bridge | Network | L2 Connectivity |
|---|---|---|
|
192.168.122.0/24 (NAT, isolated) |
Only within same host |
|
VLAN 100 (10.50.1.0/24) |
Spans physical network |
17.1.2. Fix: Move Gi3 to Common Bridge
Both WLCs' Gi3 must be on a bridge that provides L2 connectivity. Since br-mgmt spans the physical network via VLAN 100, use it for both.
Step 1: Check what bridge kvm-01 has for VLAN 100
# On kvm-01
bridge link show | awk '{print $2}'
ip link show type bridge
If kvm-01 has br-mgmt or similar, use that. If not, you may need to create one or use the physical interface with VLAN tagging.
Step 2: Shut down WLC-01
ssh kvm-01
sudo virsh shutdown 9800-WLC-01
Step 3: Identify and detach the wrong Gi3 vnet
# List current interfaces
sudo virsh domiflist 9800-WLC-01
Note the MAC address of the third interface (Gi3).
# Detach the wrong interface (replace MAC with actual)
sudo virsh detach-interface 9800-WLC-01 bridge --mac 52:54:00:xx:xx:xx --persistent
Step 4: Attach Gi3 to correct bridge
# Attach to br-mgmt (or whatever bridge provides L2 to VLAN 100)
sudo virsh attach-interface 9800-WLC-01 --type bridge --source br-mgmt --model virtio --persistent
Step 5: Start WLC-01 and verify
sudo virsh start 9800-WLC-01
sudo virsh domiflist 9800-WLC-01
All 3 NICs should now show the same bridge (or bridges with L2 connectivity).
Step 6: Set PVID on new vnet
# Get the new vnet name for Gi3
VNET=$(sudo virsh domiflist 9800-WLC-01 | awk 'NR==4 {print $1}')
echo "Gi3 vnet: $VNET"
# Set PVID 100 (CRITICAL for untagged traffic)
sudo bridge vlan del vid 1 dev $VNET pvid untagged
sudo bridge vlan add vid 100 dev $VNET pvid untagged
# Verify
bridge vlan show dev $VNET
17.2. Gi3 Shows "unassigned" After chassis redundancy Command
Symptom:
WLC-01#show ip interface brief
GigabitEthernet3 unassigned YES unset up up
Even after running:
chassis redundancy ha-interface GigabitEthernet 3 local-ip 169.254.1.1 /24 remote-ip 169.254.1.2
Root Cause: The chassis redundancy ha-interface command doesn’t show the IP in show ip interface brief. This is normal behavior.
Verify HA interface status instead:
show chassis redundancy ha-intf summary
The HA interface is managed by the redundancy subsystem, not the standard IP stack.
17.3. ip address Command Fails on Gi3
Symptom:
WLC-01(config-if)#ip address 169.254.1.1 255.255.255.0
% Invalid input detected at '^' marker.
Root Cause: On 9800 WLCs, Gi3 (HA Redundancy Port) cannot be configured with ip address in interface config mode.
Fix: Use the chassis redundancy ha-interface command in global config:
chassis redundancy ha-interface GigabitEthernet 3 local-ip 169.254.1.1 /24 remote-ip 169.254.1.2
write memory
17.4. HA Still Won’t Form After All Fixes
If L2 connectivity is confirmed (pings work) but HA still shows DISABLED:
-
Reload both WLCs simultaneously - Per Cisco docs, both controllers must be reloaded at the same time for HA to form:
! On both WLCs at the same time reload -
Check version mismatch - Both WLCs must run the exact same IOS-XE version:
show version | include IOSXEIf versions differ (e.g., 17.15.3 vs 17.15.4d), HA will not form. Upgrade the older WLC.
-
Check chassis priority - In 17.15.x, priority values are 1-2 only (not 200/100):
show chassis redundancy
17.5. Key Learning (2026-03-06)
-
L2 adjacency is mandatory for Gi3 (HA port) - link-local IPs don’t route
-
virbr0(libvirt NAT) is isolated per host - NEVER use for HA -
ip addresscommand doesn’t work on Gi3 - usechassis redundancy ha-interface -
show ip interface briefshows Gi3 as "unassigned" - this is normal for HA ports -
Both WLCs must have identical IOS-XE versions for SSO
-
Reload both WLCs simultaneously after HA config for pairing to occur
18. Migrate WLC VM Between KVM Hosts
When WLCs are on different KVM hosts with incompatible bridges, temporarily migrate to the same host to establish HA, then migrate back.
18.1. Use Case
-
WLC-01 on kvm-01 with
virbr0(no L2 to VLAN 100) -
WLC-02 on kvm-02 with
br-mgmt(VLAN 100) -
Solution: Move WLC-01 to kvm-02, configure HA, then move back later
18.2. Step 1: Export VM Definition (Source Host)
ssh kvm-01
sudo virsh dumpxml 9800-WLC-01 > /tmp/9800-WLC-01.xml
18.3. Step 2: Check Disk Location
sudo virsh domblklist 9800-WLC-01
Target Source --------------------------------------------------------- sda /mnt/onboard-ssd/libvirt/images/9800-WLC-01.qcow2 sdb /var/lib/libvirt/images/C9800-CL-universalk9.17.15.03.iso
| If disk is on shared NFS (nas-01), skip the copy step - just update the path in XML. |
18.4. Step 3: Shut Down VM
sudo virsh shutdown 9800-WLC-01
# Wait for clean shutdown
watch -n2 'sudo virsh list --all | grep -i wlc'
18.5. Step 4: Copy Disk to Destination Host
# Copy qcow2 (skip if on shared NFS)
scp /mnt/onboard-ssd/libvirt/images/9800-WLC-01.qcow2 kvm-02:/mnt/onboard-ssd/libvirt/images/
# Copy XML definition
scp /tmp/9800-WLC-01.xml kvm-02:/tmp/
18.6. Step 5: Edit XML for Destination Host
ssh kvm-02
# Edit the XML
sudo vim /tmp/9800-WLC-01.xml
Changes required:
-
Bridge names - Change all
virbr0tobr-mgmt:<!-- BEFORE --> <source bridge='virbr0'/> <!-- AFTER --> <source bridge='br-mgmt'/> -
Disk path - Update to kvm-02 location:
<!-- BEFORE --> <source file='/mnt/onboard-ssd/libvirt/images/9800-WLC-01.qcow2'/> <!-- AFTER (if path differs) --> <source file='/mnt/onboard-ssd/libvirt/images/9800-WLC-01.qcow2'/> -
Remove UUID (optional) - Let libvirt generate new one:
<!-- Remove this line --> <uuid>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx</uuid>
Quick sed replacement:
# Replace all virbr0 with br-mgmt
sed -i 's/virbr0/br-mgmt/g' /tmp/9800-WLC-01.xml
# Verify changes
grep -E "bridge=|source file=" /tmp/9800-WLC-01.xml
18.7. Step 6: Define and Start on Destination Host
sudo virsh define /tmp/9800-WLC-01.xml
sudo virsh start 9800-WLC-01
18.8. Step 7: Set PVID on All vNets
# Set PVID 100 on all WLC-01 interfaces
for VNET in $(sudo virsh domiflist 9800-WLC-01 | awk 'NR>2 {print $1}'); do
echo "Setting PVID 100 on $VNET"
sudo bridge vlan del vid 1 dev $VNET pvid untagged 2>/dev/null
sudo bridge vlan add vid 100 dev $VNET pvid untagged
done
# Verify
for VNET in $(sudo virsh domiflist 9800-WLC-01 | awk 'NR>2 {print $1}'); do
echo "=== $VNET ==="
bridge vlan show dev $VNET
done
18.9. Step 8: Verify Both WLCs Running
sudo virsh list | grep -i wlc
Id Name State -------------------------------- XX 9800-WLC-01 running YY 9800-WLC-02 running
18.10. Step 9: Undefine on Source Host
After confirming WLC-01 works on kvm-02:
ssh kvm-01
# Remove the old definition (disk already copied)
sudo virsh undefine 9800-WLC-01
18.11. Step 10: Test HA Connectivity
Now both WLCs are on kvm-02 with br-mgmt. Test L2 connectivity:
On WLC-01:
ping 169.254.1.2
On WLC-02:
ping 169.254.1.1
If pings succeed, reload both WLCs simultaneously for HA to form.
18.12. Migrate Back to Original Host (Later)
After HA is working and you want to spread VMs across hosts:
-
Ensure kvm-01 has a bridge with L2 connectivity to VLAN 100
-
Create
br-mgmton kvm-01 if it doesn’t exist -
Follow the same migration procedure in reverse
-
Both WLCs must be on bridges with L2 adjacency for Gi3
19. Appendix E: Cross-KVM Platform Differences
|
kvm-01 (Arch Linux) and kvm-02 (Rocky Linux) use different QEMU/libvirt configurations. VM XML definitions exported from one host will NOT work on the other without modification. |
19.1. Platform Comparison Table
| Item | kvm-01 (Arch Linux) | kvm-02 (Rocky Linux 9) |
|---|---|---|
QEMU Emulator Path |
|
|
Machine Type |
|
|
virsh console |
Works (sometimes) |
Doesn’t work with WLC VMs |
Preferred Console |
Cockpit VNC + SSH |
Cockpit VNC + SSH |
IP Address |
10.50.1.99 |
10.50.1.111 |
19.2. XML Modifications Required (kvm-01 → kvm-02)
When moving a VM definition from kvm-01 to kvm-02, apply these sed transformations:
# 1. Fix emulator path
sed -i 's|/usr/bin/qemu-system-x86_64|/usr/libexec/qemu-kvm|' /tmp/vm.xml
# 2. Fix machine type
sed -i "s/machine='pc-i440fx-10.1'/machine='pc-i440fx-rhel7.6.0'/" /tmp/vm.xml
# 3. Update bridge names (if needed)
sed -i "s|source bridge='virbr0'|source bridge='br-mgmt'|g" /tmp/vm.xml
# 4. Update disk path (if on local storage)
sed -i "s|/mnt/onboard-ssd/vms/|/mnt/nas/vms/|g" /tmp/vm.xml
19.3. XML Modifications Required (kvm-02 → kvm-01)
When moving a VM definition from kvm-02 back to kvm-01:
# 1. Fix emulator path
sed -i 's|/usr/libexec/qemu-kvm|/usr/bin/qemu-system-x86_64|' /tmp/vm.xml
# 2. Fix machine type (check available types on kvm-01)
# On kvm-01: qemu-system-x86_64 -machine help | grep pc-i440fx
sed -i "s/machine='pc-i440fx-rhel7.6.0'/machine='pc-i440fx-10.1'/" /tmp/vm.xml
19.4. Finding Available Machine Types
Each QEMU installation supports different machine types. Check before migrating:
# On kvm-01 (Arch)
qemu-system-x86_64 -machine help | grep pc-i440fx | head -5
# On kvm-02 (Rocky)
/usr/libexec/qemu-kvm -machine help | grep pc-i440fx | head -5
19.5. Console Access for WLC VMs
|
Use Cockpit VNC instead:
|
19.6. Key Learnings (2026-03-06)
-
XML is NOT portable between distros - Different QEMU packaging = different paths
-
Machine types vary by QEMU version - RHEL/Rocky uses
rhel7.6.0suffix -
Always verify emulator path -
virsh definefails silently with wrong path, thenvirsh starterrors -
NAS storage helps - Disk on
/mnt/nas/vms/accessible from both hosts -
Document platform differences - Prevents future confusion during migrations
20. Appendix F: 3 NICs Attached But Only Gi1/Gi2 Detected
20.1. Symptom
virsh domiflist shows 3 NICs attached, but IOS-XE only detects Gi1 and Gi2:
sudo virsh domiflist 9800-WLC-01
Interface Type Source Model MAC ------------------------------------------------------- vnet0 bridge br-mgmt virtio 52:54:00:aa:aa:aa vnet1 bridge br-mgmt virtio 52:54:00:bb:bb:bb vnet2 bridge br-mgmt virtio 52:54:00:cc:cc:cc
But inside the WLC:
9800-WLC-01#show ip interface brief | include Gig
GigabitEthernet1 10.50.1.40 YES NVRAM up up
GigabitEthernet2 unassigned YES unset down down
No Gi3 appears despite 3 vNICs being attached.
20.2. Root Cause: PCI Slot Ordering Corruption
IOS-XE maps PCI slot numbers to GigabitEthernet interfaces:
-
PCI slot 0 → GigabitEthernet1
-
PCI slot 1 → GigabitEthernet2
-
PCI slot 2 → GigabitEthernet3
When you use virsh attach-interface and virsh detach-interface multiple times (during VM migration, troubleshooting, etc.), libvirt assigns non-sequential PCI slots. IOS-XE can’t handle gaps or out-of-order slot assignments.
Example of corrupted PCI slots:
<!-- What we want (sequential) -->
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0'/>
<!-- What we get after attach/detach cycles (gaps) -->
<address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/>
<address type='pci' domain='0x0000' bus='0x00' slot='0x08' function='0x0'/>
20.3. Fix: Recreate VM with virt-install
The only reliable fix is to recreate the VM definition with all 3 --network options specified upfront. This ensures clean, sequential PCI slot assignments.
|
The qcow2 disk is preserved - all config (hostname, IP, VLANs) remains intact. Only the VM definition (XML) is recreated. |
20.3.1. Step 1: Shutdown and Undefine
sudo virsh shutdown 9800-WLC-01
# Wait for clean shutdown
watch -n2 'sudo virsh list --all | grep -i wlc'
# Undefine removes XML, keeps disk
sudo virsh undefine 9800-WLC-01
20.3.2. Step 2: Recreate with 3 NICs
sudo virt-install \
--name 9800-WLC-01 \
--memory 16384 \
--vcpus 4 \
--import \
--disk path=/mnt/nas/vms/9800-WLC-01.qcow2,format=qcow2 \
--network bridge=br-mgmt,model=virtio \
--network bridge=br-mgmt,model=virtio \
--network bridge=br-mgmt,model=virtio \
--os-variant generic \
--noautoconsole
Key options:
-
--import- Use existing qcow2 (preserves all config) -
--networkx3 - Creates 3 NICs with proper PCI slot ordering -
No
--graphics- Default to none (use SSH after boot)
20.3.3. Step 3: Verify 3 NICs
sudo virsh domiflist 9800-WLC-01
Interface Type Source Model MAC ------------------------------------------------------- vnet0 bridge br-mgmt virtio 52:54:00:xx:xx:xx vnet1 bridge br-mgmt virtio 52:54:00:yy:yy:yy vnet2 bridge br-mgmt virtio 52:54:00:zz:zz:zz
20.3.4. Step 4: Set PVID on All vNets
for VNET in $(sudo virsh domiflist 9800-WLC-01 | awk 'NR>2 {print $1}'); do
echo "Setting PVID 100 on $VNET"
sudo bridge vlan del vid 1 dev $VNET pvid untagged 2>/dev/null
sudo bridge vlan add vid 100 dev $VNET pvid untagged
done
20.3.5. Step 5: Verify Inside WLC
Wait 2-3 minutes for IOS-XE boot, then verify all 3 interfaces:
ssh admin@10.50.1.40 "show ip interface brief | include Gig"
GigabitEthernet1 10.50.1.40 YES NVRAM up up GigabitEthernet2 unassigned YES unset up up GigabitEthernet3 unassigned YES unset up up
20.4. Prevention: Always Use virt-install for Multi-NIC VMs
|
NEVER use Always recreate the VM definition with |
sudo virt-install \
--name 9800-WLC-XX \
--network bridge=br-mgmt,model=virtio \
--network bridge=br-mgmt,model=virtio \
--network bridge=br-mgmt,model=virtio \
...
# DON'T DO THIS
sudo virsh attach-interface 9800-WLC-XX --type bridge --source br-mgmt --persistent
sudo virsh attach-interface 9800-WLC-XX --type bridge --source br-mgmt --persistent
20.5. Key Learnings (2026-03-06)
-
PCI slot ordering = interface mapping - Gi1/Gi2/Gi3 map to slots 3/4/5 (or similar sequential)
-
attach/detach corrupts slot ordering - libvirt fills gaps, causing non-sequential slots
-
virt-install creates clean slots - All
--networkoptions processed sequentially -
qcow2 preserves config - Undefine + reimport keeps all IOS-XE settings
-
PVID still required - Set PVID 100 on all vnet interfaces after recreation