diff --git a/doc/source/contributor/testenv.rst b/doc/source/contributor/testenv.rst
index 38d9be4b..60046cba 100644
--- a/doc/source/contributor/testenv.rst
+++ b/doc/source/contributor/testenv.rst
@@ -188,3 +188,17 @@ sushy-tools_ is also installed.
.. _VirtualBMC: https://docs.openstack.org/virtualbmc/
.. _sushy-tools: https://docs.openstack.org/sushy-tools/
+
+Virtual Switching
+-----------------
+By default, Bifrost sets up a Linux bridge as the virtual switch
+interconnecting the virtual machines that implement the nodes. To support
+more complex test scenarios, it is possible to configure OVS as the virtual
+switch. This enables updates to port VLAN assignments to test complex
+networking scenarios.
+
+The virtual switch type can be controlled by modifying the
+``test_vm_switch_type`` variable via ansible extra vars supplied to the Ansible
+commands or via bifrost-cli's ``-e`` option. Setting the variable to 'ovs'
+enables the OVS switch type.
+
diff --git a/playbooks/roles/bifrost-create-vm-nodes/README.md b/playbooks/roles/bifrost-create-vm-nodes/README.md
index 31812c9a..b37e333e 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/README.md
+++ b/playbooks/roles/bifrost-create-vm-nodes/README.md
@@ -12,6 +12,10 @@ The following packages are required and ensured to be present:
- qemu-kvm
- sgabios (except on CentOS Stream 10 / Rocky Linux 10)
+Additional packages required when using test_vm_switch_type: 'ovs':
+- openvswitch-switch (Debian/Ubuntu)
+- openvswitch (RedHat/CentOS)
+
Warning
-------
@@ -150,6 +154,31 @@ test_vm_network_dhcp_end: End of DHCP range for 'test_vm_network'.
from scratch and when
'test_vm_network_enable_dhcp' is enabled.
+test_vm_switch_type: Type of virtual switch to use for test VMs.
+ Defaults to 'linux_bridge'.
+ Set to 'ovs' to use Open vSwitch with VLAN support
+ for testing networking features.
+
+test_ovs_bridge_name: Name of the OVS bridge to create when using
+ test_vm_switch_type: 'ovs'.
+ Defaults to 'brtest'.
+
+test_ovs_host_vlans: List of VLAN IDs to configure on the OVS bridge.
+ Defaults to ['10', '20', '30'].
+ Creates separate VLANs for inspection, tenant, and
+ other network types (cleaning, rescuing, servicing).
+ VLAN IDs must be 1-255.
+
+test_ovs_vm_initial_vlan: Initial VLAN ID for test VMs on OVS bridge.
+ Defaults to '10'.
+ VMs start on this VLAN and can be moved between
+ VLANs by the networking driver.
+
+test_ovs_user: Username for OVS restricted user access.
+ Defaults to 'ovsuser'.
+ Uses SSH key-based authentication (password login is disabled).
+ Used for controlled VLAN management operations.
+
Dependencies
------------
@@ -158,12 +187,27 @@ None at this time.
Example Playbook
----------------
+Basic usage with default Linux bridge:
+
+- hosts: localhost
+ connection: local
+ become: yes
+ gather_facts: yes
+ roles:
+ - role: bifrost-create-vm-nodes
+
+Using Open vSwitch for testing standalone networking features:
+
- hosts: localhost
connection: local
become: yes
gather_facts: yes
roles:
- role: bifrost-create-vm-nodes
+ vars:
+ test_vm_switch_type: ovs
+ test_ovs_host_vlans: ['10', '20', '30']
+ test_ovs_vm_initial_vlan: '10'
License
-------
diff --git a/playbooks/roles/bifrost-create-vm-nodes/defaults/main.yml b/playbooks/roles/bifrost-create-vm-nodes/defaults/main.yml
index e95da717..d2021fc5 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/defaults/main.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/defaults/main.yml
@@ -97,3 +97,18 @@ efi_nvram_locations_secboot:
- /usr/share/OVMF/OVMF_VARS.secboot.fd
efi_nvram_locations: >-
{{ efi_nvram_locations_secboot if test_vm_secure_boot | bool else efi_nvram_locations_normal }}
+
+# Switch type configuration (default: linux_bridge)
+test_vm_switch_type: linux_bridge
+
+# OVS-specific configuration
+test_ovs_bridge_name: brtest
+
+# Simple VLAN configuration
+# NOTE: VLAN IDs must be 1-255 when used for IP subnets (192.168.{VLAN}.0/24)
+test_ovs_host_vlans: ['10', '20', '30']
+test_ovs_vm_initial_vlan: '10'
+
+# OVS restricted user configuration
+# Uses SSH key-based authentication (password authentication is disabled)
+test_ovs_user: ovsuser
diff --git a/playbooks/roles/bifrost-create-vm-nodes/handlers/main.yml b/playbooks/roles/bifrost-create-vm-nodes/handlers/main.yml
new file mode 100644
index 00000000..9610f837
--- /dev/null
+++ b/playbooks/roles/bifrost-create-vm-nodes/handlers/main.yml
@@ -0,0 +1,17 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+---
+- name: Restart sshd
+ systemd:
+ name: sshd
+ state: restarted
diff --git a/playbooks/roles/bifrost-create-vm-nodes/tasks/create_vm.yml b/playbooks/roles/bifrost-create-vm-nodes/tasks/create_vm.yml
index 54b7e071..9e87f845 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/tasks/create_vm.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/tasks/create_vm.yml
@@ -19,6 +19,7 @@
vm_name: "{{ item }}"
vm_log_file: "{{ test_vm_logdir }}/{{ item }}_console.log"
vm_host_group: "{{ test_vm_default_groups }}"
+ vm_port_name: "{{ item }}-port0"
- set_fact:
vm_host_group: "{{ test_vm_default_groups | union(test_vm_groups[vm_name]) }}"
@@ -41,6 +42,28 @@
command: list_vms
register: existing_vms
+# Create OVS port with VLAN tag for VM when using OVS
+- name: check if OVS port already exists
+ shell:
+ cmd: |
+ set -eo pipefail
+ ovs-vsctl list-ports {{ test_ovs_bridge_name }} | grep -x {{ vm_port_name }} || echo "not_found"
+ register: ovs_port_check
+ when: test_vm_switch_type == 'ovs'
+
+- name: create OVS port with VLAN tag
+ shell: |
+ ovs-vsctl add-port {{ test_ovs_bridge_name }} {{ vm_port_name }} tag={{ test_ovs_vm_initial_vlan }} -- set interface {{ vm_port_name }} type=internal
+ when:
+ - test_vm_switch_type == 'ovs'
+ - ovs_port_check.stdout.strip() == "not_found"
+
+- name: configure OVS linux interface
+ shell: |
+ ovs-vsctl set interface {{ vm_port_name }} lldp:enable=true
+ ip link set {{ vm_port_name }} up
+ when: test_vm_switch_type == 'ovs'
+
# NOTE(pas-ha) wrapping in block/rescue to have diagnostic output, requires Ansible>=2
- when: vm_name not in existing_vms.list_vms
block:
@@ -129,6 +152,20 @@
set_fact:
vm_mac: "{{ (testvm_xml.get_xml | regex_findall(\"\") | first).split('=') | last | regex_replace(\"['/>]\", '') }}"
+- name: set VM network configuration for OVS
+ set_fact:
+ vm_network_base: "192.168.{{ 100 + test_ovs_vm_initial_vlan | int }}."
+ vm_ip_offset: "{{ 2 + (testvm_json_data | length) }}"
+ mgmt_network_ip: "192.168.{{ 100 + test_ovs_vm_initial_vlan | int }}.1"
+ when: test_vm_switch_type == 'ovs'
+
+- name: set VM network configuration for bridge
+ set_fact:
+ vm_network_base: "192.168.122."
+ vm_ip_offset: "{{ 2 + (testvm_json_data | length) }}"
+ mgmt_network_ip: "192.168.122.1"
+ when: test_vm_switch_type == 'linux_bridge'
+
# NOTE(pas-ha) using default username and password set by virtualbmc - "admin" and "password" respectively
# see vbmc add --help
- name: set the json entry for vm
@@ -139,7 +176,7 @@
host_groups: "{{ vm_host_group }}"
driver: "{{ test_vm_node_driver }}"
driver_info:
- ipmi_address: "192.168.122.1"
+ ipmi_address: "{{ mgmt_network_ip }}"
ipmi_port: "{{ virtual_ipmi_port }}"
ipmi_username: "admin"
ipmi_password: "password"
@@ -149,8 +186,8 @@
redfish_password: "password"
nics:
- mac: "{{ vm_mac }}"
- ansible_ssh_host: "192.168.122.{{ testvm_json_data | length + 2 }}"
- ipv4_address: "192.168.122.{{ testvm_json_data | length + 2 }}"
+ ansible_ssh_host: "{{ vm_network_base }}{{ vm_ip_offset }}"
+ ipv4_address: "{{ vm_network_base }}{{ vm_ip_offset }}"
properties:
cpu_arch: "{{ test_vm_arch }}"
ram: "{{ test_vm_memory_size }}"
@@ -161,7 +198,7 @@
uuid: "{{ vm_name | to_uuid }}"
driver: "{{ test_vm_node_driver }}"
driver_info:
- ipmi_address: "192.168.122.1"
+ ipmi_address: "{{ mgmt_network_ip }}"
ipmi_port: "{{ virtual_ipmi_port }}"
ipmi_username: "admin"
ipmi_password: "password"
diff --git a/playbooks/roles/bifrost-create-vm-nodes/tasks/main.yml b/playbooks/roles/bifrost-create-vm-nodes/tasks/main.yml
index 6efb1bc2..c8b1c44f 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/tasks/main.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/tasks/main.yml
@@ -85,6 +85,9 @@
group: "{{ ansible_user_gid }}"
when: copy_from_local_path | bool
+- import_tasks: prepare_ovs.yml
+ when: test_vm_switch_type == 'ovs'
+
- import_tasks: prepare_libvirt.yml
- name: truncate explicit list of vm names
diff --git a/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_libvirt.yml b/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_libvirt.yml
index f5b49c1f..aaf6e029 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_libvirt.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_libvirt.yml
@@ -104,7 +104,7 @@
virt_net:
name: "{{ test_vm_network }}"
state: present
- xml: "{{ lookup('template', 'net.xml.j2') }}"
+ xml: "{{ lookup('template', 'ovs-net.xml.j2' if test_vm_switch_type == 'ovs' else 'net.xml.j2') }}"
uri: "{{ test_vm_libvirt_uri }}"
- name: find facts on libvirt networks
diff --git a/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_ovs.yml b/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_ovs.yml
new file mode 100644
index 00000000..e58db860
--- /dev/null
+++ b/playbooks/roles/bifrost-create-vm-nodes/tasks/prepare_ovs.yml
@@ -0,0 +1,120 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Setup OVS bridge with VLAN interfaces and DHCP services
+---
+- name: enable NFV repository for OVS on CentOS Stream 10
+ package:
+ name: centos-release-nfv-openvswitch
+ state: present
+ when:
+ - ansible_distribution == "CentOS"
+ - ansible_distribution_major_version|int >= 10
+
+- name: install OVS packages
+ package:
+ name: "{{ ovs_packages }}"
+ state: present
+
+- name: ensure OVS services are started and enabled
+ systemd:
+ name: "{{ ovs_service_name }}"
+ state: started
+ enabled: yes
+
+- name: create OVS bridge
+ openvswitch.openvswitch.openvswitch_bridge:
+ bridge: "{{ test_ovs_bridge_name }}"
+ state: present
+
+- name: bring up OVS bridge
+ command: ip link set {{ test_ovs_bridge_name }} up
+
+- name: create VLAN interfaces on OVS bridge
+ shell: |
+ ovs-vsctl add-port {{ test_ovs_bridge_name }} {{ test_ovs_bridge_name }}.{{ item }} tag={{ item }} -- set interface {{ test_ovs_bridge_name }}.{{ item }} type=internal
+ ip addr add 192.168.{{ 100 + item | int }}.1/24 dev {{ test_ovs_bridge_name }}.{{ item }}
+ ip link set {{ test_ovs_bridge_name }}.{{ item }} up
+ loop: "{{ test_ovs_host_vlans }}"
+ ignore_errors: yes
+
+- name: enable IP forwarding for OVS bridge
+ sysctl:
+ name: "net.ipv4.ip_forward"
+ value: 1
+ sysctl_set: yes
+ state: present
+ reload: yes
+
+- name: ensure .ssh directory exists for OVS user
+ file:
+ path: /home/{{ test_ovs_user }}/.ssh
+ state: directory
+ owner: "{{ test_ovs_user }}"
+ mode: '0700'
+
+- name: create OVS user
+ user:
+ name: "{{ test_ovs_user }}"
+ password: '!' # Disabled password
+ shell: /bin/bash
+ home: /home/{{ test_ovs_user }}
+ create_home: yes
+ groups: openvswitch
+ state: present
+
+- name: generate SSH key pair for OVS user
+ user:
+ name: "{{ test_ovs_user }}"
+ generate_ssh_key: yes
+ ssh_key_type: ed25519
+ ssh_key_file: .ssh/id_ed25519
+
+- name: read OVS user public key
+ slurp:
+ src: /home/{{ test_ovs_user }}/.ssh/id_ed25519.pub
+ register: ovs_user_pubkey
+
+- name: add public key to authorized_keys for OVS user
+ authorized_key:
+ user: "{{ test_ovs_user }}"
+ key: "{{ ovs_user_pubkey['content'] | b64decode }}"
+ state: present
+
+- name: set OVS socket group permissions
+ file:
+ path: /var/run/openvswitch/db.sock
+ group: openvswitch
+ mode: '0660'
+
+# TODO(alegacy): this could be refined so that access is restricted to a
+# specific set of OVS commands only using something like rbash
+- name: add OVS user to sudoers for privileged access
+ copy:
+ dest: /etc/sudoers.d/{{ test_ovs_user }}-ovs
+ mode: '0440'
+ content: |
+ # Allow {{ test_ovs_user }} to run OVS commands as root without password
+ {{ test_ovs_user }} ALL=(ALL) NOPASSWD: /bin/bash
+
+- name: Restrict OVS user SSH access from localhost only and disable password auth
+ ansible.builtin.blockinfile:
+ path: /etc/ssh/sshd_config
+ block: |
+ Match User {{ test_ovs_user }}
+ AllowUsers {{ test_ovs_user }}@localhost {{ test_ovs_user }}@127.0.0.1 {{ test_ovs_user }}@::1
+ PasswordAuthentication no
+ PubkeyAuthentication yes
+ marker: "# {mark} ANSIBLE MANAGED BLOCK FOR {{ test_ovs_user }}"
+ validate: 'sshd -t -f %s'
+ notify: Restart sshd
diff --git a/playbooks/roles/bifrost-create-vm-nodes/templates/ovs-net.xml.j2 b/playbooks/roles/bifrost-create-vm-nodes/templates/ovs-net.xml.j2
new file mode 100644
index 00000000..c54ec425
--- /dev/null
+++ b/playbooks/roles/bifrost-create-vm-nodes/templates/ovs-net.xml.j2
@@ -0,0 +1,6 @@
+
+ {{ test_vm_network }}
+
+
+
+
diff --git a/playbooks/roles/bifrost-create-vm-nodes/templates/testvm.xml.j2 b/playbooks/roles/bifrost-create-vm-nodes/templates/testvm.xml.j2
index 150e7591..310edcb1 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/templates/testvm.xml.j2
+++ b/playbooks/roles/bifrost-create-vm-nodes/templates/testvm.xml.j2
@@ -36,6 +36,16 @@
+ {% if test_vm_switch_type == 'ovs' %}
+
+
+
+
+ {% if default_boot_mode == 'uefi' %}
+
+ {% endif %}
+
+ {% else %}
@@ -43,6 +53,7 @@
{% endif %}
+ {% endif %}
diff --git a/playbooks/roles/bifrost-create-vm-nodes/vars/debian.yml b/playbooks/roles/bifrost-create-vm-nodes/vars/debian.yml
index e0ac86e7..dfb8814b 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/vars/debian.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/vars/debian.yml
@@ -15,3 +15,8 @@ required_packages:
- ovmf
- ebtables
- dnsmasq
+
+ovs_packages:
+ - openvswitch-switch
+ - python3-openvswitch
+ovs_service_name: openvswitch-switch
diff --git a/playbooks/roles/bifrost-create-vm-nodes/vars/redhat.yml b/playbooks/roles/bifrost-create-vm-nodes/vars/redhat.yml
index 6e118e76..dade70b1 100644
--- a/playbooks/roles/bifrost-create-vm-nodes/vars/redhat.yml
+++ b/playbooks/roles/bifrost-create-vm-nodes/vars/redhat.yml
@@ -16,4 +16,11 @@ required_packages:
- libxslt-devel
- libxml2-devel
- edk2-ovmf
+
+ovs_packages:
+ # TODO(alegacy): Update versioned package names periodically
+ - openvswitch3.5
+ - python3-openvswitch3.5
+ovs_service_name: openvswitch
+
test_vm_emulator: "/usr/libexec/qemu-kvm"
diff --git a/playbooks/roles/bifrost-ironic-install/defaults/main.yml b/playbooks/roles/bifrost-ironic-install/defaults/main.yml
index 2f2cb858..975d6915 100644
--- a/playbooks/roles/bifrost-ironic-install/defaults/main.yml
+++ b/playbooks/roles/bifrost-ironic-install/defaults/main.yml
@@ -409,6 +409,10 @@ keystone:
# Timeout for gathering facts.
fact_gather_timeout: "{{ lookup('config', 'DEFAULT_GATHER_TIMEOUT', on_missing='skip') | default(omit, true) }}"
+# Custom switch support (duplicated from create-vm-nodes)
+test_vm_switch_type: linux_bridge
+test_ovs_bridge_name: brtest
+
# Enable TLS support.
enable_tls: false
vmedia_enable_tls: "{{ enable_tls }}"
diff --git a/playbooks/roles/bifrost-ironic-install/tasks/bootstrap.yml b/playbooks/roles/bifrost-ironic-install/tasks/bootstrap.yml
index 29a230a5..83454e03 100644
--- a/playbooks/roles/bifrost-ironic-install/tasks/bootstrap.yml
+++ b/playbooks/roles/bifrost-ironic-install/tasks/bootstrap.yml
@@ -297,13 +297,21 @@
itf_infos: "{{ internal_interface }}"
dhcp_netaddr: "{{ dhcp_pool_start }}/{{ dhcp_static_mask }}"
when: enable_dhcp | bool
+
- name: "Compute interface and DHCP network information"
set_fact:
itf_netaddr1: "{{ itf_infos['address'] }}/{{ itf_infos['netmask'] }}"
itf_netaddr2: "{{ itf_infos['network'] }}/{{ itf_infos['netmask'] }}"
- itf_broadcast: "{{ itf_infos['broadcast'] }}/{{ itf_infos['netmask'] }}"
dhcp_netaddr: "{{ dhcp_netaddr | ansible.utils.ipaddr('network') }}/{{ dhcp_static_mask }}"
when: enable_dhcp | bool
+
+- name: "Compute broadcast address for interface"
+ set_fact:
+ # NOTE: VLAN interfaces (e.g., brtest.10) may have empty broadcast field in Ansible facts.
+ # If broadcast is empty, compute it from the address/netmask to avoid validation failures.
+ itf_broadcast: "{{ (itf_infos['broadcast'] | length > 0) | ternary(itf_infos['broadcast'] + '/' + itf_infos['netmask'], itf_netaddr1 | ansible.utils.ipaddr('broadcast') + '/' + itf_infos['netmask']) }}"
+ when: enable_dhcp | bool
+
- name: "Validate interface network addresses"
fail:
msg: >
@@ -313,6 +321,7 @@
when:
- enable_dhcp | bool
- itf_netaddr1 | ansible.utils.ipaddr('network') != itf_netaddr2 | ansible.utils.ipaddr('network')
+
- name: "Validate interface broadcast addresses"
fail:
msg: >
@@ -322,6 +331,7 @@
when:
- enable_dhcp | bool
- itf_netaddr1 | ansible.utils.ipaddr('broadcast') != itf_broadcast | ansible.utils.ipaddr('broadcast')
+
- name: "Validate DHCP and interface addresses"
debug:
msg: >
@@ -332,6 +342,7 @@
when:
- enable_dhcp | bool
- itf_netaddr2 | ansible.utils.ipaddr('network') != dhcp_netaddr | ansible.utils.ipaddr('network')
+
- name: "Computing new DHCP information"
set_fact:
dhcp_start_ip: "{{ dhcp_pool_start.split('.')[-1] }}"
@@ -340,6 +351,7 @@
when:
- enable_dhcp | bool
- itf_netaddr2 | ansible.utils.ipaddr('network') != dhcp_netaddr | ansible.utils.ipaddr('network')
+
# Note(olivierbourdon38): we could do much more complex network
# computation to derive exact (or way closer to exact) range for
# the new network depending on netmasks and indexes.
diff --git a/playbooks/roles/bifrost-ironic-install/tasks/main.yml b/playbooks/roles/bifrost-ironic-install/tasks/main.yml
index 94f2a610..f01caf46 100644
--- a/playbooks/roles/bifrost-ironic-install/tasks/main.yml
+++ b/playbooks/roles/bifrost-ironic-install/tasks/main.yml
@@ -34,6 +34,13 @@
include_tasks: bootstrap.yml
when: not skip_bootstrap | bool
+- name: "Bootstrap OVS"
+ include_tasks: ovs_bootstrap.yml
+ when:
+ - not skip_bootstrap | bool
+ - testing | bool
+ - test_vm_switch_type == 'ovs'
+
- name: "Start Ironic services"
include_tasks: start.yml
when: not skip_start | bool
diff --git a/playbooks/roles/bifrost-ironic-install/tasks/ovs_bootstrap.yml b/playbooks/roles/bifrost-ironic-install/tasks/ovs_bootstrap.yml
new file mode 100644
index 00000000..1a1aca7a
--- /dev/null
+++ b/playbooks/roles/bifrost-ironic-install/tasks/ovs_bootstrap.yml
@@ -0,0 +1,62 @@
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+---
+- name: "Collect configured OVS VLANs from the bridge"
+ shell:
+ cmd: |
+ set -eo pipefail
+ ovs-vsctl list-ports {{ test_ovs_bridge_name }} | grep -E '\.([0-9]+)$' | sed 's/.*\.\([0-9]\+\)$/\1/' | sort -n
+ register: ovs_configured_vlans
+ when: enable_dhcp | bool
+ failed_when: false
+ changed_when: false
+
+- name: "Set variable with configured OVS VLAN IDs"
+ set_fact:
+ ovs_vlan_ids: "{{ ovs_configured_vlans.stdout_lines | default([]) }}"
+ when: enable_dhcp | bool
+
+- name: "Configure OVS VLAN DHCP in main dnsmasq"
+ template:
+ src: ovs-vlans-dhcp.conf.j2
+ dest: "/etc/dnsmasq.d/ovs-vlans.conf"
+ mode: "0644"
+ when: enable_dhcp | bool
+
+- name: "Get OVS ports with Linux interfaces"
+ shell:
+ cmd: |
+ set -eo pipefail
+ for port in $(ovs-vsctl list-ports {{ test_ovs_bridge_name }}); do
+ if ovs-vsctl get interface $port type 2>/dev/null | grep -q "internal\|\"\""; then
+ if ip link show $port >/dev/null 2>&1; then
+ echo $port
+ fi
+ fi
+ done
+ register: ovs_linux_interfaces
+ when: use_firewalld | bool
+
+- name: "Add OVS Linux interfaces to firewall zone"
+ firewalld:
+ zone: "{{ 'libvirt' if testing | bool else firewalld_internal_zone }}"
+ interface: "{{ item }}"
+ state: enabled
+ permanent: yes
+ immediate: yes
+ loop: "{{ ovs_linux_interfaces.stdout_lines | default([]) }}"
+ when:
+ - use_firewalld | bool
+ - ovs_linux_interfaces.stdout_lines is defined
+ - ovs_linux_interfaces.stdout_lines | length > 0
diff --git a/playbooks/roles/bifrost-ironic-install/templates/dnsmasq.conf.j2 b/playbooks/roles/bifrost-ironic-install/templates/dnsmasq.conf.j2
index 7934abc1..c89af148 100644
--- a/playbooks/roles/bifrost-ironic-install/templates/dnsmasq.conf.j2
+++ b/playbooks/roles/bifrost-ironic-install/templates/dnsmasq.conf.j2
@@ -13,7 +13,9 @@ port=53
port=0
{% endif %}
+{% if test_vm_switch_type == 'linux_bridge' %}
listen-address={{ internal_ip }}
+{% endif %}
# On systems which support it, dnsmasq binds the wildcard address,
# even when it is listening on only some interfaces. It then discards
@@ -61,6 +63,7 @@ domain={{ domain }}
# a lease time. If you have more than one network, you will need to
# repeat this for each network on which you want to supply DHCP
# service.
+{% if test_vm_switch_type == 'linux_bridge' %}
{% if testing | bool == true %}
dhcp-range=192.168.122.2,192.168.122.254,12h
{% elif inventory_dhcp | bool == true %}
@@ -68,6 +71,7 @@ dhcp-range={{dhcp_pool_start}},static,{{dhcp_static_mask}},{{dhcp_lease_time}}
{% else %}
dhcp-range={{dhcp_pool_start}},{{dhcp_pool_end}},{% if dhcp_pool_mask is defined %}{{dhcp_pool_mask}},{% endif %}{{dhcp_lease_time}}
{% endif %}
+{% endif %}
# Override the default route supplied by dnsmasq, which assumes the
# router is the same machine as the one running dnsmasq.
diff --git a/playbooks/roles/bifrost-ironic-install/templates/ovs-vlans-dhcp.conf.j2 b/playbooks/roles/bifrost-ironic-install/templates/ovs-vlans-dhcp.conf.j2
new file mode 100644
index 00000000..db15952f
--- /dev/null
+++ b/playbooks/roles/bifrost-ironic-install/templates/ovs-vlans-dhcp.conf.j2
@@ -0,0 +1,12 @@
+# OVS VLAN DHCP configuration for main dnsmasq
+# Generated by bifrost-ironic-install role during installation
+
+{% for vlan in ovs_vlan_ids | default([]) %}
+{% set subnet_id = 100 + vlan|int %}
+# DHCP configuration for VLAN {{ vlan }} (subnet 192.168.{{ subnet_id }}.0/24)
+interface={{ test_ovs_bridge_name }}.{{ vlan }}
+dhcp-range=tag:{{ test_ovs_bridge_name }}.{{ vlan }},192.168.{{ subnet_id }}.10,192.168.{{ subnet_id }}.100,255.255.255.0,12h
+dhcp-option=tag:{{ test_ovs_bridge_name }}.{{ vlan }},3,192.168.{{ subnet_id }}.1
+dhcp-option=tag:{{ test_ovs_bridge_name }}.{{ vlan }},6,192.168.{{ subnet_id }}.1
+
+{% endfor %}
diff --git a/releasenotes/notes/ovs-virtual-switch-support-36b3693a1076e2f6.yaml b/releasenotes/notes/ovs-virtual-switch-support-36b3693a1076e2f6.yaml
new file mode 100644
index 00000000..b7b9ad66
--- /dev/null
+++ b/releasenotes/notes/ovs-virtual-switch-support-36b3693a1076e2f6.yaml
@@ -0,0 +1,10 @@
+---
+features:
+ - |
+ Adds support for using Open vSwitch (OVS) as a virtual switch for testing
+ environments. Setting ``test_vm_switch_type`` to ``ovs`` creates 3 separate
+ VLANs for comprehensive network testing: one dedicated as an inspection
+ network, another for the final tenant network, and a third for all other
+ network types (cleaning, rescuing, servicing, etc.). This enhancement
+ enables testing of the standalone networking feature with proper VLAN
+ configuration support through the networking generic switch driver.