Skip to content

Commit 4ceff77

Browse files
committed
Major refactoring of switch-images support
Assisted-By: Claude Code/claude-4.5-sonnet Signed-off-by: Harald Jensås <hjensas@redhat.com>
1 parent 48be961 commit 4ceff77

40 files changed

+4426
-303
lines changed

docs/virtual_switches.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,207 @@
11
# Using virtual switches with Hotstack
22

3+
## Creating a switch image
4+
35
```bash
46
openstack image create hotstack-switch \
57
--disk-format qcow2 \
68
--file <switch-image-file> \
79
--property hw_firmware_type=uefi \
810
--property hw_machine_type=q35 --public
911
```
12+
13+
## Network wiring: OpenStack networks as bridges
14+
15+
Hotstack uses a special network architecture to connect VMs (like baremetal
16+
nodes managed by Ironic) to ports on the virtual switch. This approach uses
17+
**OpenStack Neutron networks as L2 bridges** between VMs and switch ports.
18+
19+
### Architecture overview
20+
21+
The architecture consists of three key components:
22+
23+
1. **Trunk port on the switch**: A Neutron trunk port that carries multiple
24+
VLANs to the switch
25+
2. **Bridge networks**: Small point-to-point Neutron networks connecting each
26+
VM to the switch
27+
3. **VLAN configuration inside the switch**: The switch's internal
28+
configuration determines which VLAN each port belongs to
29+
30+
### How it works
31+
32+
The key insight is that **the switch's internal configuration determines
33+
VLAN membership**, not OpenStack:
34+
35+
- The bridge network (e.g. `ironic0-br-net`) provides L2 connectivity
36+
between the VM and the switch
37+
- Inside the switch, you configure which VLAN the port (connected to the
38+
bridge network) belongs to
39+
- When you change the VLAN configuration of the switch port, the VM
40+
effectively "moves" to a different VLAN
41+
42+
**Example flow:**
43+
44+
1. VM `ironic0` is connected to `ironic0-br-net`
45+
2. The switch port `ethernet1/2` is also connected to `ironic0-br-net`
46+
3. Inside the switch, configure `ethernet1/2` to be an access port on
47+
VLAN 101
48+
4. Traffic from `ironic0` now flows through the bridge network to the
49+
switch, where it enters VLAN 101
50+
5. VLAN 101 traffic exits the switch through the trunk port (tagged with
51+
VLAN 101)
52+
6. OpenStack Neutron receives this as traffic on the `ironic-net` network
53+
(which is VLAN 101 on the trunk)
54+
55+
**To move the VM to a different VLAN:**
56+
57+
1. Reconfigure switch port `ethernet1/2` to be on VLAN 103 instead
58+
2. Now traffic from `ironic0` enters VLAN 103 and exits as traffic on the
59+
`tenant-vlan103` network
60+
3. **No changes needed in OpenStack** - the bridge network stays the same
61+
62+
### Benefits of this approach
63+
64+
- **Flexibility**: VMs can be moved between VLANs by reconfiguring the
65+
switch, not by reconfiguring OpenStack
66+
- **Realistic testing**: Mimics how physical networks work with switches
67+
controlling VLAN membership
68+
- **Simplified OpenStack config**: Each VM just needs one static connection
69+
(the bridge network)
70+
- **Switch-driven networking**: The switch becomes the central point of
71+
control for network topology, just like in a physical datacenter
72+
73+
### Implementation in Heat templates
74+
75+
#### 1. Switch trunk port setup
76+
77+
The switch has a trunk port that connects to multiple VLAN networks as
78+
subports:
79+
80+
```yaml
81+
switch-trunk-parent-port:
82+
type: OS::Neutron::Port
83+
properties:
84+
network: {get_resource: trunk-net}
85+
port_security_enabled: false
86+
87+
switch-trunk-ironic-port:
88+
type: OS::Neutron::Port
89+
properties:
90+
network: {get_resource: ironic-net}
91+
port_security_enabled: false
92+
93+
switch-trunk-tenant-vlan103-port:
94+
type: OS::Neutron::Port
95+
properties:
96+
network: {get_resource: tenant-vlan103}
97+
port_security_enabled: false
98+
99+
switch-trunk-tenant-vlan104-port:
100+
type: OS::Neutron::Port
101+
properties:
102+
network: {get_resource: tenant-vlan104}
103+
port_security_enabled: false
104+
105+
switch-trunk-tenant-vlan105-port:
106+
type: OS::Neutron::Port
107+
properties:
108+
network: {get_resource: tenant-vlan105}
109+
port_security_enabled: false
110+
111+
switch-trunk:
112+
type: OS::Neutron::Trunk
113+
properties:
114+
port: {get_resource: switch-trunk-parent-port}
115+
sub_ports:
116+
# Ironic VLAN
117+
- port: {get_resource: switch-trunk-ironic-port}
118+
segmentation_id: 101
119+
segmentation_type: vlan
120+
# Tenant VLANs
121+
- port: {get_resource: switch-trunk-tenant-vlan103-port}
122+
segmentation_id: 103
123+
segmentation_type: vlan
124+
- port: {get_resource: switch-trunk-tenant-vlan104-port}
125+
segmentation_id: 104
126+
segmentation_type: vlan
127+
- port: {get_resource: switch-trunk-tenant-vlan105-port}
128+
segmentation_id: 105
129+
segmentation_type: vlan
130+
```
131+
132+
This trunk port presents multiple VLANs to the switch VM, just like a
133+
physical trunk port would.
134+
135+
#### 2. Bridge networks connecting VMs to the switch
136+
137+
Each VM that needs to connect to the switch gets a dedicated "bridge
138+
network" - a small Neutron network that acts as a point-to-point L2
139+
connection:
140+
141+
```yaml
142+
ironic0-br-net:
143+
type: OS::Neutron::Net
144+
properties:
145+
port_security_enabled: false
146+
147+
ironic1-br-net:
148+
type: OS::Neutron::Net
149+
properties:
150+
port_security_enabled: false
151+
```
152+
153+
Both the VM and the switch get a port on this bridge network:
154+
155+
```yaml
156+
switch-ironic0-br-port:
157+
type: OS::Neutron::Port
158+
properties:
159+
network: {get_resource: ironic0-br-net}
160+
port_security_enabled: false
161+
162+
switch-ironic1-br-port:
163+
type: OS::Neutron::Port
164+
properties:
165+
network: {get_resource: ironic1-br-net}
166+
port_security_enabled: false
167+
168+
switch:
169+
type: OS::Nova::Server
170+
properties:
171+
# ... image, flavor, and disk configuration ...
172+
networks:
173+
- port: {get_resource: switch-machine-port}
174+
- port: {get_attr: [switch-trunk, port_id]}
175+
- port: {get_resource: switch-ironic0-br-port}
176+
- port: {get_resource: switch-ironic1-br-port}
177+
```
178+
179+
The VMs also connect to their respective bridge networks:
180+
181+
```yaml
182+
ironic0-port:
183+
type: OS::Neutron::Port
184+
properties:
185+
network: {get_resource: ironic0-br-net}
186+
port_security_enabled: false
187+
188+
ironic0:
189+
type: OS::Nova::Server
190+
properties:
191+
# ... image, flavor, and disk configuration ...
192+
networks:
193+
- port: {get_resource: ironic0-port}
194+
195+
ironic1-port:
196+
type: OS::Neutron::Port
197+
properties:
198+
network: {get_resource: ironic1-br-net}
199+
port_security_enabled: false
200+
201+
ironic1:
202+
type: OS::Nova::Server
203+
properties:
204+
# ... image, flavor, and disk configuration ...
205+
networks:
206+
- port: {get_resource: ironic1-port}
207+
```

