Merge ~corey.bryant/ubuntu/+source/ironic:master into ~ubuntu-openstack-dev/ubuntu/+source/ironic:master

Proposed by Corey Bryant
Status: Rejected
Rejected by: Corey Bryant
Proposed branch: ~corey.bryant/ubuntu/+source/ironic:master
Merge into: ~ubuntu-openstack-dev/ubuntu/+source/ironic:master
Diff against target: 2682 lines (+1289/-242)
59 files modified
ChangeLog (+17/-0)
PKG-INFO (+43/-46)
api-ref/source/baremetal-api-v1-node-management.inc (+8/-0)
api-ref/source/baremetal-api-v1-nodes-firmware.inc (+1/-1)
debian/changelog (+4/-2)
debian/control (+15/-0)
devstack/lib/ironic (+3/-4)
doc/source/admin/drivers/redfish.rst (+16/-0)
doc/source/contributor/webapi-version-history.rst (+10/-10)
driver-requirements.txt (+1/-1)
ironic.egg-info/PKG-INFO (+43/-46)
ironic.egg-info/SOURCES.txt (+7/-0)
ironic.egg-info/entry_points.txt (+1/-0)
ironic.egg-info/pbr.json (+1/-1)
ironic/api/controllers/v1/utils.py (+1/-1)
ironic/common/pxe_utils.py (+0/-11)
ironic/common/utils.py (+6/-2)
ironic/conductor/cleaning.py (+2/-0)
ironic/conductor/steps.py (+12/-9)
ironic/conductor/utils.py (+12/-0)
ironic/conf/inspector.py (+5/-3)
ironic/conf/redfish.py (+6/-0)
ironic/db/sqlalchemy/api.py (+12/-15)
ironic/drivers/modules/deploy_utils.py (+4/-2)
ironic/drivers/modules/fake.py (+2/-0)
ironic/drivers/modules/inspect_utils.py (+3/-0)
ironic/drivers/modules/redfish/firmware.py (+452/-0)
ironic/drivers/modules/redfish/firmware_utils.py (+44/-0)
ironic/drivers/modules/redfish/management.py (+108/-5)
ironic/drivers/modules/redfish/utils.py (+38/-1)
ironic/drivers/redfish.py (+5/-0)
ironic/objects/firmware.py (+9/-5)
ironic/tests/unit/api/base.py (+3/-6)
ironic/tests/unit/api/controllers/v1/test_port.py (+0/-1)
ironic/tests/unit/api/test_acl.py (+0/-10)
ironic/tests/unit/common/test_neutron.py (+0/-1)
ironic/tests/unit/common/test_pxe_utils.py (+0/-5)
ironic/tests/unit/common/test_utils.py (+6/-0)
ironic/tests/unit/conductor/test_steps.py (+9/-0)
ironic/tests/unit/db/test_nodes.py (+35/-0)
ironic/tests/unit/drivers/modules/ilo/test_management.py (+4/-4)
ironic/tests/unit/drivers/modules/ilo/test_raid.py (+2/-2)
ironic/tests/unit/drivers/modules/redfish/test_bios.py (+3/-2)
ironic/tests/unit/drivers/modules/redfish/test_firmware.py (+40/-0)
ironic/tests/unit/drivers/modules/redfish/test_management.py (+173/-30)
ironic/tests/unit/drivers/modules/redfish/test_raid.py (+4/-2)
ironic/tests/unit/drivers/modules/redfish/test_utils.py (+8/-0)
ironic/tests/unit/drivers/modules/test_agent_base.py (+8/-8)
ironic/tests/unit/drivers/modules/test_inspect_utils.py (+12/-0)
ironic/tests/unit/drivers/test_redfish.py (+2/-1)
releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml (+17/-0)
releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml (+1/-1)
releasenotes/notes/bug-2036455-edd0e97335579684.yaml (+6/-0)
releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml (+31/-0)
releasenotes/notes/remove-400a563030224c4f.yaml (+9/-0)
releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml (+20/-0)
releasenotes/source/2023.1.rst (+3/-3)
setup.cfg (+1/-0)
zuul.d/project.yaml (+1/-1)
Reviewer Review Type Date Requested Status
Ubuntu OpenStack uploaders Pending
Review via email: mp+450925@code.launchpad.net

Commit message

New upstream snapshot for OpenStack Bobcat.

Description of the change

This is an automated merge proposal.

To post a comment you must log in.
66433f8... by Corey Bryant

New upstream version 23.0.0

1a2385d... by Corey Bryant

Merge tag '23.0.0'

Upstream version 23.0.0

a65cc36... by Corey Bryant

New upstream release for OpenStack Bobcat.

ac0de27... by Corey Bryant

d/control: Align (Build-)Depends with upstream.

109e412... by Corey Bryant

releasing package ironic version 1:23.0.0-0ubuntu1

Revision history for this message
Corey Bryant (corey.bryant) wrote :

Unmerged commits

109e412... by Corey Bryant

releasing package ironic version 1:23.0.0-0ubuntu1

Failed
[FAILED] ubuntu-build:0 (build)
[WAITING] ubuntu-autopkgtest:0 (build)
[WAITING] cloud-archive-build:0 (build)
[WAITING] cloud-archive-autopkgtest:0 (build)
14 of 4 results
ac0de27... by Corey Bryant

d/control: Align (Build-)Depends with upstream.

a65cc36... by Corey Bryant

New upstream release for OpenStack Bobcat.

1a2385d... by Corey Bryant

Merge tag '23.0.0'

Upstream version 23.0.0

66433f8... by Corey Bryant

