Skip to content

Commit 4ab218e

Browse files
committed
libvirt_manager: Add boot_order configuration support
Add support for configuring VM boot device priority via boot_order parameter. Accepts list of 'hd'/'disk' and 'network' in desired boot order. Boot order is applied after device attachment. Example: boot_order: ['hd', 'network'] # Try disk first, then PXE Includes molecule test validating domain XML boot order attributes. Assisted-By: Claude Code/claude-sonnet-4 Signed-off-by: Harald Jensås <[email protected]>
1 parent 2c7f6a0 commit 4ab218e

File tree

11 files changed

+528
-0
lines changed

11 files changed

+528
-0
lines changed

roles/libvirt_manager/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ cifmw_libvirt_manager_configuration:
9595
target: (Hypervisor hostname you want to deploy the family on. Optional)
9696
uefi: (boolean, toggle UEFI boot. Optional, defaults to false)
9797
bootmenu_enable: (string, toggle bootmenu. Optional, defaults to "no")
98+
boot_order: (list, optional. Ordered list of boot devices. Valid values are 'hd' or 'disk' for disk boot, and 'network' for network boot. Example: ['hd', 'network'] will attempt disk boot first, then network boot. The boot order is applied after all devices are attached to the VM.)
9899
networkconfig: (dict or list[dict], [network-config](https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html#network-config-v2) v2 config, needed if a static ip address should be defined at boot time in absence of a dhcp server in special scenarios. Optional)
99100
devices: (dict, optional, defaults to {}. The keys are the VMs of that type that needs devices to be attached, and the values are lists of strings, where each string must contain a valid <hostdev/> libvirt XML element that will be passed to virsh attach-device)
100101
networks:
@@ -166,6 +167,9 @@ cifmw_libvirt_manager_configuration:
166167
memory: 8
167168
cpus: 4
168169
bootmenu_enable: "yes"
170+
boot_order:
171+
- hd
172+
- network
169173
nets:
170174
- public
171175
networks:
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
# Copyright 2025 Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
- name: Cleanup
18+
hosts: instance
19+
gather_facts: true
20+
vars:
21+
cifmw_basedir: "/opt/basedir"
22+
tasks:
23+
- name: Run libvirt_manager cleanup
24+
ansible.builtin.import_role:
25+
name: libvirt_manager
26+
tasks_from: clean_layout
27+
28+
- name: Remove /opt/basedir tree
29+
become: true
30+
ansible.builtin.file:
31+
path: "/opt/basedir"
32+
state: absent
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
---
2+
# Copyright 2025 Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
- name: Test boot_order configuration
18+
hosts: instance
19+
gather_facts: true
20+
vars_files:
21+
- vars/net-def.yml
22+
vars:
23+
ansible_user_dir: "{{ lookup('env', 'HOME') }}"
24+
cifmw_basedir: "/opt/basedir"
25+
cifmw_libvirt_manager_configuration:
26+
vms:
27+
# Test VM with disk first, then network boot
28+
baremetal_disk_first:
29+
amount: 1
30+
disksize: 10
31+
memory: 1
32+
cpus: 1
33+
disk_file_name: 'blank'
34+
boot_order:
35+
- hd
36+
- network
37+
nets:
38+
- public
39+
- osp_trunk
40+
# Test VM with network first, then disk boot
41+
baremetal_net_first:
42+
amount: 1
43+
disksize: 10
44+
memory: 1
45+
cpus: 1
46+
disk_file_name: 'blank'
47+
boot_order:
48+
- network
49+
- disk
50+
nets:
51+
- public
52+
- osp_trunk
53+
# Test VM with only network boot
54+
baremetal_net_only:
55+
amount: 1
56+
disksize: 10
57+
memory: 1
58+
cpus: 1
59+
disk_file_name: 'blank'
60+
boot_order:
61+
- network
62+
nets:
63+
- public
64+
# Test VM without boot_order (should not have boot order attributes)
65+
baremetal_no_boot_order:
66+
amount: 1
67+
disksize: 10
68+
memory: 1
69+
cpus: 1
70+
disk_file_name: 'blank'
71+
nets:
72+
- public
73+
networks:
74+
public: |-
75+
<network>
76+
<name>public</name>
77+
<forward mode='nat'/>
78+
<bridge name='public' stp='on' delay='0'/>
79+
<dns enable="no"/>
80+
<ip
81+
family='ipv4'
82+
address='{{ _networks.public.range | ansible.utils.nthhost(1) }}'
83+
prefix='24'>
84+
</ip>
85+
</network>
86+
osp_trunk: |-
87+
<network>
88+
<name>osp_trunk</name>
89+
<forward mode='nat'/>
90+
<bridge name='osp_trunk' stp='on' delay='0'/>
91+
<dns enable="no"/>
92+
<ip
93+
family='ipv4'
94+
address='{{ _networks.osp_trunk.range | ansible.utils.nthhost(1) }}'
95+
prefix='24'>
96+
</ip>
97+
</network>
98+
tasks:
99+
- name: Load networking definition
100+
ansible.builtin.include_vars:
101+
file: input.yml
102+
name: cifmw_networking_definition
103+
104+
- name: Deploy layout with boot_order configurations
105+
ansible.builtin.import_role:
106+
name: libvirt_manager
107+
tasks_from: deploy_layout
108+
109+
- name: Verify boot_order configurations
110+
block:
111+
# Test 1: Verify disk-first VM has correct boot order
112+
- name: Get baremetal_disk_first VM XML
113+
register: _disk_first_xml
114+
community.libvirt.virt:
115+
command: "get_xml"
116+
name: "cifmw-baremetal_disk_first-0"
117+
uri: "qemu:///system"
118+
119+
- name: Check disk boot order in disk-first VM
120+
register: _disk_first_disk_boot
121+
community.general.xml:
122+
xmlstring: "{{ _disk_first_xml.get_xml }}"
123+
xpath: "/domain/devices/disk[@device='disk']/boot"
124+
content: "attribute"
125+
126+
- name: Check interface boot order in disk-first VM
127+
register: _disk_first_net_boot
128+
community.general.xml:
129+
xmlstring: "{{ _disk_first_xml.get_xml }}"
130+
xpath: "/domain/devices/interface[1]/boot"
131+
content: "attribute"
132+
133+
- name: Assert disk-first VM has correct boot order
134+
ansible.builtin.assert:
135+
that:
136+
- _disk_first_disk_boot.matches[0].boot.order == "1"
137+
- _disk_first_net_boot.matches[0].boot.order == "2"
138+
quiet: true
139+
msg: >-
140+
Expected disk boot order=1 and network boot order=2,
141+
got disk={{ _disk_first_disk_boot.matches[0].boot.order }}
142+
and network={{ _disk_first_net_boot.matches[0].boot.order }}
143+
144+
# Test 2: Verify network-first VM has correct boot order
145+
- name: Get baremetal_net_first VM XML
146+
register: _net_first_xml
147+
community.libvirt.virt:
148+
command: "get_xml"
149+
name: "cifmw-baremetal_net_first-0"
150+
uri: "qemu:///system"
151+
152+
- name: Check disk boot order in network-first VM
153+
register: _net_first_disk_boot
154+
community.general.xml:
155+
xmlstring: "{{ _net_first_xml.get_xml }}"
156+
xpath: "/domain/devices/disk[@device='disk']/boot"
157+
content: "attribute"
158+
159+
- name: Check interface boot order in network-first VM
160+
register: _net_first_net_boot
161+
community.general.xml:
162+
xmlstring: "{{ _net_first_xml.get_xml }}"
163+
xpath: "/domain/devices/interface[1]/boot"
164+
content: "attribute"
165+
166+
- name: Assert network-first VM has correct boot order
167+
ansible.builtin.assert:
168+
that:
169+
- _net_first_net_boot.matches[0].boot.order == "1"
170+
- _net_first_disk_boot.matches[0].boot.order == "2"
171+
quiet: true
172+
msg: >-
173+
Expected network boot order=1 and disk boot order=2,
174+
got network={{ _net_first_net_boot.matches[0].boot.order }}
175+
and disk={{ _net_first_disk_boot.matches[0].boot.order }}
176+
177+
# Test 3: Verify network-only VM has only network boot
178+
- name: Get baremetal_net_only VM XML
179+
register: _net_only_xml
180+
community.libvirt.virt:
181+
command: "get_xml"
182+
name: "cifmw-baremetal_net_only-0"
183+
uri: "qemu:///system"
184+
185+
- name: Check interface boot order in network-only VM
186+
register: _net_only_net_boot
187+
community.general.xml:
188+
xmlstring: "{{ _net_only_xml.get_xml }}"
189+
xpath: "/domain/devices/interface[1]/boot"
190+
content: "attribute"
191+
192+
- name: Check disk boot order in network-only VM (should not exist)
193+
register: _net_only_disk_boot
194+
failed_when: false
195+
community.general.xml:
196+
xmlstring: "{{ _net_only_xml.get_xml }}"
197+
xpath: "/domain/devices/disk[@device='disk']/boot"
198+
content: "attribute"
199+
200+
- name: Assert network-only VM has correct boot order
201+
ansible.builtin.assert:
202+
that:
203+
- _net_only_net_boot.matches[0].boot.order == "1"
204+
- _net_only_disk_boot.matches | default([]) | length == 0
205+
quiet: true
206+
msg: >-
207+
Expected only network boot with order=1,
208+
got network={{ _net_only_net_boot.matches[0].boot.order }}
209+
and disk boot count={{ _net_only_disk_boot.matches | default([]) | length }}
210+
211+
# Test 4: Verify VM without boot_order has no boot order attributes
212+
- name: Get baremetal_no_boot_order VM XML
213+
register: _no_boot_order_xml
214+
community.libvirt.virt:
215+
command: "get_xml"
216+
name: "cifmw-baremetal_no_boot_order-0"
217+
uri: "qemu:///system"
218+
219+
- name: Check for any boot order attributes in no-boot-order VM
220+
register: _no_boot_order_check
221+
failed_when: false
222+
community.general.xml:
223+
xmlstring: "{{ _no_boot_order_xml.get_xml }}"
224+
xpath: "/domain/devices//boot"
225+
content: "attribute"
226+
227+
- name: Assert no-boot-order VM has no boot order attributes
228+
ansible.builtin.assert:
229+
that:
230+
- _no_boot_order_check.matches | default([]) | length == 0
231+
quiet: true
232+
msg: >-
233+
Expected no boot order attributes,
234+
but found {{ _no_boot_order_check.matches | default([]) | length }} boot elements
235+
236+
- name: Output success message
237+
ansible.builtin.debug:
238+
msg: "All boot_order validations passed successfully!"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
ansible_python_interpreter: /usr/bin/python3
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
log: true
3+
4+
provisioner:
5+
name: ansible
6+
log: true
7+
inventory:
8+
links:
9+
host_vars: ./host_vars/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
# Copyright 2025 Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
18+
- name: Prepare
19+
ansible.builtin.import_playbook: ../deploy_layout/prepare.yml
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
# Copyright 2025 Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
networks:
18+
ctlplane:
19+
network: "192.168.140.0/24"
20+
gateway: "192.168.140.1"
21+
mtu: 1500
22+
group-templates:
23+
baremetal_disk_firsts:
24+
network-template:
25+
range:
26+
start: 10
27+
length: 1
28+
networks:
29+
ctlplane: {}
30+
baremetal_net_firsts:
31+
network-template:
32+
range:
33+
start: 20
34+
length: 1
35+
networks:
36+
ctlplane: {}
37+
baremetal_net_onlys:
38+
network-template:
39+
range:
40+
start: 30
41+
length: 1
42+
networks:
43+
ctlplane: {}
44+
baremetal_no_boot_orders:
45+
network-template:
46+
range:
47+
start: 40
48+
length: 1
49+
networks:
50+
ctlplane: {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
# Copyright 2025 Red Hat, Inc.
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
_networks:
18+
osp_trunk:
19+
default: true
20+
range: "192.168.140.0/24"
21+
mtu: 1500
22+
public:
23+
range: "192.168.110.0/24"

0 commit comments

Comments
 (0)