images/.gitignore

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
# Built images
12
controller.qcow2
23
blank-image.qcow2
34
nat64-appliance.qcow2
4-
switch-host.qcow2
5-
switch-host-base.qcow2
5+
6+
# Temporary files
7+
*.tmp
8+
9+
# NAT64 build artifacts
610
.ci-framework/
711
.nat64-build/

images/Makefile

Lines changed: 0 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ NAT64_IMAGE_NAME ?= nat64-appliance.qcow2
99
NAT64_IMAGE_FORMAT ?= raw
1010
NAT64_CIFMW_DIR ?= $(CURDIR)/.ci-framework
1111
NAT64_BASEDIR ?= $(CURDIR)/.nat64-build
12-
SWITCH_HOST_IMAGE_URL ?= https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-9-latest.x86_64.qcow2
13-
SWITCH_HOST_BASE_IMAGE ?= switch-host-base.qcow2
14-
SWITCH_HOST_IMAGE_NAME ?= switch-host.qcow2
15-
SWITCH_HOST_IMAGE_FORMAT ?= raw
16-
SWITCH_HOST_INSTALL_PACKAGES ?= libvirt,qemu-kvm,qemu-img,expect,unzip,jq,iproute,nmap-ncat,telnet,git,vim-enhanced,tmux,bind-utils,bash-completion,nmstate,tcpdump,python3-jinja2
17-
18-
# Switch vendor image locations (optional, leave empty if not available)
19-
# These will be copied into the image at /opt/<model>/ during build
20-
FORCE10_10_IMAGE ?=
21-
FORCE10_9_IMAGE ?=
22-
NXOS_IMAGE ?=
23-
SONIC_IMAGE ?=
2412