New upstream version 23.0.0

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/ChangeLog b/ChangeLog
2index f4e38ca..4f21043 100644
3--- a/ChangeLog
4+++ b/ChangeLog
5@@ -1,10 +1,26 @@
6 CHANGES
7 =======
8
9+23.0.0
10+------
11+
12+* RedfishFirmware Interface
13+* inspect\_utils, handle bracketed IPv6 redfish addr
14+* Trivial: attach versions to release series
15+* redfish\_address - wrap\_ipv6 address
16+* Remove most prints for unit tests
17+* [releasenotes] Prelude for 2023.2/bobcat
18+* devstack - configurable ipv6 address mode
19+* Redfish: wait for secure boot state change if it's not immediate
20+* CI: Remove ubuntu focal job
21+* Fix two places that can cause issues under SQLite
22 * [CI] Unblock CI by fixing job regex and non-voting snmp
23+* Update proliantutils driver requirements for bobcat
24+* DB: Only re-query for a lock holder if we cannot lock
25 * Add service steps and initial docs
26 * Log an exception from heartbeat
27 * log the version of the conductor starting
28+* PXE: Remove DHCP option 210 from being set
29 * Fully monkey patch eventlet for consistent behavior
30 * Add missing release mappings for 22.0 and 22.1
31 * Correct bindep.txt entries for bookworm
32@@ -33,6 +49,7 @@ CHANGES
33 * Support sha256/sha512 with the ilo firmware upgrade logic
34 * tox: Remove basepython
35 * Fix typo in deploy\_templates docs
36+* Fix minor grammar issues in the help for new inspector options
37 * Add python3.10 support in testing runtime
38 * DB: Select upon delete for allocations
39 * DB: Streamline allocation interactions
40diff --git a/PKG-INFO b/PKG-INFO
41index d87c849..b71bd51 100644
42--- a/PKG-INFO
43+++ b/PKG-INFO
44@@ -1,11 +1,53 @@
45 Metadata-Version: 2.1
46 Name: ironic
47-Version: 22.1.1.dev36
48+Version: 23.0.0
49 Summary: OpenStack Bare Metal Provisioning
50 Home-page: https://docs.openstack.org/ironic/latest/
51 Author: OpenStack
52 Author-email: openstack-discuss@lists.openstack.org
53 License: UNKNOWN
54+Description: ======
55+ Ironic
56+ ======
57+
58+ Team and repository tags
59+ ------------------------
60+
61+ .. image:: https://governance.openstack.org/tc/badges/ironic.svg
62+ :target: https://governance.openstack.org/tc/reference/tags/index.html
63+
64+ Overview
65+ --------
66+
67+ Ironic consists of an API and plug-ins for managing and provisioning
68+ physical machines in a security-aware and fault-tolerant manner. It can be
69+ used with nova as a hypervisor driver, or standalone service using bifrost.
70+ By default, it will use PXE and IPMI to interact with bare metal machines.
71+ Ironic also supports vendor-specific plug-ins which may implement additional
72+ functionality.
73+
74+ Ironic is distributed under the terms of the Apache License, Version 2.0. The
75+ full terms and conditions of this license are detailed in the LICENSE file.
76+
77+ Project resources
78+ ~~~~~~~~~~~~~~~~~
79+
80+ * Documentation: https://docs.openstack.org/ironic/latest
81+ * Source: https://opendev.org/openstack/ironic
82+ * Bugs: https://bugs.launchpad.net/ironic/+bugs
83+ * Wiki: https://wiki.openstack.org/wiki/Ironic
84+ * APIs: https://docs.openstack.org/api-ref/baremetal/index.html
85+ * Release Notes: https://docs.openstack.org/releasenotes/ironic/
86+ * Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
87+
88+ Project status, bugs, and requests for feature enhancements (RFEs) are tracked
89+ in StoryBoard:
90+ https://storyboard.openstack.org/#!/project/943
91+
92+ For information on how to contribute to ironic, see
93+ https://docs.openstack.org/ironic/latest/contributor
94+
95+
96 Platform: UNKNOWN
97 Classifier: Environment :: OpenStack
98 Classifier: Intended Audience :: Information Technology
99@@ -23,48 +65,3 @@ Provides-Extra: devstack
100 Provides-Extra: guru_meditation_reports
101 Provides-Extra: i18n
102 Provides-Extra: test
103-License-File: LICENSE
104-
105-======
106-Ironic
107-======
108-
109-Team and repository tags
110-------------------------
111-
112-.. image:: https://governance.openstack.org/tc/badges/ironic.svg
113- :target: https://governance.openstack.org/tc/reference/tags/index.html
114-
115-Overview
116---------
117-
118-Ironic consists of an API and plug-ins for managing and provisioning
119-physical machines in a security-aware and fault-tolerant manner. It can be
120-used with nova as a hypervisor driver, or standalone service using bifrost.
121-By default, it will use PXE and IPMI to interact with bare metal machines.
122-Ironic also supports vendor-specific plug-ins which may implement additional
123-functionality.
124-
125-Ironic is distributed under the terms of the Apache License, Version 2.0. The
126-full terms and conditions of this license are detailed in the LICENSE file.
127-
128-Project resources
129-~~~~~~~~~~~~~~~~~
130-
131-* Documentation: https://docs.openstack.org/ironic/latest
132-* Source: https://opendev.org/openstack/ironic
133-* Bugs: https://bugs.launchpad.net/ironic/+bugs
134-* Wiki: https://wiki.openstack.org/wiki/Ironic
135-* APIs: https://docs.openstack.org/api-ref/baremetal/index.html
136-* Release Notes: https://docs.openstack.org/releasenotes/ironic/
137-* Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
138-
139-Project status, bugs, and requests for feature enhancements (RFEs) are tracked
140-in StoryBoard:
141-https://storyboard.openstack.org/#!/project/943
142-
143-For information on how to contribute to ironic, see
144-https://docs.openstack.org/ironic/latest/contributor
145-
146-
147-
148diff --git a/api-ref/source/baremetal-api-v1-node-management.inc b/api-ref/source/baremetal-api-v1-node-management.inc
149index 6ff275e..4d9bdaa 100644
150--- a/api-ref/source/baremetal-api-v1-node-management.inc
151+++ b/api-ref/source/baremetal-api-v1-node-management.inc
152@@ -306,6 +306,10 @@ Change Node Boot Mode
153
154 Request a change to the Node's boot mode.
155
156+.. note::
157+ Depending on the driver and the underlying hardware, changing boot mode may
158+ result in an automatic reboot.
159+
160 .. versionadded:: 1.76
161 A change in node's boot mode can be requested.
162
163@@ -341,6 +345,10 @@ Change Node Secure Boot
164
165 Request a change to the Node's secure boot state.
166
167+.. note::
168+ Depending on the driver and the underlying hardware, changing the secure
169+ boot state may result in an automatic reboot.
170+
171 .. versionadded:: 1.76
172 A change in node's secure boot state can be requested.
173
174diff --git a/api-ref/source/baremetal-api-v1-nodes-firmware.inc b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
175index ed17e0b..15ea992 100644
176--- a/api-ref/source/baremetal-api-v1-nodes-firmware.inc
177+++ b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
178@@ -4,7 +4,7 @@
179 Node Firmware (nodes)
180 =====================
181
182-.. versionadded:: 1.84
183+.. versionadded:: 1.86
184
185 Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
186 all Firmware Components associated with that Node.
187diff --git a/debian/changelog b/debian/changelog
188index 105559b..1783bf3 100644
189--- a/debian/changelog
190+++ b/debian/changelog
191@@ -1,8 +1,10 @@
192-ironic (1:22.1.0+git2023090714.985c7fdf-0ubuntu2) UNRELEASED; urgency=medium
193+ironic (1:23.0.0-0ubuntu1) mantic; urgency=medium
194
195 * d/watch: Drop major version.
196+ * New upstream release for OpenStack Bobcat.
197+ * d/control: Align (Build-)Depends with upstream.
198
199- -- Corey Bryant <corey.bryant@canonical.com> Wed, 04 Oct 2023 10:21:10 -0400
200+ -- Corey Bryant <corey.bryant@canonical.com> Wed, 04 Oct 2023 10:27:14 -0400
201
202 ironic (1:22.1.0+git2023090714.985c7fdf-0ubuntu1) mantic; urgency=medium
203
204diff --git a/debian/control b/debian/control
205index a66689c..1cfb645 100644
206--- a/debian/control
207+++ b/debian/control
208@@ -11,6 +11,21 @@ Build-Depends:
209 python3-setuptools,
210 python3-sphinx (>= 2.0.0),
211 Build-Depends-Indep:
212+*** new test-requirements.txt deps (start)
213+ NOT_FOUND(pyasn1-lextudio)>=1.1.0,
214+ NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
215+ NOT_FOUND(pysnmp-lextudio)>=5.0.0,
216+*** new test-requirements.txt deps (end) ***
217+*** new driver-requirements.txt deps (start)
218+ NOT_FOUND(pyasn1-lextudio)>=1.1.0,
219+ NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
220+ NOT_FOUND(pysnmp-lextudio)>=5.0.0,
221+*** new driver-requirements.txt deps (end) ***
222+*** new doc/requirements.txt deps (start)
223+ NOT_FOUND(pyasn1-lextudio)>=1.1.0,
224+ NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
225+ NOT_FOUND(pysnmp-lextudio)>=5.0.0,
226+*** new doc/requirements.txt deps (end) ***
227 alembic (>= 0.9.6),
228 crudini,
229 python3-alembic (>= 1.4.2),
230diff --git a/devstack/lib/ironic b/devstack/lib/ironic
231index 882b5f6..ffd6601 100644
232--- a/devstack/lib/ironic
233+++ b/devstack/lib/ironic
234@@ -382,6 +382,7 @@ IRONIC_UWSGI=$IRONIC_BIN_DIR/ironic-api-wsgi
235
236 # Lets support IPv6 testing!
237 IRONIC_IP_VERSION=${IRONIC_IP_VERSION:-${IP_VERSION:-4}}
238+IRONIC_IPV6_ADDRESS_MODE=${IRONIC_IPV6_ADDRESS_MODE:-dhcpv6-stateless}
239
240 # Ironic connection info. Note the port must be specified.
241 if is_service_enabled tls-proxy; then
242@@ -1424,11 +1425,9 @@ function configure_ironic_provision_network {
243 --subnet-range $IRONIC_PROVISION_SUBNET_PREFIX \
244 --dns-nameserver 8.8.8.8 -f value -c id)"
245 else
246- # NOTE(TheJulia): Consider changing this to stateful to support UEFI once we move
247- # CI to Ubuntu Jammy as it will support v6 and v4 UEFI firmware driven boot ops.
248 subnet_id="$(openstack --os-cloud $OS_CLOUD subnet create --ip-version 6 \
249- --ipv6-address-mode dhcpv6-stateless \
250- --ipv6-ra-mode dhcpv6-stateless \
251+ --ipv6-address-mode $IRONIC_IPV6_ADDRESS_MODE \
252+ --ipv6-ra-mode $IRONIC_IPV6_ADDRESS_MODE \
253 --dns-nameserver 2001:4860:4860::8888 \
254 ${net_segment_id:+--network-segment $net_segment_id} \
255 $IRONIC_PROVISION_PROVIDER_SUBNET_NAME \
256diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst
257index 6628f48..c4694b8 100644
258--- a/doc/source/admin/drivers/redfish.rst
259+++ b/doc/source/admin/drivers/redfish.rst
260@@ -140,6 +140,22 @@ Two clean and deploy steps are provided for key management:
261 ``management.clear_secure_boot_keys``
262 removes all secure boot keys from the node.
263
264+Rebooting on boot mode changes
265+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
266+
267+While some hardware is able to change the boot mode or the `UEFI secure boot`_
268+state immediately, other models may require a reboot for such a change to be
269+applied. Furthermore, some hardware models cannot change the boot mode and the
270+secure boot state simultaneously, requiring several reboots.
271+
272+The Bare Metal service refreshes the System resource after issuing a PATCH
273+request against it. If the expected change is not observed, the node is
274+rebooted, and the Bare Metal service waits until the change is applied. In the
275+end, the previous power state is restored.
276+
277+This logic makes changing boot configuration more robust at the expense of
278+several reboots in the worst case.
279+
280 Out-Of-Band inspection
281 ======================
282
283diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst
284index efd34d7..e2f0a73 100644
285--- a/doc/source/contributor/webapi-version-history.rst
286+++ b/doc/source/contributor/webapi-version-history.rst
287@@ -15,8 +15,8 @@ based resources, which indicates the current step.
288
289 Adds a ``firmware_interface`` field to the ``/v1/nodes`` resources.
290
291-1.85 (Bobcat)
292--------------
293+1.85 (Bobcat, 22.1)
294+-------------------
295
296 This version adds a new provision state change verb called ``unhold``
297 to be utilized with the new ``provision_state`` values ``clean hold``
298@@ -24,14 +24,14 @@ and ``deploy hold``. The verb instructs Ironic to remove the node
299 from it's present hold and to resume it's prior cleaning or
300 deployment process.
301
302-1.84 (Bobcat)
303--------------
304+1.84 (Bobcat, 22.1)
305+-------------------
306
307 Add callback endpoint for in-band inspection ``/v1/continue_inspection``.
308 This endpoint is not designed to be used by regular users.
309
310-1.83 (Bobcat)
311--------------
312+1.83 (Bobcat, 22.0)
313+-------------------
314
315 This version adds a concept of child nodes through the use of a
316 ``parent_node`` field which can be set on a node.
317@@ -51,8 +51,8 @@ Additionally:
318 - Adds ``GET /v1/nodes?parent_node={node_ident}`` to explicitly request
319 a detailed list of nodes by parent relationship.
320
321-1.82 (Antelope)
322-----------------------
323+1.82 (Antelope, 21.4)
324+---------------------
325
326 This version signifies the addition of node sharding endpoints.
327
328@@ -60,8 +60,8 @@ This version signifies the addition of node sharding endpoints.
329 - Adds support for ``GET /v1/shards`` which returns a list of all shards and
330 the count of nodes assigned to each.
331
332-1.81 (Antelope)
333-----------------------
334+1.81 (Antelope, 21.3)
335+---------------------
336
337 Add endpoint to retrieve introspection data for nodes via the REST API.
338
339diff --git a/driver-requirements.txt b/driver-requirements.txt
340index a26c3cf..b0852d0 100644
341--- a/driver-requirements.txt
342+++ b/driver-requirements.txt
343@@ -4,7 +4,7 @@
344 # python projects they should package as optional dependencies for Ironic.
345
346 # These are available on pypi
347-proliantutils>=2.14.0
348+proliantutils>=2.16.0
349 pysnmp-lextudio>=5.0.0 # BSD
350 pyasn1-lextudio>=1.1.0 # BSD
351 pyasn1-modules-lextudio>=0.2.0 # BSD
352diff --git a/ironic.egg-info/PKG-INFO b/ironic.egg-info/PKG-INFO
353index d87c849..b71bd51 100644
354--- a/ironic.egg-info/PKG-INFO
355+++ b/ironic.egg-info/PKG-INFO
356@@ -1,11 +1,53 @@
357 Metadata-Version: 2.1
358 Name: ironic
359-Version: 22.1.1.dev36
360+Version: 23.0.0
361 Summary: OpenStack Bare Metal Provisioning
362 Home-page: https://docs.openstack.org/ironic/latest/
363 Author: OpenStack
364 Author-email: openstack-discuss@lists.openstack.org
365 License: UNKNOWN
366+Description: ======
367+ Ironic
368+ ======
369+
370+ Team and repository tags
371+ ------------------------
372+
373+ .. image:: https://governance.openstack.org/tc/badges/ironic.svg
374+ :target: https://governance.openstack.org/tc/reference/tags/index.html
375+
376+ Overview
377+ --------
378+
379+ Ironic consists of an API and plug-ins for managing and provisioning
380+ physical machines in a security-aware and fault-tolerant manner. It can be
381+ used with nova as a hypervisor driver, or standalone service using bifrost.
382+ By default, it will use PXE and IPMI to interact with bare metal machines.
383+ Ironic also supports vendor-specific plug-ins which may implement additional
384+ functionality.
385+
386+ Ironic is distributed under the terms of the Apache License, Version 2.0. The
387+ full terms and conditions of this license are detailed in the LICENSE file.
388+
389+ Project resources
390+ ~~~~~~~~~~~~~~~~~
391+
392+ * Documentation: https://docs.openstack.org/ironic/latest
393+ * Source: https://opendev.org/openstack/ironic
394+ * Bugs: https://bugs.launchpad.net/ironic/+bugs
395+ * Wiki: https://wiki.openstack.org/wiki/Ironic
396+ * APIs: https://docs.openstack.org/api-ref/baremetal/index.html
397+ * Release Notes: https://docs.openstack.org/releasenotes/ironic/
398+ * Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
399+
400+ Project status, bugs, and requests for feature enhancements (RFEs) are tracked
401+ in StoryBoard:
402+ https://storyboard.openstack.org/#!/project/943
403+
404+ For information on how to contribute to ironic, see
405+ https://docs.openstack.org/ironic/latest/contributor
406+
407+
408 Platform: UNKNOWN
409 Classifier: Environment :: OpenStack
410 Classifier: Intended Audience :: Information Technology
411@@ -23,48 +65,3 @@ Provides-Extra: devstack
412 Provides-Extra: guru_meditation_reports
413 Provides-Extra: i18n
414 Provides-Extra: test
415-License-File: LICENSE
416-
417-======
418-Ironic
419-======
420-
421-Team and repository tags
422-------------------------
423-
424-.. image:: https://governance.openstack.org/tc/badges/ironic.svg
425- :target: https://governance.openstack.org/tc/reference/tags/index.html
426-
427-Overview
428---------
429-
430-Ironic consists of an API and plug-ins for managing and provisioning
431-physical machines in a security-aware and fault-tolerant manner. It can be
432-used with nova as a hypervisor driver, or standalone service using bifrost.
433-By default, it will use PXE and IPMI to interact with bare metal machines.
434-Ironic also supports vendor-specific plug-ins which may implement additional
435-functionality.
436-
437-Ironic is distributed under the terms of the Apache License, Version 2.0. The
438-full terms and conditions of this license are detailed in the LICENSE file.
439-
440-Project resources
441-~~~~~~~~~~~~~~~~~
442-
443-* Documentation: https://docs.openstack.org/ironic/latest
444-* Source: https://opendev.org/openstack/ironic
445-* Bugs: https://bugs.launchpad.net/ironic/+bugs
446-* Wiki: https://wiki.openstack.org/wiki/Ironic
447-* APIs: https://docs.openstack.org/api-ref/baremetal/index.html
448-* Release Notes: https://docs.openstack.org/releasenotes/ironic/
449-* Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
450-
451-Project status, bugs, and requests for feature enhancements (RFEs) are tracked
452-in StoryBoard:
453-https://storyboard.openstack.org/#!/project/943
454-
455-For information on how to contribute to ironic, see
456-https://docs.openstack.org/ironic/latest/contributor
457-
458-
459-
460diff --git a/ironic.egg-info/SOURCES.txt b/ironic.egg-info/SOURCES.txt
461index e1d1e3c..7da3f81 100644
462--- a/ironic.egg-info/SOURCES.txt
463+++ b/ironic.egg-info/SOURCES.txt
464@@ -695,6 +695,7 @@ ironic/drivers/modules/network/noop.py
465 ironic/drivers/modules/redfish/__init__.py
466 ironic/drivers/modules/redfish/bios.py
467 ironic/drivers/modules/redfish/boot.py
468+ironic/drivers/modules/redfish/firmware.py
469 ironic/drivers/modules/redfish/firmware_utils.py
470 ironic/drivers/modules/redfish/inspect.py
471 ironic/drivers/modules/redfish/management.py
472@@ -975,6 +976,7 @@ ironic/tests/unit/drivers/modules/network/json_samples/network_data.json
473 ironic/tests/unit/drivers/modules/redfish/__init__.py
474 ironic/tests/unit/drivers/modules/redfish/test_bios.py
475 ironic/tests/unit/drivers/modules/redfish/test_boot.py
476+ironic/tests/unit/drivers/modules/redfish/test_firmware.py
477 ironic/tests/unit/drivers/modules/redfish/test_firmware_utils.py
478 ironic/tests/unit/drivers/modules/redfish/test_inspect.py
479 ironic/tests/unit/drivers/modules/redfish/test_management.py
480@@ -1021,6 +1023,7 @@ releasenotes/config.yaml
481 releasenotes/notes/.placeholder
482 releasenotes/notes/18.2-prelude-3c8609692bab70a3.yaml
483 releasenotes/notes/20.1-prelude-612672742f417477.yaml
484+releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
485 releasenotes/notes/5.0-release-afb1fbbe595b6bc8.yaml
486 releasenotes/notes/Add-port-option-support-to-ipmitool-e125d07fe13c53e7.yaml
487 releasenotes/notes/Cleanfail-power-off-13b5fdcc2727866a.yaml
488@@ -1265,6 +1268,7 @@ releasenotes/notes/bug-2007963-idrac-wsman-raid-apply-configuration-792ccf195057
489 releasenotes/notes/bug-2008058-fix-factory-reset-status.yaml-52a6119b46e33b37.yaml
490 releasenotes/notes/bug-2009762-403eac24c4823d2d.yaml
491 releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml
492+releasenotes/notes/bug-2036455-edd0e97335579684.yaml
493 releasenotes/notes/bug-30315-e46eafe5b575f3da.yaml
494 releasenotes/notes/bug-30316-8c53358681e464eb.yaml
495 releasenotes/notes/bug-30317-a972c8d879c98941.yaml
496@@ -1453,6 +1457,7 @@ releasenotes/notes/fast-track-with-cleaning-438225116a11662d.yaml
497 releasenotes/notes/fifteen-0da3cca48dceab8b.yaml
498 releasenotes/notes/file-name-too-long-72265bb3fec704f8.yaml
499 releasenotes/notes/fips-hashlib-bca9beacc2b48fe7.yaml
500+releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
501 releasenotes/notes/fix-agent-clean-up-9a25deb85bc53d9b.yaml
502 releasenotes/notes/fix-agent-ilo-temp-image-cleanup-711429d0e67807ae.yaml
503 releasenotes/notes/fix-anaconda-deploy-interface-bfa2cfca22b04680.yaml
504@@ -2002,6 +2007,7 @@ releasenotes/notes/releasenote-b3b25c13ea1e2844.yaml
505 releasenotes/notes/reloadable-301ec2aa421abf66.yaml
506 releasenotes/notes/rely-on-standalone-ports-supported-8153e1135787828b.yaml
507 releasenotes/notes/removal-pre-allocation-for-oneview-09310a215b3aaf3c.yaml
508+releasenotes/notes/remove-400a563030224c4f.yaml
509 releasenotes/notes/remove-DEPRECATED-options-from-[agent]-7b6cce21b5f52022.yaml
510 releasenotes/notes/remove-agent-heartbeat-timeout-abf8787b8477bae7.yaml
511 releasenotes/notes/remove-agent-passthru-432b18e6c430cee6.yaml
512@@ -2149,6 +2155,7 @@ releasenotes/notes/token-reboot-b48b5981a58a30ae.yaml
513 releasenotes/notes/train-release-59ff1643ec92c10a.yaml
514 releasenotes/notes/transmit-all-ports-b570009d1a008067.yaml
515 releasenotes/notes/type-error-str-6826c53d7e5e1243.yaml
516+releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
517 releasenotes/notes/uefi-first-prepare-e7fa1e2a78b4af99.yaml
518 releasenotes/notes/uefi-grub2-by-default-6b797a9e690d2dd5.yaml
519 releasenotes/notes/uefi-is-now-the-default-562b0d68adc59008.yaml
520diff --git a/ironic.egg-info/entry_points.txt b/ironic.egg-info/entry_points.txt
521index 7d3e573..7bb5a43 100644
522--- a/ironic.egg-info/entry_points.txt
523+++ b/ironic.egg-info/entry_points.txt
524@@ -54,6 +54,7 @@ ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy
525 [ironic.hardware.interfaces.firmware]
526 fake = ironic.drivers.modules.fake:FakeFirmware
527 no-firmware = ironic.drivers.modules.noop:NoFirmware
528+redfish = ironic.drivers.modules.redfish.firmware:RedfishFirmware
529
530 [ironic.hardware.interfaces.inspect]
531 agent = ironic.drivers.modules.inspector:AgentInspect
532diff --git a/ironic.egg-info/pbr.json b/ironic.egg-info/pbr.json
533index 3a310bb..21af2bb 100644
534--- a/ironic.egg-info/pbr.json
535+++ b/ironic.egg-info/pbr.json
536@@ -1 +1 @@
537-{"git_version": "985c7fdf2", "is_release": false}
538\ No newline at end of file
539+{"git_version": "f78f87227", "is_release": true}
540\ No newline at end of file
541diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
542index 10c631d..3a7806e 100644
543--- a/ironic/api/controllers/v1/utils.py
544+++ b/ironic/api/controllers/v1/utils.py
545@@ -2018,6 +2018,6 @@ def allow_continue_inspection_endpoint():
546 def allow_firmware_interface():
547 """Check if we should support firmware interface and endpoints.
548
549- Version 1.84 of the API added support for firmware interface.
550+ Version 1.86 of the API added support for firmware interface.
551 """
552 return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE
553diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py
554index a4fd438..8c61e23 100644
555--- a/ironic/common/pxe_utils.py
556+++ b/ironic/common/pxe_utils.py
557@@ -58,7 +58,6 @@ DHCPV6_BOOTFILE_NAME = '59' # rfc5970
558 # DHCPV6_BOOTFILE_PARAMS = '60' # rfc5970
559 DHCP_TFTP_SERVER_ADDRESS = '150' # rfc5859
560 DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned
561-DHCP_TFTP_PATH_PREFIX = '210' # rfc5071
562 DHCP_SERVER_IP_ADDRESS = '255' # dnsmasq server-ip-address
563
564 DEPLOY_KERNEL_RAMDISK_LABELS = ['deploy_kernel', 'deploy_ramdisk']
565@@ -562,16 +561,6 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
566 else:
567 dhcp_opts.append({'opt_name': boot_file_param,
568 'opt_value': boot_file})
569- # 210 == tftp server path-prefix or tftp root, will be used to find
570- # pxelinux.cfg directory. The pxelinux.0 loader infers this information
571- # from it's own path, but Petitboot needs it to be specified by this
572- # option since it doesn't use pxelinux.0 loader.
573- if not url_boot:
574- # Enforce trailing slash
575- prefix = os.path.join(CONF.pxe.tftp_root, '')
576- dhcp_opts.append(
577- {'opt_name': DHCP_TFTP_PATH_PREFIX,
578- 'opt_value': prefix})
579
580 if not url_boot:
581 dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,
582diff --git a/ironic/common/utils.py b/ironic/common/utils.py
583index 6bae4e7..efd3049 100644
584--- a/ironic/common/utils.py
585+++ b/ironic/common/utils.py
586@@ -578,8 +578,12 @@ def pop_node_nested_field(node, collection, field, default=None):
587
588 def wrap_ipv6(ip):
589 """Wrap the address in square brackets if it's an IPv6 address."""
590- if ipaddress.ip_address(ip).version == 6:
591- return "[%s]" % ip
592+ try:
593+ if ipaddress.ip_address(ip).version == 6:
594+ return "[%s]" % ip
595+ except ValueError:
596+ pass
597+
598 return ip
599
600
601diff --git a/ironic/conductor/cleaning.py b/ironic/conductor/cleaning.py
602index 1e79f97..cd2d999 100644
603--- a/ironic/conductor/cleaning.py
604+++ b/ironic/conductor/cleaning.py
605@@ -85,6 +85,8 @@ def do_node_clean(task, clean_steps=None, disable_ramdisk=False):
606 # Retrieve BIOS config settings for this node
607 utils.node_cache_bios_settings(task, node)
608
609+ # Retrieve Firmware Components for this node if possible
610+ utils.node_cache_firmware_components(task)
611 # Allow the deploy driver to set up the ramdisk again (necessary for
612 # IPA cleaning)
613 try:
614diff --git a/ironic/conductor/steps.py b/ironic/conductor/steps.py
615index 3dfbb2e..dcdfa46 100644
616--- a/ironic/conductor/steps.py
617+++ b/ironic/conductor/steps.py
618@@ -31,9 +31,10 @@ CLEANING_INTERFACE_PRIORITY = {
619 # by which interface is implementing the clean step. The clean step of the
620 # interface with the highest value here, will be executed first in that
621 # case.
622- 'vendor': 6,
623- 'power': 5,
624- 'management': 4,
625+ 'vendor': 7,
626+ 'power': 6,
627+ 'management': 5,
628+ 'firmware': 4,
629 'deploy': 3,
630 'bios': 2,
631 'raid': 1,
632@@ -46,9 +47,10 @@ DEPLOYING_INTERFACE_PRIORITY = {
633 # TODO(rloo): If we think it makes sense to have the interface priorities
634 # the same for cleaning & deploying, replace the two with one e.g.
635 # 'INTERFACE_PRIORITIES'.
636- 'vendor': 6,
637- 'power': 5,
638- 'management': 4,
639+ 'vendor': 7,
640+ 'power': 6,
641+ 'management': 5,
642+ 'firmware': 4,
643 'deploy': 3,
644 'bios': 2,
645 'raid': 1,
646@@ -61,11 +63,12 @@ VERIFYING_INTERFACE_PRIORITY = {
647 # by which interface is implementing the verify step. The verifying step of
648 # the interface with the highest value here, will be executed first in
649 # that case.
650- 'power': 12,
651- 'management': 11,
652- 'boot': 8,
653+ 'power': 13,
654+ 'management': 12,
655+ 'firmware': 11,
656 'inspect': 10,
657 'deploy': 9,
658+ 'boot': 8,
659 'bios': 7,
660 'raid': 6,
661 'vendor': 5,
662diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py
663index 1255c86..e79ce3d 100644
664--- a/ironic/conductor/utils.py
665+++ b/ironic/conductor/utils.py
666@@ -1828,3 +1828,15 @@ def servicing_error_handler(task, logmsg, errmsg=None, traceback=False,
667
668 if set_fail_state and node.provision_state != states.SERVICEFAIL:
669 task.process_event('fail')
670+
671+
672+def node_cache_firmware_components(task):
673+ """Do caching of firmware components if supported by driver"""
674+
675+ try:
676+ LOG.debug('Getting Firmware Components for node %s', task.node.uuid)
677+ task.driver.firmware.validate(task)
678+ task.driver.firmware.cache_firmware_components(task)
679+ except exception.UnsupportedDriverExtension:
680+ LOG.warning('Firmware Components are not supported for node %s, '
681+ 'skipping', task.node.uuid)
682diff --git a/ironic/conf/inspector.py b/ironic/conf/inspector.py
683index 6014fb3..434c863 100644
684--- a/ironic/conf/inspector.py
685+++ b/ironic/conf/inspector.py
686@@ -20,13 +20,15 @@ from ironic.conf import auth
687
688 VALID_ADD_PORTS_VALUES = {
689 'all': _('all MAC addresses'),
690- 'active': _('MAC addresses of NIC\'s with IP addresses'),
691+ 'active': _('MAC addresses of NICs with IP addresses'),
692 'pxe': _('only the MAC address of the PXE NIC'),
693 'disabled': _('do not create any ports'),
694 }
695 VALID_KEEP_PORTS_VALUES = {
696- 'all': _('keep even ports with MAC\'s not present in the inventory'),
697- 'present': _('keep only ports with MAC\'s present in the inventory'),
698+ 'all': _('keep all ports, even ones with MAC addresses that are not '
699+ 'present in the inventory'),
700+ 'present': _('keep only ports with MAC addresses present in '
701+ 'the inventory'),
702 'added': _('keep only ports determined by the add_ports option'),
703 }
704
705diff --git a/ironic/conf/redfish.py b/ironic/conf/redfish.py
706index 68aa961..87a359d 100644
707--- a/ironic/conf/redfish.py
708+++ b/ironic/conf/redfish.py
709@@ -115,6 +115,12 @@ opts = [
710 default=60,
711 help=_('Number of seconds to wait between checking for '
712 'failed raid config tasks')),
713+ cfg.IntOpt('boot_mode_config_timeout',
714+ min=0,
715+ default=900,
716+ help=_('Number of seconds to wait for boot mode or secure '
717+ 'boot status change to take effect after a reboot. '
718+ 'Set to 0 to disable waiting.')),
719 ]
720
721
722diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
723index bf591ca..a6ab523 100644
724--- a/ironic/db/sqlalchemy/api.py
725+++ b/ironic/db/sqlalchemy/api.py
726@@ -387,7 +387,7 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
727 return []
728 if return_base_tuple:
729 # The caller expects a tuple, lets just give it to them.
730- return res
731+ return [tuple(r) for r in res]
732 # Everything is a tuple in a resultset from the unified interface
733 # but for objects, our model expects just object access,
734 # so we extract and return them.
735@@ -585,16 +585,9 @@ class Connection(api.Connection):
736 # If we are not using sorting, or any other query magic,
737 # we could likely just do a query execution and
738 # prepare the tuple responses.
739- results = _paginate_query(models.Node, limit, marker,
740- sort_key, sort_dir, query,
741- return_base_tuple=True)
742- # Need to copy the data to close out the _paginate_query
743- # object.
744- new_result = [tuple([ent for ent in r]) for r in results]
745- # Explicitly free results so we don't hang on to it.
746- del results
747-
748- return new_result
749+ return _paginate_query(models.Node, limit, marker,
750+ sort_key, sort_dir, query,
751+ return_base_tuple=True)
752
753 def get_node_list(self, filters=None, limit=None, marker=None,
754 sort_key=None, sort_dir=None, fields=None):
755@@ -714,14 +707,17 @@ class Connection(api.Connection):
756 values(reservation=tag).
757 execution_options(synchronize_session=False))
758 session.flush()
759- node = self._get_node_reservation(node.id)
760 # NOTE(TheJulia): In SQLAlchemy 2.0 style, we don't
761 # magically get a changed node as they moved from the
762 # many ways to do things to singular ways to do things.
763 if res.rowcount != 1:
764 # Nothing updated and node exists. Must already be
765- # locked.
766- raise exception.NodeLocked(node=node.uuid, host=node.reservation)
767+ # locked. Identify who holds it and log.
768+ if utils.is_ironic_using_sqlite():
769+ lock_holder = CONF.hostname
770+ else:
771+ lock_holder = self._get_node_reservation(node.id).reservation
772+ raise exception.NodeLocked(node=node.uuid, host=lock_holder)
773
774 @oslo_db_api.retry_on_deadlock
775 def reserve_node(self, tag, node_id):
776@@ -1481,7 +1477,8 @@ class Connection(api.Connection):
777 result = session.query(
778 field
779 ).filter(models.Conductor.online.is_(False))
780- return [row[0] for row in result]
781+ result = [row[0] for row in result]
782+ return result
783
784 def get_online_conductors(self):
785 with _session_for_read() as session:
786diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py
787index 010e384..18a5068 100644
788--- a/ironic/drivers/modules/deploy_utils.py
789+++ b/ironic/drivers/modules/deploy_utils.py
790@@ -1536,10 +1536,12 @@ def prepare_agent_boot(task):
791 task.driver.boot.prepare_ramdisk(task, deploy_opts)
792
793
794-def reboot_to_finish_step(task):
795+def reboot_to_finish_step(task, timeout=None):
796 """Reboot the node into IPA to finish a deploy/clean step.
797
798 :param task: a TaskManager instance.
799+ :param timeout: timeout (in seconds) positive integer (> 0) for any
800+ power state. ``None`` indicates to use default timeout.
801 :returns: states.CLEANWAIT if cleaning operation in progress
802 or states.DEPLOYWAIT if deploy operation in progress.
803 """
804@@ -1552,7 +1554,7 @@ def reboot_to_finish_step(task):
805 manager_utils.node_power_action(task, states.POWER_OFF)
806 prepare_agent_boot(task)
807
808- manager_utils.node_power_action(task, states.REBOOT)
809+ manager_utils.node_power_action(task, states.REBOOT, timeout)
810 return get_async_step_return_state(task.node)
811
812
813diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py
814index 625ffbb..0889098 100644
815--- a/ironic/drivers/modules/fake.py
816+++ b/ironic/drivers/modules/fake.py
817@@ -477,6 +477,8 @@ class FakeFirmware(base.FirmwareInterface):
818 'needs to contain a dictionary with name/value pairs'),
819 'required': True}})
820 def update(self, task, settings):
821+ LOG.debug('Calling update clean step with settings %s.',
822+ settings)
823 sleep(CONF.fake.firmware_delay)
824
825 def cache_firmware_components(self, task):
826diff --git a/ironic/drivers/modules/inspect_utils.py b/ironic/drivers/modules/inspect_utils.py
827index 2a66edc..14caa05 100644
828--- a/ironic/drivers/modules/inspect_utils.py
829+++ b/ironic/drivers/modules/inspect_utils.py
830@@ -342,6 +342,9 @@ def _get_bmc_addresses(node):
831 if '//' in address:
832 address = urllib.parse.urlparse(address).hostname
833
834+ # Strip brackets in case used on IPv6 address.
835+ address = address.strip('[').strip(']')
836+
837 try:
838 addrinfo = socket.getaddrinfo(address, None, proto=socket.SOL_TCP)
839 except socket.gaierror as exc:
840diff --git a/ironic/drivers/modules/redfish/firmware.py b/ironic/drivers/modules/redfish/firmware.py
841new file mode 100644
842index 0000000..207c61a
843--- /dev/null
844+++ b/ironic/drivers/modules/redfish/firmware.py
845@@ -0,0 +1,452 @@
846+#
847+# Licensed under the Apache License, Version 2.0 (the "License"); you may
848+# not use this file except in compliance with the License. You may obtain
849+# a copy of the License at
850+#
851+# http://www.apache.org/licenses/LICENSE-2.0
852+#
853+# Unless required by applicable law or agreed to in writing, software
854+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
855+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
856+# License for the specific language governing permissions and limitations
857+# under the License.
858+
859+from urllib.parse import urlparse
860+
861+from ironic_lib import metrics_utils
862+from oslo_log import log
863+from oslo_utils import importutils
864+from oslo_utils import timeutils
865+
866+from ironic.common import exception
867+from ironic.common.i18n import _
868+from ironic.common import states
869+from ironic.conductor import periodics
870+from ironic.conductor import utils as manager_utils
871+from ironic.conf import CONF
872+from ironic.drivers import base
873+from ironic.drivers.modules import deploy_utils
874+from ironic.drivers.modules.redfish import firmware_utils
875+from ironic.drivers.modules.redfish import utils as redfish_utils
876+from ironic import objects
877+
878+LOG = log.getLogger(__name__)
879+
880+METRICS = metrics_utils.get_metrics_logger(__name__)
881+
882+sushy = importutils.try_import('sushy')
883+
884+
885+class RedfishFirmware(base.FirmwareInterface):
886+
887+ _FW_SETTINGS_ARGSINFO = {
888+ 'settings': {
889+ 'description': (
890+ 'A list of dicts with firmware components to be updated'
891+ ),
892+ 'required': True
893+ }
894+ }
895+
896+ def __init__(self):
897+ super(RedfishFirmware, self).__init__()
898+ if sushy is None:
899+ raise exception.DriverLoadError(
900+ driver='redfish',
901+ reason=_("Unable to import the sushy library"))
902+
903+ def get_properties(self):
904+ """Return the properties of the interface.
905+
906+ :returns: dictionary of <property name>:<property description> entries.
907+ """
908+ return redfish_utils.COMMON_PROPERTIES.copy()
909+
910+ def validate(self, task):
911+ """Validates the driver information needed by the redfish driver.
912+
913+ :param task: a TaskManager instance containing the node to act on.
914+ :raises: InvalidParameterValue on malformed parameter(s)
915+ :raises: MissingParameterValue on missing parameter(s)
916+ """
917+ redfish_utils.parse_driver_info(task.node)
918+
919+ def cache_firmware_components(self, task):
920+ """Store or update Firmware Components on the given node.
921+
922+ This method stores Firmware Components to the firmware_information
923+ table during 'cleaning' operation. It will also update the timestamp
924+ of each Firmware Component.
925+
926+ :param task: a TaskManager instance.
927+ :raises: UnsupportedDriverExtension, if the node's driver doesn't
928+ support getting Firmware Components from bare metal.
929+ """
930+
931+ node_id = task.node.id
932+ settings = []
933+ # NOTE(iurygregory): currently we will only retrieve BIOS and BMC
934+ # firmware information trough the redfish system and manager.
935+
936+ system = redfish_utils.get_system(task.node)
937+
938+ bios_fw = {'component': 'bios',
939+ 'current_version': system.bios_version}
940+ settings.append(bios_fw)
941+
942+ # NOTE(iurygregory): normally we only relay on the System to
943+ # perform actions, but to retrieve the BMC Firmware we need to
944+ # access the Manager.
945+ try:
946+ manager = redfish_utils.get_manager(task.node, system)
947+ bmc_fw = {'component': 'bmc',
948+ 'current_version': manager.firmware_version}
949+ settings.append(bmc_fw)
950+ except exception.RedfishError:
951+ LOG.warning('No manager available to retrieve Firmware '
952+ 'from the bmc of node %s', task.node.uuid)
953+
954+ if not settings:
955+ error_msg = (_('Cannot retrieve firmware for node %s.')
956+ % task.node.uuid)
957+ LOG.error(error_msg)
958+ raise exception.UnsupportedDriverExtension(error_msg)
959+
960+ create_list, update_list, nochange_list = (
961+ objects.FirmwareComponentList.sync_firmware_components(
962+ task.context, node_id, settings))
963+
964+ if create_list:
965+ for new_fw in create_list:
966+ new_fw_cmp = objects.FirmwareComponent(
967+ task.context,
968+ node_id=node_id,
969+ component=new_fw['component'],
970+ current_version=new_fw['current_version']
971+ )
972+ new_fw_cmp.create()
973+ if update_list:
974+ for up_fw in update_list:
975+ up_fw_cmp = objects.FirmwareComponent.get(
976+ task.context,
977+ node_id=node_id,
978+ name=up_fw['component']
979+ )
980+ up_fw_cmp.last_version_flashed = up_fw.get('current_version')
981+ up_fw_cmp.current_version = up_fw.get('current_version')
982+ up_fw_cmp.save()
983+
984+ @METRICS.timer('RedfishFirmware.update')
985+ @base.deploy_step(priority=0, argsinfo=_FW_SETTINGS_ARGSINFO)
986+ @base.clean_step(priority=0, abortable=False,
987+ argsinfo=_FW_SETTINGS_ARGSINFO,
988+ requires_ramdisk=True)
989+ @base.cache_firmware_components
990+ def update(self, task, settings):
991+ """Update the Firmware on the node using the settings for components.
992+
993+ :param task: a TaskManager instance.
994+ :param settings: a list of dictionaries, each dictionary contains the
995+ component name and the url that will be used to update the
996+ firmware.
997+ :raises: UnsupportedDriverExtension, if the node's driver doesn't
998+ support update via the interface.
999+ :raises: InvalidParameterValue, if validation of the settings fails.
1000+ :raises: MissingParamterValue, if some required parameters are
1001+ missing.
1002+ :returns: states.CLEANWAIT if Firmware update with the settings is in
1003+ progress asynchronously of None if it is complete.
1004+ """
1005+ node = task.node
1006+
1007+ update_service = redfish_utils.get_update_service(node)
1008+
1009+ LOG.debug('Updating Firmware on node %(node_uuid)s with settings '
1010+ '%(settings)s',
1011+ {'node_uuid': node.uuid, 'settings': settings})
1012+
1013+ self._execute_firmware_update(node, update_service, settings)
1014+
1015+ fw_upd = settings[0]
1016+ wait_interval = fw_upd.get('wait')
1017+
1018+ deploy_utils.set_async_step_flags(
1019+ node,
1020+ reboot=True,
1021+ skip_current_step=True,
1022+ polling=True
1023+ )
1024+
1025+ return deploy_utils.reboot_to_finish_step(task, timeout=wait_interval)
1026+
1027+ def _execute_firmware_update(self, node, update_service, settings):
1028+ """Executes the next firmware update to the node
1029+
1030+ Executes the first firmware update in the settings list to the node.
1031+
1032+ :param node: the node that will have a firmware update executed.
1033+ :param update_service: the sushy firmware update service.
1034+ :param settings: remaining settings for firmware update that needs
1035+ to be executed.
1036+ """
1037+ fw_upd = settings[0]
1038+ component_url, cleanup = self._stage_firmware_file(node, fw_upd)
1039+
1040+ LOG.debug('Applying new firmware %(url)s for %(component)s on node '
1041+ '%(node_uuid)s',
1042+ {'url': fw_upd['url'], 'component': fw_upd['component'],
1043+ 'node_uuid': node.uuid})
1044+
1045+ task_monitor = update_service.simple_update(component_url)
1046+
1047+ fw_upd['task_monitor'] = task_monitor.task_monitor_uri
1048+ node.set_driver_internal_info('redfish_fw_updates', settings)
1049+
1050+ if cleanup:
1051+ fw_clean = node.driver_internal_info.get('firmware_cleanup')
1052+ if not fw_clean:
1053+ fw_clean = [cleanup]
1054+ elif cleanup not in fw_clean:
1055+ fw_clean.append(cleanup)
1056+ node.set_driver_internal_info('firmware_cleanup', fw_clean)
1057+
1058+ def _continue_updates(self, task, update_service, settings):
1059+ """Continues processing the firmware updates
1060+
1061+ Continues to process the firmware updates on the node.
1062+
1063+ Note that the caller must have an exclusive lock on the node.
1064+
1065+ :param task: a TaskManager instance containing the node to act on.
1066+ :param update_service: the sushy firmware update service
1067+ :param settings: the remaining firmware updates to apply
1068+ """
1069+ node = task.node
1070+ fw_upd = settings[0]
1071+ wait_interval = fw_upd.get('wait')
1072+ if wait_interval:
1073+ time_now = str(timeutils.utcnow().isoformat())
1074+ fw_upd['wait_start_time'] = time_now
1075+
1076+ LOG.debug('Waiting at %(time)s for %(seconds)s seconds after '
1077+ '%(component)s firmware update %(url)s '
1078+ 'on node %(node)s',
1079+ {'time': time_now,
1080+ 'seconds': wait_interval,
1081+ 'component': fw_upd['component'],
1082+ 'url': fw_upd['url'],
1083+ 'node': node.uuid})
1084+
1085+ node.set_driver_internal_info('redfish_fw_updates', settings)
1086+ node.save()
1087+ return
1088+
1089+ if len(settings) == 1:
1090+ self._clear_updates(node)
1091+
1092+ LOG.info('Firmware updates completed for node %(node)s',
1093+ {'node': node.uuid})
1094+
1095+ manager_utils.notify_conductor_resume_clean(task)
1096+ else:
1097+ settings.pop(0)
1098+ self._execute_firmware_update(node,
1099+ update_service,
1100+ settings)
1101+ node.save()
1102+ manager_utils.node_power_action(task, states.REBOOT)
1103+
1104+ def _clear_updates(self, node):
1105+ """Clears firmware updates artifacts
1106+
1107+ Clears firmware updates from driver_internal_info and any files
1108+ that were staged.
1109+
1110+ Note that the caller must have an exclusive lock on the node.
1111+
1112+ :param node: the node to clear the firmware updates from
1113+ """
1114+ firmware_utils.cleanup(node)
1115+ node.del_driver_internal_info('redfish_fw_updates')
1116+ node.del_driver_internal_info('firmware_cleanup')
1117+ node.save()
1118+
1119+ @METRICS.timer('RedfishFirmware._query_update_failed')
1120+ @periodics.node_periodic(
1121+ purpose='checking if async update of firmware component failed',
1122+ spacing=CONF.redfish.firmware_update_fail_interval,
1123+ filters={'reserved': False, 'provision_state': states.CLEANFAIL,
1124+ 'maintenance': True},
1125+ predicate_extra_fields=['driver_internal_info'],
1126+ predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
1127+ )
1128+ def _query_update_failed(self, task, manager, context):
1129+
1130+ """Periodic job to check for failed firmware updates."""
1131+ # A firmware update failed. Discard any remaining firmware
1132+ # updates so when the user takes the node out of
1133+ # maintenance mode, pending firmware updates do not
1134+ # automatically continue.
1135+ LOG.error('Update firmware failed for node %(node)s. '
1136+ 'Discarding remaining firmware updates.',
1137+ {'node': task.node.uuid})
1138+
1139+ task.upgrade_lock()
1140+ self._clear_updates(task.node)
1141+
1142+ @METRICS.timer('RedfishFirmware._query_update_status')
1143+ @periodics.node_periodic(
1144+ purpose='checking async update of firmware component',
1145+ spacing=CONF.redfish.firmware_update_fail_interval,
1146+ filters={'reserved': False, 'provision_state': states.CLEANWAIT},
1147+ predicate_extra_fields=['driver_internal_info'],
1148+ predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
1149+ )
1150+ def _query_update_status(self, task, manager, context):
1151+ """Periodic job to check firmware update tasks."""
1152+ self._check_node_redfish_firmware_update(task)
1153+
1154+ @METRICS.timer('RedfishFirmware._check_node_redfish_firmware_update')
1155+ def _check_node_redfish_firmware_update(self, task):
1156+ """Check the progress of running firmware update on a node."""
1157+
1158+ node = task.node
1159+
1160+ settings = node.driver_internal_info['redfish_fw_updates']
1161+ current_update = settings[0]
1162+
1163+ try:
1164+ update_service = redfish_utils.get_update_service(node)
1165+ except exception.RedfishConnectionError as e:
1166+ # If the BMC firmware is being updated, the BMC will be
1167+ # unavailable for some amount of time.
1168+ LOG.warning('Unable to communicate with firmware update service '
1169+ 'on node %(node)s. Will try again on the next poll. '
1170+ 'Error: %(error)s',
1171+ {'node': node.uuid,
1172+ 'error': e})
1173+ return
1174+
1175+ wait_start_time = current_update.get('wait_start_time')
1176+ if wait_start_time:
1177+ wait_start = timeutils.parse_isotime(wait_start_time)
1178+
1179+ elapsed_time = timeutils.utcnow(True) - wait_start
1180+ if elapsed_time.seconds >= current_update['wait']:
1181+ LOG.debug('Finished waiting after firmware update '
1182+ '%(firmware_image)s on node %(node)s. '
1183+ 'Elapsed time: %(seconds)s seconds',
1184+ {'firmware_image': current_update['url'],
1185+ 'node': node.uuid,
1186+ 'seconds': elapsed_time.seconds})
1187+ current_update.pop('wait', None)
1188+ current_update.pop('wait_start_time', None)
1189+
1190+ self._continue_updates(task, update_service, settings)
1191+ else:
1192+ LOG.debug('Continuing to wait after firmware update '
1193+ '%(firmware_image)s on node %(node)s. '
1194+ 'Elapsed time: %(seconds)s seconds',
1195+ {'firmware_image': current_update['url'],
1196+ 'node': node.uuid,
1197+ 'seconds': elapsed_time.seconds})
1198+
1199+ return
1200+
1201+ try:
1202+ task_monitor = redfish_utils.get_task_monitor(
1203+ node, current_update['task_monitor'])
1204+ except exception.RedfishError:
1205+ # The BMC deleted the Task before we could query it
1206+ LOG.warning('Firmware update completed for node %(node)s, '
1207+ 'firmware %(firmware_image)s, but success of the '
1208+ 'update is unknown. Assuming update was successful.',
1209+ {'node': node.uuid,
1210+ 'firmware_image': current_update['url']})
1211+ self._continue_updates(task, update_service, settings)
1212+ return
1213+
1214+ if not task_monitor.is_processing:
1215+ # The last response does not necessarily contain a Task,
1216+ # so get it
1217+ sushy_task = task_monitor.get_task()
1218+
1219+ # Only parse the messages if the BMC did not return parsed
1220+ # messages
1221+ messages = []
1222+ if sushy_task.messages and not sushy_task.messages[0].message:
1223+ sushy_task.parse_messages()
1224+
1225+ if sushy_task.messages is not None:
1226+ messages = [m.message for m in sushy_task.messages]
1227+
1228+ task.upgrade_lock()
1229+ if (sushy_task.task_state == sushy.TASK_STATE_COMPLETED
1230+ and sushy_task.task_status in
1231+ [sushy.HEALTH_OK, sushy.HEALTH_WARNING]):
1232+ LOG.info('Firmware update succeeded for node %(node)s, '
1233+ 'firmware %(firmware_image)s: %(messages)s',
1234+ {'node': node.uuid,
1235+ 'firmware_image': current_update['url'],
1236+ 'messages': ", ".join(messages)})
1237+
1238+ self._continue_updates(task, update_service, settings)
1239+ else:
1240+ error_msg = (_('Firmware update failed for node %(node)s, '
1241+ 'firmware %(firmware_image)s. '
1242+ 'Error: %(errors)s') %
1243+ {'node': node.uuid,
1244+ 'firmware_image': current_update['url'],
1245+ 'errors': ", ".join(messages)})
1246+
1247+ self._clear_updates(node)
1248+ if task.node.clean_step:
1249+ manager_utils.cleaning_error_handler(task, error_msg)
1250+ else:
1251+ manager_utils.deploying_error_handler(task, error_msg)
1252+
1253+ else:
1254+ LOG.debug('Firmware update in progress for node %(node)s, '
1255+ 'firmware %(firmware_image)s.',
1256+ {'node': node.uuid,
1257+ 'firmware_image': current_update['url']})
1258+
1259+ def _stage_firmware_file(self, node, component_update):
1260+
1261+ try:
1262+ url = component_update['url']
1263+ name = component_update['component']
1264+ parsed_url = urlparse(url)
1265+ scheme = parsed_url.scheme.lower()
1266+ source = (CONF.redfish.firmware_source).lower()
1267+
1268+ # Keep it simple, in further processing TLS does not matter
1269+ if scheme == 'https':
1270+ scheme = 'http'
1271+
1272+ # If source and scheme is HTTP, then no staging,
1273+ # returning original location
1274+ if scheme == 'http' and source == scheme:
1275+ LOG.debug('For node %(node)s serving firmware for '
1276+ '%(component)s from original location %(url)s',
1277+ {'node': node.uuid, 'component': name, 'url': url})
1278+ return url, None
1279+
1280+ # If source and scheme is Swift, then not moving, but
1281+ # returning Swift temp URL
1282+ if scheme == 'swift' and source == scheme:
1283+ temp_url = firmware_utils.get_swift_temp_url(parsed_url)
1284+ LOG.debug('For node %(node)s serving original firmware at '
1285+ 'for %(component)s at %(url)s via Swift temporary '
1286+ 'url %(temp_url)s',
1287+ {'node': node.uuid, 'component': name, 'url': url,
1288+ 'temp_url': temp_url})
1289+ return temp_url, None
1290+
1291+ # For remaining, download the image to temporary location
1292+ temp_file = firmware_utils.download_to_temp(node, url)
1293+
1294+ return firmware_utils.stage(node, source, temp_file)
1295+
1296+ except exception.IronicException:
1297+ firmware_utils.cleanup(node)
1298diff --git a/ironic/drivers/modules/redfish/firmware_utils.py b/ironic/drivers/modules/redfish/firmware_utils.py
1299index feeec2d..843597d 100644
1300--- a/ironic/drivers/modules/redfish/firmware_utils.py
1301+++ b/ironic/drivers/modules/redfish/firmware_utils.py
1302@@ -63,6 +63,36 @@ _UPDATE_FIRMWARE_SCHEMA = {
1303 "additionalProperties": False
1304 }
1305 }
1306+
1307+_FIRMWARE_INTERFACE_UPDATE_SCHEMA = {
1308+ "$schema": "http://json-schema.org/schema#",
1309+ "title": "update_firmware clean step schema",
1310+ "type": "array",
1311+ # list of firmware update images
1312+ "items": {
1313+ "type": "object",
1314+ "required": ["component", "url"],
1315+ "properties": {
1316+ "component": {
1317+ "description": "name of the firmware component to be updated",
1318+ "type": "string",
1319+ "minLenght": 1
1320+ },
1321+ "url": {
1322+ "description": "URL for firmware file",
1323+ "type": "string",
1324+ "minLength": 1
1325+ },
1326+ "wait": {
1327+ "description": "optional wait time for firmware update",
1328+ "type": "integer",
1329+ "minimum": 1
1330+ }
1331+ },
1332+ "additionalProperties": False
1333+ }
1334+}
1335+
1336 _FIRMWARE_SUBDIR = 'firmware'
1337
1338
1339@@ -80,6 +110,20 @@ def validate_update_firmware_args(firmware_images):
1340 % {'firmware_images': firmware_images, 'err': err})
1341
1342
1343+def validate_firmware_interface_update_args(settings):
1344+ """Validate ``update`` step input argument
1345+
1346+ :param settings: args to validate.
1347+ :raises: InvalidParameterValue When argument is not valid
1348+ """
1349+ try:
1350+ jsonschema.validate(settings, _FIRMWARE_INTERFACE_UPDATE_SCHEMA)
1351+ except jsonschema.ValidationError as err:
1352+ raise exception.InvalidParameterValue(
1353+ _('Invalid firmware update %(settings)s. Errors: %(err)s')
1354+ % {'settings': settings, 'err': err})
1355+
1356+
1357 def get_swift_temp_url(parsed_url):
1358 """Gets Swift temporary URL
1359
1360diff --git a/ironic/drivers/modules/redfish/management.py b/ironic/drivers/modules/redfish/management.py
1361index 7f7dbe7..d0b8045 100644
1362--- a/ironic/drivers/modules/redfish/management.py
1363+++ b/ironic/drivers/modules/redfish/management.py
1364@@ -14,6 +14,7 @@
1365 # under the License.
1366
1367 import collections
1368+import time
1369 from urllib.parse import urlparse
1370
1371 from ironic_lib import metrics_utils
1372@@ -44,6 +45,8 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
1373
1374 sushy = importutils.try_import('sushy')
1375
1376+BOOT_MODE_CONFIG_INTERVAL = 15
1377+
1378 if sushy:
1379 BOOT_DEVICE_MAP = {
1380 sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE,
1381@@ -327,9 +330,13 @@ class RedfishManagement(base.ManagementInterface):
1382 """
1383 system = redfish_utils.get_system(task.node)
1384
1385+ # NOTE(dtantsur): check the readability of the current mode before
1386+ # modifying anything. I suspect it can become None transiently after
1387+ # the update, while we need to know if it is supported *at all*.
1388+ get_mode_unsupported = (system.boot.get('mode') is None)
1389+
1390 try:
1391 system.set_system_boot_options(mode=BOOT_MODE_MAP_REV[mode])
1392-
1393 except sushy.exceptions.SushyError as e:
1394 error_msg = (_('Setting boot mode to %(mode)s '
1395 'failed for node %(node)s. '
1396@@ -342,7 +349,7 @@ class RedfishManagement(base.ManagementInterface):
1397 # getting or setting the boot mode. When setting failed and the
1398 # mode attribute is missing from the boot field, raising
1399 # UnsupportedDriverExtension will allow the deploy to continue.
1400- if system.boot.get('mode') is None:
1401+ if get_mode_unsupported:
1402 LOG.info(_('Attempt to set boot mode on node %(node)s '
1403 'failed to set boot mode as the node does not '
1404 'appear to support overriding the boot mode. '
1405@@ -352,6 +359,66 @@ class RedfishManagement(base.ManagementInterface):
1406 driver=task.node.driver, extension='set_boot_mode')
1407 raise exception.RedfishError(error=error_msg)
1408
1409+ # NOTE(dtantsur): this case is rather hypothetical, but in our own
1410+ # emulator, it's possible that mode is constantly set to None, while
1411+ # the request to change the mode succeeds.
1412+ if get_mode_unsupported:
1413+ LOG.warning('The request to set boot mode for node %(node)s to '
1414+ '%(value)s has succeeded, but the current mode is '
1415+ 'not known. Skipping reboot and assuming '
1416+ 'the operation has succeeded.',
1417+ {'node': task.node.uuid, 'value': mode})
1418+ return
1419+
1420+ self._wait_for_boot_mode(task, system, mode)
1421+ LOG.info('Boot mode for node %(node)s has been set to '
1422+ '%(value)s', {'node': task.node.uuid, 'value': mode})
1423+
1424+ def _wait_for_boot_mode(self, task, system, mode):
1425+ system.refresh(force=True)
1426+
1427+ # NOTE(dtantsur/janders): at least Dell machines change boot mode via
1428+ # a BIOS configuration job. A reboot is needed to apply it.
1429+ if system.boot.get('mode') == BOOT_MODE_MAP_REV[mode]:
1430+ LOG.debug('Node %(node)s is already configured with requested '
1431+ 'boot mode %(new_value)s.',
1432+ {'node': task.node.uuid,
1433+ 'new_value': BOOT_MODE_MAP_REV[mode]})
1434+ return
1435+
1436+ LOG.info('Rebooting node %(node)s to change boot mode from '
1437+ '%(old_value)s to %(new_value)s',
1438+ {'node': task.node.uuid,
1439+ 'old_value': system.boot.get('mode'),
1440+ 'new_value': BOOT_MODE_MAP_REV[mode]})
1441+
1442+ old_power_state = task.driver.power.get_power_state(task)
1443+ manager_utils.node_power_action(task, states.REBOOT)
1444+
1445+ if CONF.redfish.boot_mode_config_timeout:
1446+ threshold = time.time() + CONF.redfish.boot_mode_config_timeout
1447+ while (time.time() <= threshold
1448+ and system.boot.get('mode') != BOOT_MODE_MAP_REV[mode]):
1449+ LOG.debug('Still waiting for boot mode of node %(node)s '
1450+ 'to become %(value)s, current is %(current)s',
1451+ {'node': task.node.uuid,
1452+ 'value': BOOT_MODE_MAP_REV[mode],
1453+ 'current': system.boot.get('mode')})
1454+ time.sleep(BOOT_MODE_CONFIG_INTERVAL)
1455+ system.refresh(force=True)
1456+
1457+ if system.boot.get('mode') != BOOT_MODE_MAP_REV[mode]:
1458+ msg = (_('Timeout reached while waiting for boot mode of '
1459+ 'node %(node)s to become %(value)s, '
1460+ 'current is %(current)s')
1461+ % {'node': task.node.uuid,
1462+ 'value': BOOT_MODE_MAP_REV[mode],
1463+ 'current': system.boot.get('mode')})
1464+ LOG.error(msg)
1465+ raise exception.RedfishError(error=msg)
1466+
1467+ manager_utils.node_power_action(task, old_power_state)
1468+
1469 def get_boot_mode(self, task):
1470 """Get the current boot mode for a node.
1471
1472@@ -1142,9 +1209,45 @@ class RedfishManagement(base.ManagementInterface):
1473 % {'node': task.node.uuid, 'value': state, 'exc': exc})
1474 LOG.error(msg)
1475 raise exception.RedfishError(error=msg)
1476- else:
1477- LOG.info('Secure boot state for node %(node)s has been set to '
1478- '%(value)s', {'node': task.node.uuid, 'value': state})
1479+
1480+ self._wait_for_secure_boot(task, sb, state)
1481+ LOG.info('Secure boot state for node %(node)s has been set to '
1482+ '%(value)s', {'node': task.node.uuid, 'value': state})
1483+
1484+ def _wait_for_secure_boot(self, task, sb, state):
1485+ # NOTE(dtantsur): at least Dell machines change secure boot status via
1486+ # a BIOS configuration job. A reboot is needed to apply it.
1487+ sb.refresh(force=True)
1488+ if sb.enabled == state:
1489+ return
1490+
1491+ LOG.info('Rebooting node %(node)s to change secure boot state to '
1492+ '%(value)s', {'node': task.node.uuid, 'value': state})
1493+
1494+ old_power_state = task.driver.power.get_power_state(task)
1495+ manager_utils.node_power_action(task, states.REBOOT)
1496+
1497+ if CONF.redfish.boot_mode_config_timeout:
1498+ threshold = time.time() + CONF.redfish.boot_mode_config_timeout
1499+ while time.time() <= threshold and sb.enabled != state:
1500+ LOG.debug(
1501+ 'Still waiting for secure boot state of node %(node)s '
1502+ 'to become %(value)s, current is %(current)s',
1503+ {'node': task.node.uuid, 'value': state,
1504+ 'current': sb.enabled})
1505+ time.sleep(BOOT_MODE_CONFIG_INTERVAL)
1506+ sb.refresh(force=True)
1507+
1508+ if sb.enabled != state:
1509+ msg = (_('Timeout reached while waiting for secure boot state '
1510+ 'of node %(node)s to become %(state)s, '
1511+ 'current is %(current)s')
1512+ % {'node': task.node.uuid, 'state': state,
1513+ 'current': sb.enabled})
1514+ LOG.error(msg)
1515+ raise exception.RedfishError(error=msg)
1516+
1517+ manager_utils.node_power_action(task, old_power_state)
1518
1519 def _reset_keys(self, task, reset_type):
1520 system = redfish_utils.get_system(task.node)
1521diff --git a/ironic/drivers/modules/redfish/utils.py b/ironic/drivers/modules/redfish/utils.py
1522index e85e2ec..4182c84 100644
1523--- a/ironic/drivers/modules/redfish/utils.py
1524+++ b/ironic/drivers/modules/redfish/utils.py
1525@@ -28,6 +28,7 @@ import tenacity
1526
1527 from ironic.common import exception
1528 from ironic.common.i18n import _
1529+from ironic.common import utils
1530 from ironic.conf import CONF
1531
1532 sushy = importutils.try_import('sushy')
1533@@ -97,7 +98,7 @@ def parse_driver_info(node):
1534 'info': missing_info})
1535
1536 # Validate the Redfish address
1537- address = driver_info['redfish_address']
1538+ address = utils.wrap_ipv6(driver_info['redfish_address'])
1539 try:
1540 parsed = rfc3986.uri_reference(address)
1541 except TypeError:
1542@@ -474,3 +475,39 @@ def wait_until_get_system_ready(node):
1543 driver_info = parse_driver_info(node)
1544 system_id = driver_info['system_id']
1545 return _get_system(driver_info, system_id)
1546+
1547+
1548+def get_manager(node, system, manager_id=None):
1549+ """Get a node's manager.
1550+
1551+ :param system: a Sushy system object
1552+ :param manager_id: the id of the manager
1553+ :return: a sushy Manager
1554+ :raises: RedfishError when the System doesn't have Managers associated
1555+ """
1556+
1557+ try:
1558+ sushy_manager = None
1559+ available_managers = system.managers
1560+ if available_managers:
1561+ if manager_id is None:
1562+ sushy_manager = available_managers[0]
1563+ else:
1564+ for manager in available_managers:
1565+ if manager.identity == manager_id:
1566+ sushy_manager = manager
1567+ if sushy_manager is None:
1568+ raise Exception("Couldn't find any Sushy Manager")
1569+ return sushy_manager
1570+ except sushy.exceptions.MissingAttributeError as e:
1571+ LOG.error('Redfish Managers for node %(node)s are not associated '
1572+ 'with system %(system)s. Error %(error)s',
1573+ {'system': system.identity,
1574+ 'node': node.uuid, 'error': e})
1575+ raise exception.RedfishError(error=e)
1576+ except Exception as exc:
1577+ LOG.error('Redfish Manager was not found for '
1578+ 'node %(node)s under system %(system)s. Error %(error)s',
1579+ {'system': system.identity,
1580+ 'node': node.uuid, 'error': exc})
1581+ raise exception.RedfishError(error=exc)
1582diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py
1583index 3852cd3..094119e 100644
1584--- a/ironic/drivers/redfish.py
1585+++ b/ironic/drivers/redfish.py
1586@@ -21,6 +21,7 @@ from ironic.drivers.modules import noop_mgmt
1587 from ironic.drivers.modules import pxe
1588 from ironic.drivers.modules.redfish import bios as redfish_bios
1589 from ironic.drivers.modules.redfish import boot as redfish_boot
1590+from ironic.drivers.modules.redfish import firmware as redfish_firmware
1591 from ironic.drivers.modules.redfish import inspect as redfish_inspect
1592 from ironic.drivers.modules.redfish import management as redfish_mgmt
1593 from ironic.drivers.modules.redfish import power as redfish_power
1594@@ -69,3 +70,7 @@ class RedfishHardware(generic.GenericHardware):
1595 def supported_raid_interfaces(self):
1596 """List of supported raid interfaces."""
1597 return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]
1598+
1599+ @property
1600+ def supported_firmware_interfaces(self):
1601+ return [redfish_firmware.RedfishFirmware, noop.NoFirmware]
1602diff --git a/ironic/objects/firmware.py b/ironic/objects/firmware.py
1603index 2a0f5f2..d30bc16 100644
1604--- a/ironic/objects/firmware.py
1605+++ b/ironic/objects/firmware.py
1606@@ -145,11 +145,15 @@ class FirmwareComponentList(base.IronicObjectListBase, base.IronicObject):
1607 for cmp in components:
1608 if cmp['component'] in current_components_dict:
1609 values = current_components_dict[cmp['component']]
1610-
1611- cv_changed = cmp['current_version'] \
1612- != values.get('current_version')
1613- lvf_changed = cmp['last_version_flashed'] \
1614- != values.get('last_version_flashed')
1615+ if values.get('last_version_flashed') is None:
1616+ lvf_changed = False
1617+ cv_changed = cmp['current_version'] \
1618+ != values.get('current_version')
1619+ else:
1620+ lvf_changed = cmp['current_version'] \
1621+ != values.get('last_version_flashed')
1622+ cv_changed = cmp['current_version'] \
1623+ != values.get('current_version')
1624
1625 if cv_changed or lvf_changed:
1626 update_list.append(cmp)
1627diff --git a/ironic/tests/unit/api/base.py b/ironic/tests/unit/api/base.py
1628index 5f53e30..7a0638f 100644
1629--- a/ironic/tests/unit/api/base.py
1630+++ b/ironic/tests/unit/api/base.py
1631@@ -104,7 +104,6 @@ class BaseApiTest(db_base.DbTestCase):
1632 :param path_prefix: prefix of the url path
1633 """
1634 full_path = path_prefix + path
1635- print('%s: %s %s' % (method.upper(), full_path, params))
1636 response = getattr(self.app, "%s_json" % method)(
1637 str(full_path),
1638 params=params,
1639@@ -113,7 +112,7 @@ class BaseApiTest(db_base.DbTestCase):
1640 extra_environ=extra_environ,
1641 expect_errors=expect_errors
1642 )
1643- print('GOT:%s' % response)
1644+ print(method.upper(), full_path, "WITH", params, "GOT", str(response))
1645 return response
1646
1647 def put_json(self, path, params, expect_errors=False, headers=None,
1648@@ -187,13 +186,12 @@ class BaseApiTest(db_base.DbTestCase):
1649 :param path_prefix: prefix of the url path
1650 """
1651 full_path = path_prefix + path
1652- print('DELETE: %s' % (full_path))
1653 response = self.app.delete(str(full_path),
1654 headers=headers,
1655 status=status,
1656 extra_environ=extra_environ,
1657 expect_errors=expect_errors)
1658- print('GOT:%s' % response)
1659+ print("DELETE", full_path, "GOT", str(response))
1660 return response
1661
1662 def get_json(self, path, expect_errors=False, headers=None,
1663@@ -225,15 +223,14 @@ class BaseApiTest(db_base.DbTestCase):
1664 all_params.update(params)
1665 if q:
1666 all_params.update(query_params)
1667- print('GET: %s %r' % (full_path, all_params))
1668 response = self.app.get(full_path,
1669 params=all_params,
1670 headers=headers,
1671 extra_environ=extra_environ,
1672 expect_errors=expect_errors)
1673+ print("GET", full_path, "WITH", params, "GOT", str(response))
1674 if not expect_errors:
1675 response = response.json
1676- print('GOT:%s' % response)
1677 return response
1678
1679 def validate_link(self, link, bookmark=False, headers=None):
1680diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py
1681index 31885f4..088fe3c 100644
1682--- a/ironic/tests/unit/api/controllers/v1/test_port.py
1683+++ b/ironic/tests/unit/api/controllers/v1/test_port.py
1684@@ -1114,7 +1114,6 @@ class TestListPortsByShard(test_api_base.BaseApiTest):
1685
1686 res = self.get_json('/ports?shard=shard1,shard2', headers=self.headers)
1687 self.assertEqual(2, len(res['ports']))
1688- print(res['ports'][0])
1689 self.assertNotEqual(res['ports'][0]['address'], bad_shard_address)
1690 self.assertNotEqual(res['ports'][1]['address'], bad_shard_address)
1691
1692diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py
1693index 0481843..ded1d0b 100644
1694--- a/ironic/tests/unit/api/test_acl.py
1695+++ b/ironic/tests/unit/api/test_acl.py
1696@@ -102,7 +102,6 @@ class TestACLBase(base.BaseApiTest):
1697 # in troubleshooting ACL testing. This is a pattern
1698 # followed in API unit testing in ironic, and
1699 # really does help.
1700- print('API ACL Testing Path %s %s' % (method, path))
1701 if headers:
1702 for k, v in headers.items():
1703 rheaders[k] = v.format(**self.format_data)
1704@@ -185,8 +184,6 @@ class TestACLBase(base.BaseApiTest):
1705 if assert_dict_contains:
1706 for k, v in assert_dict_contains.items():
1707 self.assertIn(k, response)
1708- print(k)
1709- print(v)
1710 if str(v) == "None":
1711 # Compare since the variable loaded from the
1712 # json ends up being null in json or None.
1713@@ -219,13 +216,6 @@ class TestACLBase(base.BaseApiTest):
1714 # a filtered view in these cases.
1715 self.assertEqual(0, len(items))
1716
1717- # NOTE(TheJulia): API tests in Ironic tend to have a pattern
1718- # to print request and response data to aid in development
1719- # and troubleshooting. As such the prints should remain,
1720- # at least until we are through primary development of the
1721- # this test suite.
1722- print('ACL Test GOT %s' % response)
1723-
1724
1725 @ddt.ddt
1726 class TestRBACBasic(TestACLBase):
1727diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py
1728index c3595a1..fe238df 100644
1729--- a/ironic/tests/unit/common/test_neutron.py
1730+++ b/ironic/tests/unit/common/test_neutron.py
1731@@ -678,7 +678,6 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
1732
1733 network_data = neutron.get_neutron_port_data('port1', 'vif1')
1734
1735- print(network_data)
1736 expected_port = {
1737 'id': 'port1',
1738 'type': 'vif',
1739diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py
1740index 190bdfb..a57441f 100644
1741--- a/ironic/tests/unit/common/test_pxe_utils.py
1742+++ b/ironic/tests/unit/common/test_pxe_utils.py
1743@@ -895,9 +895,6 @@ class TestPXEUtils(db_base.DbTestCase):
1744 expected_info = [{'opt_name': '67',
1745 'opt_value': bootfile,
1746 'ip_version': ip_version},
1747- {'opt_name': '210',
1748- 'opt_value': '/tftp-path/',
1749- 'ip_version': ip_version},
1750 {'opt_name': '66',
1751 'opt_value': '192.0.2.1',
1752 'ip_version': ip_version},
1753@@ -2165,8 +2162,6 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
1754 if iso_boot:
1755 self.node.instance_info = {'boot_iso': 'http://test.url/file.iso'}
1756 self.node.save()
1757- print(expected_options)
1758- print(image_info)
1759 iso_url = os.path.join(http_url, self.node.uuid, 'boot_iso')
1760 expected_options.update(
1761 {
1762diff --git a/ironic/tests/unit/common/test_utils.py b/ironic/tests/unit/common/test_utils.py
1763index 8d8e4aa..96c6612 100644
1764--- a/ironic/tests/unit/common/test_utils.py
1765+++ b/ironic/tests/unit/common/test_utils.py
1766@@ -321,6 +321,12 @@ class GenericUtilsTestCase(base.TestCase):
1767 self.assertFalse(utils.is_fips_enabled())
1768 m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')
1769
1770+ def test_wrap_ipv6(self):
1771+ self.assertEqual('[2001:DB8::1]', utils.wrap_ipv6('2001:DB8::1'))
1772+ self.assertEqual('example.com', utils.wrap_ipv6('example.com'))
1773+ self.assertEqual('192.168.24.1', utils.wrap_ipv6('192.168.24.1'))
1774+ self.assertEqual('[2001:DB8::1]', utils.wrap_ipv6('[2001:DB8::1]'))
1775+
1776
1777 class TempFilesTestCase(base.TestCase):
1778
1779diff --git a/ironic/tests/unit/conductor/test_steps.py b/ironic/tests/unit/conductor/test_steps.py
1780index 09d267a..64317c1 100644
1781--- a/ironic/tests/unit/conductor/test_steps.py
1782+++ b/ironic/tests/unit/conductor/test_steps.py
1783@@ -585,6 +585,11 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
1784 'abortable': False, 'argsinfo': None, 'interface': 'vendor',
1785 'priority': 1, 'requires_ramdisk': True,
1786 'step': 'log_passthrough'}
1787+ self.firmware_step = {
1788+ 'abortable': False, 'argsinfo': {}, 'interface': 'firmware',
1789+ 'priority': 0, 'requires_ramdisk': True,
1790+ 'step': 'update'
1791+ }
1792
1793 # Automated cleaning should be executed in this order
1794 self.clean_steps = [self.deploy_erase, self.power_update,
1795@@ -595,6 +600,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
1796 'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
1797 'arg2': {'description': 'desc2'}}}
1798
1799+ @mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
1800+ lambda self, taks: [])
1801 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
1802 lambda self, task: [])
1803 @mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps',
1804@@ -619,6 +626,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
1805
1806 self.assertEqual(self.clean_steps, steps)
1807
1808+ @mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
1809+ lambda self, task: [])
1810 @mock.patch('ironic.drivers.modules.fake.FakeVendorB.get_clean_steps',
1811 lambda self, task: [])
1812 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
1813diff --git a/ironic/tests/unit/db/test_nodes.py b/ironic/tests/unit/db/test_nodes.py
1814index 3dee718..b10d367 100644
1815--- a/ironic/tests/unit/db/test_nodes.py
1816+++ b/ironic/tests/unit/db/test_nodes.py
1817@@ -15,6 +15,7 @@
1818
1819 """Tests for manipulating Nodes via the DB API"""
1820
1821+import copy
1822 import datetime
1823 from unittest import mock
1824
1825@@ -29,6 +30,7 @@ from sqlalchemy.orm import exc as sa_orm_exc
1826
1827 from ironic.common import exception
1828 from ironic.common import states
1829+from ironic.common import utils as common_utils
1830 from ironic.db.sqlalchemy import api as dbapi
1831 from ironic.db.sqlalchemy.api import Connection as db_conn
1832 from ironic.db.sqlalchemy.models import NodeInventory
1833@@ -1037,6 +1039,39 @@ class DbNodeTestCase(base.DbTestCase):
1834 res = self.dbapi.get_node_by_uuid(uuid)
1835 self.assertEqual(r1, res.reservation)
1836
1837+ def test_reserve_node_reads_reservation_once_sqlite(self):
1838+ node = utils.create_test_node()
1839+ uuid = node.uuid
1840+
1841+ r1 = 'fake-reservation'
1842+
1843+ with mock.patch.object(db_conn, '_get_node_reservation',
1844+ autospec=True) as mock_get_res:
1845+ mock_get_res.return_value = node
1846+ self.dbapi.reserve_node(r1, uuid)
1847+ mock_get_res.assert_called_once_with(mock.ANY, node.uuid)
1848+
1849+ @mock.patch.object(common_utils, 'is_ironic_using_sqlite', autospec=True)
1850+ def test_reserve_node_reads_reservation_twice(self, is_sqlite_mock):
1851+ # Ensure we re-query for who holds the reservation *when* lock fails
1852+ # to trigger.
1853+ node = utils.create_test_node()
1854+ uuid = node.uuid
1855+ is_sqlite_mock.return_value = False
1856+ r1 = 'fake-reservation'
1857+ self.dbapi.update_node(node.id, {'reservation': r1})
1858+ locked_node = copy.copy(node)
1859+ locked_node.reservation = r1
1860+ with mock.patch.object(db_conn, '_get_node_reservation',
1861+ autospec=True) as mock_get_res:
1862+ mock_get_res.side_effect = [node, locked_node]
1863+ self.assertRaisesRegex(exception.NodeLocked,
1864+ 'locked by host fake-reservation',
1865+ self.dbapi.reserve_node, r1, uuid)
1866+ mock_get_res.assert_has_calls([
1867+ mock.call(mock.ANY, node.uuid),
1868+ mock.call(mock.ANY, node.id)])
1869+
1870 def test_release_reservation(self):
1871 node = utils.create_test_node()
1872 uuid = node.uuid
1873diff --git a/ironic/tests/unit/drivers/modules/ilo/test_management.py b/ironic/tests/unit/drivers/modules/ilo/test_management.py
1874index f087c4d..9379954 100644
1875--- a/ironic/tests/unit/drivers/modules/ilo/test_management.py
1876+++ b/ironic/tests/unit/drivers/modules/ilo/test_management.py
1877@@ -1681,7 +1681,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1878 ilo_mock_object.do_disk_erase.assert_called_once_with(
1879 'HDD', 'overwrite')
1880 self.assertEqual(states.CLEANWAIT, result)
1881- mock_power.assert_called_once_with(task, states.REBOOT)
1882+ mock_power.assert_called_once_with(task, states.REBOOT, None)
1883
1884 @mock.patch.object(deploy_utils, 'build_agent_options',
1885 autospec=True)
1886@@ -1712,7 +1712,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1887 ilo_mock_object.do_disk_erase.assert_called_once_with(
1888 'SSD', 'block')
1889 self.assertEqual(states.CLEANWAIT, result)
1890- mock_power.assert_called_once_with(task, states.REBOOT)
1891+ mock_power.assert_called_once_with(task, states.REBOOT, None)
1892
1893 @mock.patch.object(deploy_utils, 'build_agent_options',
1894 autospec=True)
1895@@ -1746,7 +1746,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1896 ilo_mock_object.do_disk_erase.assert_called_once_with(
1897 'SSD', 'block')
1898 self.assertEqual(states.CLEANWAIT, result)
1899- mock_power.assert_called_once_with(task, states.REBOOT)
1900+ mock_power.assert_called_once_with(task, states.REBOOT, None)
1901
1902 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)
1903 @mock.patch.object(ilo_management.Ilo5Management,
1904@@ -1802,7 +1802,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1905 ilo_mock_object.do_disk_erase.assert_called_once_with(
1906 'HDD', 'zero')
1907 self.assertEqual(states.CLEANWAIT, result)
1908- mock_power.assert_called_once_with(task, states.REBOOT)
1909+ mock_power.assert_called_once_with(task, states.REBOOT, None)
1910
1911 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)
1912 @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True)
1913diff --git a/ironic/tests/unit/drivers/modules/ilo/test_raid.py b/ironic/tests/unit/drivers/modules/ilo/test_raid.py
1914index fcb0314..c102a5d 100644
1915--- a/ironic/tests/unit/drivers/modules/ilo/test_raid.py
1916+++ b/ironic/tests/unit/drivers/modules/ilo/test_raid.py
1917@@ -84,7 +84,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
1918 self.assertFalse(
1919 task.node.driver_internal_info.get(
1920 'skip_current_deploy_step'))
1921- mock_reboot.assert_called_once_with(task, states.REBOOT)
1922+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1923
1924 def test__prepare_for_read_raid_create_raid_cleaning(self):
1925 self.node.clean_step = {'step': 'create_configuration',
1926@@ -122,7 +122,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
1927 self.assertEqual(
1928 task.node.driver_internal_info.get(
1929 'skip_current_deploy_step'), False)
1930- mock_reboot.assert_called_once_with(task, states.REBOOT)
1931+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1932
1933 def test__prepare_for_read_raid_delete_raid_cleaning(self):
1934 self.node.clean_step = {'step': 'create_configuration',
1935diff --git a/ironic/tests/unit/drivers/modules/redfish/test_bios.py b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
1936index 1d30730..bccaad0 100644
1937--- a/ironic/tests/unit/drivers/modules/redfish/test_bios.py
1938+++ b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
1939@@ -203,10 +203,11 @@ class RedfishBiosTestCase(db_base.DbTestCase):
1940 if fast_track:
1941 mock_power_action.assert_has_calls([
1942 mock.call(task, states.POWER_OFF),
1943- mock.call(task, states.REBOOT),
1944+ mock.call(task, states.REBOOT, None),
1945 ])
1946 else:
1947- mock_power_action.assert_called_once_with(task, states.REBOOT)
1948+ mock_power_action.assert_called_once_with(
1949+ task, states.REBOOT, None)
1950 if step == 'factory_reset':
1951 bios.reset_bios.assert_called_once()
1952 if step == 'apply_configuration':
1953diff --git a/ironic/tests/unit/drivers/modules/redfish/test_firmware.py b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py
1954new file mode 100644
1955index 0000000..c3e984c
1956--- /dev/null
1957+++ b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py
1958@@ -0,0 +1,40 @@
1959+#
1960+# Licensed under the Apache License, Version 2.0 (the "License"); you may
1961+# not use this file except in compliance with the License. You may obtain
1962+# a copy of the License at
1963+#
1964+# http://www.apache.org/licenses/LICENSE-2.0
1965+#
1966+# Unless required by applicable law or agreed to in writing, software
1967+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
1968+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1969+# License for the specific language governing permissions and limitations
1970+# under the License.
1971+
1972+from unittest import mock
1973+
1974+from oslo_utils import importutils
1975+
1976+from ironic.tests.unit.db import base as db_base
1977+from ironic.tests.unit.db import utils as db_utils
1978+from ironic.tests.unit.objects import utils as obj_utils
1979+
1980+sushy = importutils.try_import('sushy')
1981+
1982+INFO_DICT = db_utils.get_test_redfish_info()
1983+
1984+
1985+@mock.patch('oslo_utils.eventletutils.EventletEvent.wait',
1986+ lambda *args, **kwargs: None)
1987+class RedfishFirmwareTestCase(db_base.DbTestCase):
1988+
1989+ def setUp(self):
1990+ super(RedfishFirmwareTestCase, self).setUp()
1991+ self.config(enabled_bios_interfaces=['redfish'],
1992+ enabled_hardware_types=['redfish'],
1993+ enabled_power_interfaces=['redfish'],
1994+ enabled_boot_interfaces=['redfish-virtual-media'],
1995+ enabled_management_interfaces=['redfish'],
1996+ enabled_firmware_interfaces=['redfish'])
1997+ self.node = obj_utils.create_test_node(
1998+ self.context, driver='redfish', driver_info=INFO_DICT)
1999diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py
2000index 1d752d9..3a9dca2 100644
2001--- a/ironic/tests/unit/drivers/modules/redfish/test_management.py
2002+++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py
2003@@ -28,10 +28,12 @@ from ironic.common import states
2004 from ironic.conductor import task_manager
2005 from ironic.conductor import utils as manager_utils
2006 from ironic.conf import CONF
2007+from ironic.drivers.modules import boot_mode_utils
2008 from ironic.drivers.modules import deploy_utils
2009 from ironic.drivers.modules.redfish import boot as redfish_boot
2010 from ironic.drivers.modules.redfish import firmware_utils
2011 from ironic.drivers.modules.redfish import management as redfish_mgmt
2012+from ironic.drivers.modules.redfish import power as redfish_power
2013 from ironic.drivers.modules.redfish import utils as redfish_utils
2014 from ironic.tests.unit.db import base as db_base
2015 from ironic.tests.unit.db import utils as db_utils
2016@@ -268,8 +270,10 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2017 boot_devices.PXE,
2018 task.node.driver_internal_info['redfish_boot_device'])
2019
2020+ @mock.patch.object(boot_mode_utils, 'sync_boot_mode', autospec=True)
2021 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2022- def test_set_boot_device_persistency_vendor(self, mock_get_system):
2023+ def test_set_boot_device_persistency_vendor(self, mock_get_system,
2024+ mock_sync_boot_mode):
2025 fake_system = mock_get_system.return_value
2026 fake_system.boot.get.return_value = \
2027 sushy.BOOT_SOURCE_ENABLED_CONTINUOUS
2028@@ -288,18 +292,16 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2029 shared=False) as task:
2030 task.driver.management.set_boot_device(
2031 task, boot_devices.PXE, persistent=True)
2032+ fake_system.set_system_boot_options.assert_called_once_with(
2033+ sushy.BOOT_SOURCE_TARGET_PXE, enabled=expected)
2034 if vendor == 'SuperMicro':
2035- fake_system.set_system_boot_options.assert_has_calls(
2036- [mock.call(sushy.BOOT_SOURCE_TARGET_PXE,
2037- enabled=expected),
2038- mock.call(mode=sushy.BOOT_SOURCE_MODE_UEFI)])
2039+ mock_sync_boot_mode.assert_called_once_with(task)
2040 else:
2041- fake_system.set_system_boot_options.assert_has_calls(
2042- [mock.call(sushy.BOOT_SOURCE_TARGET_PXE,
2043- enabled=expected)])
2044+ mock_sync_boot_mode.assert_not_called()
2045
2046 # Reset mocks
2047 fake_system.set_system_boot_options.reset_mock()
2048+ mock_sync_boot_mode.reset_mock()
2049 mock_get_system.reset_mock()
2050
2051 def test_restore_boot_device(self):
2052@@ -391,34 +393,46 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2053 self.assertEqual(list(redfish_mgmt.BOOT_MODE_MAP_REV),
2054 supported_boot_modes)
2055
2056+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_boot_mode',
2057+ autospec=True)
2058 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2059- def test_set_boot_mode(self, mock_get_system):
2060+ def test_set_boot_mode(self, mock_get_system, mock_wait):
2061 boot_attribute = {
2062 'target': sushy.BOOT_SOURCE_TARGET_PXE,
2063 'enabled': sushy.BOOT_SOURCE_ENABLED_CONTINUOUS,
2064- 'mode': sushy.BOOT_SOURCE_MODE_BIOS,
2065+ 'mode': None,
2066 }
2067 fake_system = mock.Mock(boot=boot_attribute)
2068- fake_system = mock.Mock()
2069 mock_get_system.return_value = fake_system
2070 with task_manager.acquire(self.context, self.node.uuid,
2071 shared=False) as task:
2072 expected_values = [
2073- (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS),
2074- (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI)
2075+ (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS,
2076+ sushy.BOOT_SOURCE_MODE_UEFI),
2077+ (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI,
2078+ sushy.BOOT_SOURCE_MODE_BIOS),
2079+ (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS, None),
2080+ (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI, None),
2081 ]
2082
2083- for mode, expected in expected_values:
2084+ for mode, expected, current in expected_values:
2085+ boot_attribute['mode'] = current
2086 task.driver.management.set_boot_mode(task, mode=mode)
2087
2088 # Asserts
2089 fake_system.set_system_boot_options.assert_called_once_with(
2090 mode=expected)
2091 mock_get_system.assert_called_once_with(task.node)
2092+ if current is not None:
2093+ mock_wait.assert_called_once_with(task.driver.management,
2094+ task, fake_system, mode)
2095+ else:
2096+ mock_wait.assert_not_called()
2097
2098 # Reset mocks
2099 fake_system.set_system_boot_options.reset_mock()
2100 mock_get_system.reset_mock()
2101+ mock_wait.reset_mock()
2102
2103 @mock.patch.object(sushy, 'Sushy', autospec=True)
2104 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2105@@ -462,6 +476,44 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2106 mode=sushy.BOOT_SOURCE_MODE_UEFI)
2107 mock_get_system.assert_called_once_with(task.node)
2108
2109+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2110+ def test_wait_for_boot_mode_immediate(self, mock_power):
2111+ fake_system = mock.Mock(spec=['boot', 'refresh'],
2112+ boot={'mode': sushy.BOOT_SOURCE_MODE_UEFI})
2113+ with task_manager.acquire(self.context, self.node.uuid,
2114+ shared=False) as task:
2115+ task.driver.management._wait_for_boot_mode(
2116+ task, fake_system, boot_modes.UEFI)
2117+ fake_system.refresh.assert_called_once_with(force=True)
2118+ mock_power.assert_not_called()
2119+
2120+ @mock.patch('time.sleep', lambda _: None)
2121+ @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
2122+ autospec=True)
2123+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2124+ def test_wait_for_boot_mode(self, mock_power, mock_get_power):
2125+ attempts = 3
2126+
2127+ def side_effect(force):
2128+ nonlocal attempts
2129+ attempts -= 1
2130+ if attempts <= 0:
2131+ fake_system.boot['mode'] = sushy.BOOT_SOURCE_MODE_UEFI
2132+
2133+ fake_system = mock.Mock(spec=['boot', 'refresh'],
2134+ boot={'mode': sushy.BOOT_SOURCE_MODE_BIOS})
2135+ fake_system.refresh.side_effect = side_effect
2136+ with task_manager.acquire(self.context, self.node.uuid,
2137+ shared=False) as task:
2138+ task.driver.management._wait_for_boot_mode(
2139+ task, fake_system, boot_modes.UEFI)
2140+ fake_system.refresh.assert_called_with(force=True)
2141+ self.assertEqual(3, fake_system.refresh.call_count)
2142+ mock_power.assert_has_calls([
2143+ mock.call(task, states.REBOOT),
2144+ mock.call(task, mock_get_power.return_value),
2145+ ])
2146+
2147 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2148 def test_get_boot_mode(self, mock_get_system):
2149 boot_attribute = {
2150@@ -865,7 +917,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2151 task.node, reboot=True, skip_current_step=True, polling=True)
2152 mock_get_async_step_return_state.assert_called_once_with(
2153 task.node)
2154- mock_node_power_action.assert_called_once_with(task, states.REBOOT)
2155+ mock_node_power_action.assert_called_once_with(
2156+ task, states.REBOOT, None)
2157
2158 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
2159 autospec=True)
2160@@ -919,7 +972,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2161 task.node, reboot=True, skip_current_step=True, polling=True)
2162 mock_get_async_step_return_state.assert_called_once_with(
2163 task.node)
2164- mock_node_power_action.assert_called_once_with(task, states.REBOOT)
2165+ mock_node_power_action.assert_called_once_with(
2166+ task, states.REBOOT, None)
2167
2168 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
2169 autospec=True)
2170@@ -979,7 +1033,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2171 task.node, reboot=True, skip_current_step=True, polling=True)
2172 mock_get_async_step_return_state.assert_called_once_with(
2173 task.node)
2174- mock_node_power_action.assert_called_once_with(task, states.REBOOT)
2175+ mock_node_power_action.assert_called_once_with(
2176+ task, states.REBOOT, None)
2177
2178 def test_update_firmware_invalid_args(self):
2179 with task_manager.acquire(self.context, self.node.uuid,
2180@@ -1462,61 +1517,84 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2181 task.driver.management.get_secure_boot_state,
2182 task)
2183
2184+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
2185+ autospec=True)
2186 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2187- def test_set_secure_boot_state(self, mock_get_system):
2188+ def test_set_secure_boot_state(self, mock_get_system, mock_wait):
2189 fake_system = mock_get_system.return_value
2190 fake_system.secure_boot.enabled = False
2191 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}
2192 with task_manager.acquire(self.context, self.node.uuid,
2193- shared=True) as task:
2194+ shared=False) as task:
2195 task.driver.management.set_secure_boot_state(task, True)
2196 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
2197+ mock_wait.assert_called_once_with(task.driver.management,
2198+ task, fake_system.secure_boot,
2199+ True)
2200
2201+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
2202+ autospec=True)
2203 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2204- def test_set_secure_boot_state_boot_mode_unknown(self, mock_get_system):
2205+ def test_set_secure_boot_state_boot_mode_unknown(self, mock_get_system,
2206+ mock_wait):
2207 fake_system = mock_get_system.return_value
2208 fake_system.secure_boot.enabled = False
2209 fake_system.boot = {}
2210 with task_manager.acquire(self.context, self.node.uuid,
2211- shared=True) as task:
2212+ shared=False) as task:
2213 task.driver.management.set_secure_boot_state(task, True)
2214 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
2215+ mock_wait.assert_called_once_with(task.driver.management,
2216+ task, fake_system.secure_boot,
2217+ True)
2218
2219+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
2220+ autospec=True)
2221 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2222- def test_set_secure_boot_state_boot_mode_no_change(self, mock_get_system):
2223+ def test_set_secure_boot_state_boot_mode_no_change(self, mock_get_system,
2224+ mock_wait):
2225 fake_system = mock_get_system.return_value
2226 fake_system.secure_boot.enabled = False
2227 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}
2228 with task_manager.acquire(self.context, self.node.uuid,
2229- shared=True) as task:
2230+ shared=False) as task:
2231 task.driver.management.set_secure_boot_state(task, False)
2232- self.assertFalse(fake_system.secure_boot.set_enabled.called)
2233+ fake_system.secure_boot.set_enabled.assert_not_called()
2234+ mock_wait.assert_not_called()
2235
2236+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
2237+ autospec=True)
2238 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2239- def test_set_secure_boot_state_boot_mode_incorrect(self, mock_get_system):
2240+ def test_set_secure_boot_state_boot_mode_incorrect(self, mock_get_system,
2241+ mock_wait):
2242 fake_system = mock_get_system.return_value
2243 fake_system.secure_boot.enabled = False
2244 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}
2245 with task_manager.acquire(self.context, self.node.uuid,
2246- shared=True) as task:
2247+ shared=False) as task:
2248 self.assertRaisesRegex(
2249 exception.RedfishError, 'requires UEFI',
2250 task.driver.management.set_secure_boot_state, task, True)
2251- self.assertFalse(fake_system.secure_boot.set_enabled.called)
2252+ fake_system.secure_boot.set_enabled.assert_not_called()
2253+ mock_wait.assert_not_called()
2254
2255+ @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
2256+ autospec=True)
2257 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2258- def test_set_secure_boot_state_boot_mode_fails(self, mock_get_system):
2259+ def test_set_secure_boot_state_boot_mode_fails(self, mock_get_system,
2260+ mock_wait):
2261 fake_system = mock_get_system.return_value
2262 fake_system.secure_boot.enabled = False
2263 fake_system.secure_boot.set_enabled.side_effect = \
2264 sushy.exceptions.SushyError
2265 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}
2266 with task_manager.acquire(self.context, self.node.uuid,
2267- shared=True) as task:
2268+ shared=False) as task:
2269 self.assertRaisesRegex(
2270 exception.RedfishError, 'Failed to set secure boot',
2271 task.driver.management.set_secure_boot_state, task, True)
2272 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
2273+ mock_wait.assert_not_called()
2274
2275 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2276 def test_set_secure_boot_state_not_implemented(self, mock_get_system):
2277@@ -1528,11 +1606,76 @@ class RedfishManagementTestCase(db_base.DbTestCase):
2278
2279 mock_get_system.return_value = NoSecureBoot()
2280 with task_manager.acquire(self.context, self.node.uuid,
2281- shared=True) as task:
2282+ shared=False) as task:
2283 self.assertRaises(exception.UnsupportedDriverExtension,
2284 task.driver.management.set_secure_boot_state,
2285 task, True)
2286
2287+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2288+ def test_wait_for_secure_boot_immediate(self, mock_power):
2289+ fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=True)
2290+ with task_manager.acquire(self.context, self.node.uuid,
2291+ shared=False) as task:
2292+ task.driver.management._wait_for_secure_boot(task, fake_sb, True)
2293+ fake_sb.refresh.assert_called_once_with(force=True)
2294+ mock_power.assert_not_called()
2295+
2296+ @mock.patch('time.sleep', lambda _: None)
2297+ @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
2298+ autospec=True)
2299+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2300+ def test_wait_for_secure_boot(self, mock_power, mock_get_power):
2301+ attempts = 3
2302+
2303+ def side_effect(force):
2304+ nonlocal attempts
2305+ attempts -= 1
2306+ if attempts <= 0:
2307+ fake_sb.enabled = True
2308+
2309+ fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
2310+ fake_sb.refresh.side_effect = side_effect
2311+ with task_manager.acquire(self.context, self.node.uuid,
2312+ shared=False) as task:
2313+ task.driver.management._wait_for_secure_boot(task, fake_sb, True)
2314+ fake_sb.refresh.assert_called_with(force=True)
2315+ self.assertEqual(3, fake_sb.refresh.call_count)
2316+ mock_power.assert_has_calls([
2317+ mock.call(task, states.REBOOT),
2318+ mock.call(task, mock_get_power.return_value),
2319+ ])
2320+
2321+ @mock.patch.object(redfish_mgmt, 'BOOT_MODE_CONFIG_INTERVAL', 0.1)
2322+ @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
2323+ autospec=True)
2324+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2325+ def test_wait_for_secure_boot_timeout(self, mock_power, mock_get_power):
2326+ CONF.set_override('boot_mode_config_timeout', 1, group='redfish')
2327+ fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
2328+ with task_manager.acquire(self.context, self.node.uuid,
2329+ shared=False) as task:
2330+ self.assertRaisesRegex(
2331+ exception.RedfishError, 'Timeout reached',
2332+ task.driver.management._wait_for_secure_boot,
2333+ task, fake_sb, True)
2334+ fake_sb.refresh.assert_called_with(force=True)
2335+ mock_power.assert_called_once_with(task, states.REBOOT)
2336+
2337+ @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
2338+ autospec=True)
2339+ @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
2340+ def test_wait_for_secure_boot_no_wait(self, mock_power, mock_get_power):
2341+ CONF.set_override('boot_mode_config_timeout', 0, group='redfish')
2342+ fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
2343+ with task_manager.acquire(self.context, self.node.uuid,
2344+ shared=False) as task:
2345+ task.driver.management._wait_for_secure_boot(task, fake_sb, True)
2346+ fake_sb.refresh.assert_called_once_with(force=True)
2347+ mock_power.assert_has_calls([
2348+ mock.call(task, states.REBOOT),
2349+ mock.call(task, mock_get_power.return_value),
2350+ ])
2351+
2352 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
2353 def test_reset_secure_boot_to_default(self, mock_get_system):
2354 with task_manager.acquire(self.context, self.node.uuid,
2355diff --git a/ironic/tests/unit/drivers/modules/redfish/test_raid.py b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
2356index 843be73..e651d8b 100644
2357--- a/ironic/tests/unit/drivers/modules/redfish/test_raid.py
2358+++ b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
2359@@ -406,7 +406,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
2360 task.node, reboot=True, skip_current_step=True, polling=True)
2361 mock_get_async_step_return_state.assert_called_once_with(
2362 task.node)
2363- mock_node_power_action.assert_called_once_with(task, states.REBOOT)
2364+ mock_node_power_action.assert_called_once_with(
2365+ task, states.REBOOT, None)
2366 mock_build_agent_options.assert_called_once_with(task.node)
2367 self.assertEqual(mock_prepare_ramdisk.call_count, 1)
2368 # Async operation, raid_config shouldn't be updated yet
2369@@ -1123,7 +1124,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
2370 task.node, reboot=True, skip_current_step=True, polling=True)
2371 mock_get_async_step_return_state.assert_called_once_with(
2372 task.node)
2373- mock_node_power_action.assert_called_once_with(task, states.REBOOT)
2374+ mock_node_power_action.assert_called_once_with(
2375+ task, states.REBOOT, None)
2376 mock_build_agent_options.assert_called_once_with(task.node)
2377 self.assertEqual(mock_prepare_ramdisk.call_count, 1)
2378 self.assertEqual(
2379diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
2380index 01b7089..5e91c15 100644
2381--- a/ironic/tests/unit/drivers/modules/redfish/test_utils.py
2382+++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
2383@@ -168,6 +168,14 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
2384 response = redfish_utils.parse_driver_info(self.node)
2385 self.assertEqual(self.parsed_driver_info, response)
2386
2387+ def test_parse_driver_info_default_scheme_ipv6_brackets_added(self):
2388+ test_redfish_address = '2001:DB8::1'
2389+ self.node.driver_info['redfish_address'] = test_redfish_address
2390+ response = redfish_utils.parse_driver_info(self.node)
2391+ self.parsed_driver_info['address'] = ("https://[%s]"
2392+ % test_redfish_address)
2393+ self.assertEqual(self.parsed_driver_info, response)
2394+
2395 def test_get_task_monitor(self):
2396 redfish_utils._get_connection = mock.Mock()
2397 fake_monitor = mock.Mock()
2398diff --git a/ironic/tests/unit/drivers/modules/test_agent_base.py b/ironic/tests/unit/drivers/modules/test_agent_base.py
2399index c589d52..6d285cb 100644
2400--- a/ironic/tests/unit/drivers/modules/test_agent_base.py
2401+++ b/ironic/tests/unit/drivers/modules/test_agent_base.py
2402@@ -1593,7 +1593,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2403 agent_base._post_step_reboot(task, 'clean')
2404 self.assertTrue(mock_build_opt.called)
2405 self.assertTrue(mock_prepare.called)
2406- mock_reboot.assert_called_once_with(task, states.REBOOT)
2407+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2408 self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])
2409 self.assertNotIn('agent_secret_token',
2410 task.node.driver_internal_info)
2411@@ -1612,7 +1612,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2412 agent_base._post_step_reboot(task, 'deploy')
2413 self.assertTrue(mock_build_opt.called)
2414 self.assertTrue(mock_prepare.called)
2415- mock_reboot.assert_called_once_with(task, states.REBOOT)
2416+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2417 self.assertTrue(
2418 task.node.driver_internal_info['deployment_reboot'])
2419 self.assertNotIn('agent_secret_token',
2420@@ -1633,7 +1633,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2421 agent_base._post_step_reboot(task, 'clean')
2422 self.assertTrue(mock_build_opt.called)
2423 self.assertTrue(mock_prepare.called)
2424- mock_reboot.assert_called_once_with(task, states.REBOOT)
2425+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2426 self.assertIn('agent_secret_token',
2427 task.node.driver_internal_info)
2428
2429@@ -1649,7 +1649,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2430 with task_manager.acquire(self.context, self.node['uuid'],
2431 shared=False) as task:
2432 agent_base._post_step_reboot(task, 'clean')
2433- mock_reboot.assert_called_once_with(task, states.REBOOT)
2434+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2435 mock_handler.assert_called_once_with(task, mock.ANY,
2436 traceback=True)
2437 self.assertNotIn('cleaning_reboot',
2438@@ -1667,7 +1667,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2439 with task_manager.acquire(self.context, self.node['uuid'],
2440 shared=False) as task:
2441 agent_base._post_step_reboot(task, 'deploy')
2442- mock_reboot.assert_called_once_with(task, states.REBOOT)
2443+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2444 mock_handler.assert_called_once_with(task, mock.ANY,
2445 traceback=True)
2446 self.assertNotIn('deployment_reboot',
2447@@ -1686,7 +1686,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
2448 with task_manager.acquire(self.context, self.node['uuid'],
2449 shared=False) as task:
2450 agent_base._post_step_reboot(task, 'service')
2451- mock_reboot.assert_called_once_with(task, states.REBOOT)
2452+ mock_reboot.assert_called_once_with(task, states.REBOOT, None)
2453 mock_handler.assert_called_once_with(task, mock.ANY,
2454 traceback=True)
2455 self.assertNotIn('servicing_reboot',
2456@@ -1829,7 +1829,7 @@ class ContinueCleaningTest(AgentDeployMixinBaseTest):
2457 with task_manager.acquire(self.context, self.node['uuid'],
2458 shared=False) as task:
2459 self.deploy.continue_cleaning(task)
2460- reboot_mock.assert_called_once_with(task, states.REBOOT)
2461+ reboot_mock.assert_called_once_with(task, states.REBOOT, None)
2462
2463 @mock.patch.object(cleaning, 'continue_node_clean', autospec=True)
2464 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
2465@@ -2147,7 +2147,7 @@ class ContinueServiceTest(AgentDeployMixinBaseTest):
2466 with task_manager.acquire(self.context, self.node['uuid'],
2467 shared=False) as task:
2468 self.deploy.continue_servicing(task)
2469- reboot_mock.assert_called_once_with(task, states.REBOOT)
2470+ reboot_mock.assert_called_once_with(task, states.REBOOT, None)
2471
2472 @mock.patch.object(servicing, 'continue_node_service', autospec=True)
2473 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
2474diff --git a/ironic/tests/unit/drivers/modules/test_inspect_utils.py b/ironic/tests/unit/drivers/modules/test_inspect_utils.py
2475index f6f5ab0..a7b2ac2 100644
2476--- a/ironic/tests/unit/drivers/modules/test_inspect_utils.py
2477+++ b/ironic/tests/unit/drivers/modules/test_inspect_utils.py
2478@@ -442,6 +442,12 @@ class GetBMCAddressesTestCase(db_base.DbTestCase):
2479 driver_info={'redfish_address': 'https://192.0.2.1/redfish'})
2480 self.assertEqual({'192.0.2.1'}, utils._get_bmc_addresses(node))
2481
2482+ def test_normal_ipv6_as_url(self):
2483+ node = obj_utils.create_test_node(
2484+ self.context,
2485+ driver_info={'redfish_address': 'https://[2001:db8::42]/redfish'})
2486+ self.assertEqual({'2001:db8::42'}, utils._get_bmc_addresses(node))
2487+
2488 @mock.patch.object(socket, 'getaddrinfo', autospec=True)
2489 def test_resolved_host(self, mock_getaddrinfo):
2490 mock_getaddrinfo.return_value = [
2491@@ -474,6 +480,12 @@ class GetBMCAddressesTestCase(db_base.DbTestCase):
2492 mock_getaddrinfo.assert_called_once_with(
2493 'example.com', None, proto=socket.SOL_TCP)
2494
2495+ def test_redfish_bmc_address_ipv6_brackets_no_scheme(self):
2496+ node = obj_utils.create_test_node(
2497+ self.context,
2498+ driver_info={'redfish_address': '[2001:db8::42]'})
2499+ self.assertEqual({'2001:db8::42'}, utils._get_bmc_addresses(node))
2500+
2501
2502 class LookupCacheTestCase(db_base.DbTestCase):
2503
2504diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py
2505index b692c61..be34fbf 100644
2506--- a/ironic/tests/unit/drivers/test_redfish.py
2507+++ b/ironic/tests/unit/drivers/test_redfish.py
2508@@ -33,7 +33,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
2509 enabled_boot_interfaces=['redfish-virtual-media'],
2510 enabled_management_interfaces=['redfish'],
2511 enabled_inspect_interfaces=['redfish'],
2512- enabled_bios_interfaces=['redfish'])
2513+ enabled_bios_interfaces=['redfish'],
2514+ enabled_firmware_interfaces=['redfish'])
2515
2516 def test_default_interfaces(self):
2517 node = obj_utils.create_test_node(self.context, driver='redfish')
2518diff --git a/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml b/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
2519new file mode 100644
2520index 0000000..1c27277
2521--- /dev/null
2522+++ b/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
2523@@ -0,0 +1,17 @@
2524+---
2525+prelude: |
2526+ Ironic is proud to announce the release of 23.0, the capstone release of a
2527+ six month OpenStack 2023.2 (Bobcat) cycle.
2528+
2529+ Our focus this cycle has been on improving the ability for operators to
2530+ secure and service their Ironic nodes. There are also, as always, a myriad
2531+ of quality of life fixes, including improvements to sqlite support,
2532+ and graceful shutdown of conductors.
2533+
2534+ We hope the latest release of Ironic serves you well!
2535+upgrade: |
2536+ Ironic 23.0 is part of the OpenStack 2023.2 (Bobcat) release. This a
2537+ non-SLURP release, meaning users of a 2023.1 (Antelope) cycle Ironic release
2538+ can upgrade directly to the release accompanying 2024.1 (Caracal) when
2539+ available. For more information, please visit
2540+ `Release Cadence Adjustment <https://governance.openstack.org/tc/resolutions/20220210-release-cadence-adjustment.html?_ga=2.126966605.1175089434.1694620440-1981816456.1685478379>`_.
2541diff --git a/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml b/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
2542index 5eec68b..521e581 100644
2543--- a/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
2544+++ b/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
2545@@ -11,7 +11,7 @@ features:
2546 to start over.
2547 - |
2548 Adds the ability to send an ``unhold`` provision state verb utilizing
2549- API version *1.84*.
2550+ API version *1.85*.
2551 other:
2552 - |
2553 Fixes the generated state machine diagram and updates it to match the
2554diff --git a/releasenotes/notes/bug-2036455-edd0e97335579684.yaml b/releasenotes/notes/bug-2036455-edd0e97335579684.yaml
2555new file mode 100644
2556index 0000000..72a000b
2557--- /dev/null
2558+++ b/releasenotes/notes/bug-2036455-edd0e97335579684.yaml
2559@@ -0,0 +1,6 @@
2560+---
2561+fixes:
2562+ - |
2563+ Fixes an issue where inspection would fail if an IPv6 address wrapped in
2564+ brackets is used for the redfish BMC address. See bug:
2565+ `2036455 <https://bugs.launchpad.net/ironic/+bug/2036455>`_.
2566diff --git a/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml b/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
2567new file mode 100644
2568index 0000000..06964b1
2569--- /dev/null
2570+++ b/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
2571@@ -0,0 +1,31 @@
2572+---
2573+features:
2574+ - |
2575+ Adds Firmware Interface support to ironic, we would like to receive
2576+ feedback since this is a new feature we introduced and we as a developer
2577+ community have limited hardware access, reach out to us in case of any
2578+ unexpected behavior.
2579+
2580+ - Adds version 1.86 of the Bare Metal API, which includes:
2581+
2582+ * List all firmware components of a node via the
2583+ ``GET /v1/nodes/{node_ident}/firmware`` API.
2584+
2585+ * The ``firmware_interface`` field of the node resource. A firmware
2586+ interface can be set when creating or updating a node.
2587+
2588+ * The ``default_firmware_interface`` and ``enabled_firmware_interface``
2589+ fields of the driver resource.
2590+
2591+ - Adds new configuration options for the firmware interface feature:
2592+
2593+ * Firmware interfaces are enabled via
2594+ ``[DEFAULT]/enabled_firmware_interfaces``. A default firmware
2595+ interface to use when creating or updating nodes can be specified with
2596+ ``[DEFAULT]/default_firmware_interface``.
2597+
2598+ - Available interfaces: ``redfish``, ``no-firmware`` and ``fake``.
2599+
2600+ - Support to update firmware of BIOS and BMC via ``update`` step, can be
2601+ done via clean or deploy steps, the node should be using the
2602+ ``redfish`` driver and set the ``firmware_interface``.
2603diff --git a/releasenotes/notes/remove-400a563030224c4f.yaml b/releasenotes/notes/remove-400a563030224c4f.yaml
2604new file mode 100644
2605index 0000000..1ce686a
2606--- /dev/null
2607+++ b/releasenotes/notes/remove-400a563030224c4f.yaml
2608@@ -0,0 +1,9 @@
2609+---
2610+other:
2611+ - |
2612+ While investigating `bug 2033430 <https://bugs.launchpad.net/ironic/+bug/2033430>`_
2613+ we discovered we were emitting DHCP option 210 *only* with OVN, and never
2614+ emitted it with dnsmasq because it was not being set previously. Our
2615+ internal notes also indicated this was for PXELinux support, but was
2616+ never actually needed. As it was excess, and redundant configuration
2617+ being provided to Neutron, it has been removed.
2618diff --git a/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml b/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
2619new file mode 100644
2620index 0000000..44ae3c6
2621--- /dev/null
2622+++ b/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
2623@@ -0,0 +1,20 @@
2624+---
2625+fixes:
2626+ - |
2627+ While updating boot mode or secure boot state in the Redfish driver,
2628+ the node is now rebooted if the change is not detected on the System
2629+ resource refresh. Ironic then waits up to
2630+ ``[redfish]boot_mode_config_timeout`` seconds until the change is applied.
2631+upgrade:
2632+ - |
2633+ Changing the boot mode or the secure boot state via the direct API
2634+ (``/v1/nodes/{node_ident}/states/boot_mode`` and
2635+ ``/v1/nodes/{node_ident}/states/secure_boot`` accordingly) may now
2636+ result in a reboot. This happens when the change cannot be applied
2637+ immediately. Previously, the change would be applied whenever the next
2638+ reboot happens for any unrelated reason, causing inconsistent behavior.
2639+issues:
2640+ - |
2641+ When boot mode needs to be changed during provisioning, an additional
2642+ reboot may happen on certain hardware. This is to ensure consistent
2643+ behavior when any boot setting change results in a separate internal job.
2644diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst
2645index d123847..dc91336 100644
2646--- a/releasenotes/source/2023.1.rst
2647+++ b/releasenotes/source/2023.1.rst
2648@@ -1,6 +1,6 @@
2649-===========================
2650-2023.1 Series Release Notes
2651-===========================
2652+=============================================
2653+2023.1 Series (21.2.0 - 21.4.x) Release Notes
2654+=============================================
2655
2656 .. release-notes::
2657 :branch: stable/2023.1
2658diff --git a/setup.cfg b/setup.cfg
2659index c14a312..9f31ef9 100644
2660--- a/setup.cfg
2661+++ b/setup.cfg
2662@@ -86,6 +86,7 @@ ironic.hardware.interfaces.deploy =
2663 ironic.hardware.interfaces.firmware =
2664 fake = ironic.drivers.modules.fake:FakeFirmware
2665 no-firmware = ironic.drivers.modules.noop:NoFirmware
2666+ redfish = ironic.drivers.modules.redfish.firmware:RedfishFirmware
2667 ironic.hardware.interfaces.inspect =
2668 agent = ironic.drivers.modules.inspector:AgentInspect
2669 fake = ironic.drivers.modules.fake:FakeInspect
2670diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
2671index 120a06e..ea1446a 100644
2672--- a/zuul.d/project.yaml
2673+++ b/zuul.d/project.yaml
2674@@ -60,7 +60,7 @@
2675 voting: false
2676 - ironic-inspector-tempest-rbac-scope-enforced:
2677 voting: false
2678- - bifrost-integration-tinyipa-ubuntu-focal:
2679+ - bifrost-integration-tinyipa-ubuntu-jammy:
2680 voting: false
2681 - bifrost-integration-redfish-vmedia-uefi-centos-9:
2682 voting: false

Subscribers

People subscribed via source and target branches