2513
all: controller blank nat64
2614

@@ -86,85 +74,3 @@ nat64_clean:
8674
rm -rf $(NAT64_CIFMW_DIR)
8775
sudo rm -rf $(NAT64_BASEDIR)
8876
rm -rf $(HOME)/test-python
89-
90-
switch-host: switch-host_download switch-host_copy switch-host_customize switch-host_convert
91-
92-
switch-host_download:
93-
@if [ ! -f $(SWITCH_HOST_BASE_IMAGE) ]; then \
94-
echo "Downloading base image to $(SWITCH_HOST_BASE_IMAGE)..."; \
95-
curl -L -o $(SWITCH_HOST_BASE_IMAGE) $(SWITCH_HOST_IMAGE_URL); \
96-
else \
97-
echo "Base image $(SWITCH_HOST_BASE_IMAGE) already exists, skipping download."; \
98-
fi
99-
100-
switch-host_copy:
101-
@echo "Creating working copy: $(SWITCH_HOST_BASE_IMAGE) -> $(SWITCH_HOST_IMAGE_NAME)"
102-
@cp $(SWITCH_HOST_BASE_IMAGE) $(SWITCH_HOST_IMAGE_NAME)
103-
104-
switch-host_customize:
105-
@echo "Customizing switch-host image..."
106-
ifneq ($(FORCE10_10_IMAGE),)
107-
@test -f $(FORCE10_10_IMAGE) || (echo "ERROR: Force10 OS10 image not found: $(FORCE10_10_IMAGE)" && exit 1)
108-
endif
109-
ifneq ($(FORCE10_9_IMAGE),)
110-
@test -f $(FORCE10_9_IMAGE) || (echo "ERROR: Force10 OS9 image not found: $(FORCE10_9_IMAGE)" && exit 1)
111-
endif
112-
ifneq ($(NXOS_IMAGE),)
113-
@test -f $(NXOS_IMAGE) || (echo "ERROR: NXOS image not found: $(NXOS_IMAGE)" && exit 1)
114-
endif
115-
ifneq ($(SONIC_IMAGE),)
116-
@test -f $(SONIC_IMAGE) || (echo "ERROR: SONiC image not found: $(SONIC_IMAGE)" && exit 1)
117-
endif
118-
virt-customize -a $(SWITCH_HOST_IMAGE_NAME) \
119-
--install $(SWITCH_HOST_INSTALL_PACKAGES) \
120-
--timezone UTC \
121-
--copy-in switch-host-scripts/start-switch-vm.sh:/usr/local/bin \
122-
--chmod 0755:/usr/local/bin/start-switch-vm.sh \
123-
--run-command 'mkdir -p /usr/local/lib/hotstack-switch-vm' \
124-
--copy-in switch-host-scripts/common.sh:/usr/local/lib/hotstack-switch-vm \
125-
--copy-in switch-host-scripts/bridges.nmstate.yaml.j2:/usr/local/lib/hotstack-switch-vm \
126-
--chmod 0644:/usr/local/lib/hotstack-switch-vm/common.sh \
127-
--chmod 0644:/usr/local/lib/hotstack-switch-vm/bridges.nmstate.yaml.j2 \
128-
--run-command 'mkdir -p /etc/hotstack-switch-vm' \
129-
--run-command 'mkdir -p /var/lib/hotstack-switch-vm' \
130-
$(if $(FORCE10_10_IMAGE), \
131-
--copy-in switch-host-scripts/force10_10:/usr/local/lib/hotstack-switch-vm \
132-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/force10_10/setup.sh \
133-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/force10_10/wait.sh \
134-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/force10_10/configure.sh \
135-
--chmod 0644:/usr/local/lib/hotstack-switch-vm/force10_10/domain.xml.j2 \
136-
--run-command 'mkdir -p /opt/force10_10' \
137-
--copy-in $(FORCE10_10_IMAGE):/opt/force10_10 \
138-
--run-command 'basename $(FORCE10_10_IMAGE) > /opt/force10_10/image-info.txt',) \
139-
$(if $(FORCE10_9_IMAGE), \
140-
--run-command 'mkdir -p /opt/force10_9' \
141-
--copy-in $(FORCE10_9_IMAGE):/opt/force10_9,) \
142-
$(if $(NXOS_IMAGE), \
143-
--copy-in switch-host-scripts/nxos:/usr/local/lib/hotstack-switch-vm \
144-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/nxos/setup.sh \
145-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/nxos/wait.sh \
146-
--chmod 0755:/usr/local/lib/hotstack-switch-vm/nxos/configure.sh \
147-
--chmod 0644:/usr/local/lib/hotstack-switch-vm/nxos/domain.xml.j2 \
148-
--run-command 'mkdir -p /opt/nxos' \
149-
--copy-in $(NXOS_IMAGE):/opt/nxos \
150-
--run-command 'basename $(NXOS_IMAGE) > /opt/nxos/image-info.txt',) \
151-
$(if $(SONIC_IMAGE), \
152-
--run-command 'mkdir -p /opt/sonic' \
153-
--copy-in $(SONIC_IMAGE):/opt/sonic,) \
154-
--selinux-relabel
155-
@echo "Switch-host image customization complete"
156-
157-
switch-host_convert:
158-
ifeq ($(SWITCH_HOST_IMAGE_FORMAT),raw)
159-
@echo "Converting switch-host image to raw format (in-place)..."
160-
qemu-img convert -p -f qcow2 -O raw $(SWITCH_HOST_IMAGE_NAME) $(SWITCH_HOST_IMAGE_NAME).tmp
161-
mv $(SWITCH_HOST_IMAGE_NAME).tmp $(SWITCH_HOST_IMAGE_NAME)
162-
@echo "Switch-host image converted to raw format: $(SWITCH_HOST_IMAGE_NAME)"
163-
endif
164-
165-
switch-host_clean:
166-
rm -f $(SWITCH_HOST_IMAGE_NAME)
167-
rm -f $(SWITCH_HOST_IMAGE_NAME).tmp
168-
169-
switch-host_clean_all: switch-host_clean
170-
rm -f $(SWITCH_HOST_BASE_IMAGE)

0 commit comments

Comments
 (0)