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
diff --git a/ChangeLog b/ChangeLog
index f4e38ca..4f21043 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,10 +1,26 @@
1CHANGES1CHANGES
2=======2=======
33
423.0.0
5------
6
7* RedfishFirmware Interface
8* inspect\_utils, handle bracketed IPv6 redfish addr
9* Trivial: attach versions to release series
10* redfish\_address - wrap\_ipv6 address
11* Remove most prints for unit tests
12* [releasenotes] Prelude for 2023.2/bobcat
13* devstack - configurable ipv6 address mode
14* Redfish: wait for secure boot state change if it's not immediate
15* CI: Remove ubuntu focal job
16* Fix two places that can cause issues under SQLite
4* [CI] Unblock CI by fixing job regex and non-voting snmp17* [CI] Unblock CI by fixing job regex and non-voting snmp
18* Update proliantutils driver requirements for bobcat
19* DB: Only re-query for a lock holder if we cannot lock
5* Add service steps and initial docs20* Add service steps and initial docs
6* Log an exception from heartbeat21* Log an exception from heartbeat
7* log the version of the conductor starting22* log the version of the conductor starting
23* PXE: Remove DHCP option 210 from being set
8* Fully monkey patch eventlet for consistent behavior24* Fully monkey patch eventlet for consistent behavior
9* Add missing release mappings for 22.0 and 22.125* Add missing release mappings for 22.0 and 22.1
10* Correct bindep.txt entries for bookworm26* Correct bindep.txt entries for bookworm
@@ -33,6 +49,7 @@ CHANGES
33* Support sha256/sha512 with the ilo firmware upgrade logic49* Support sha256/sha512 with the ilo firmware upgrade logic
34* tox: Remove basepython50* tox: Remove basepython
35* Fix typo in deploy\_templates docs51* Fix typo in deploy\_templates docs
52* Fix minor grammar issues in the help for new inspector options
36* Add python3.10 support in testing runtime53* Add python3.10 support in testing runtime
37* DB: Select upon delete for allocations54* DB: Select upon delete for allocations
38* DB: Streamline allocation interactions55* DB: Streamline allocation interactions
diff --git a/PKG-INFO b/PKG-INFO
index d87c849..b71bd51 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,11 +1,53 @@
1Metadata-Version: 2.11Metadata-Version: 2.1
2Name: ironic2Name: ironic
3Version: 22.1.1.dev363Version: 23.0.0
4Summary: OpenStack Bare Metal Provisioning4Summary: OpenStack Bare Metal Provisioning
5Home-page: https://docs.openstack.org/ironic/latest/5Home-page: https://docs.openstack.org/ironic/latest/
6Author: OpenStack6Author: OpenStack
7Author-email: openstack-discuss@lists.openstack.org7Author-email: openstack-discuss@lists.openstack.org
8License: UNKNOWN8License: UNKNOWN
9Description: ======
10 Ironic
11 ======
12
13 Team and repository tags
14 ------------------------
15
16 .. image:: https://governance.openstack.org/tc/badges/ironic.svg
17 :target: https://governance.openstack.org/tc/reference/tags/index.html
18
19 Overview
20 --------
21
22 Ironic consists of an API and plug-ins for managing and provisioning
23 physical machines in a security-aware and fault-tolerant manner. It can be
24 used with nova as a hypervisor driver, or standalone service using bifrost.
25 By default, it will use PXE and IPMI to interact with bare metal machines.
26 Ironic also supports vendor-specific plug-ins which may implement additional
27 functionality.
28
29 Ironic is distributed under the terms of the Apache License, Version 2.0. The
30 full terms and conditions of this license are detailed in the LICENSE file.
31
32 Project resources
33 ~~~~~~~~~~~~~~~~~
34
35 * Documentation: https://docs.openstack.org/ironic/latest
36 * Source: https://opendev.org/openstack/ironic
37 * Bugs: https://bugs.launchpad.net/ironic/+bugs
38 * Wiki: https://wiki.openstack.org/wiki/Ironic
39 * APIs: https://docs.openstack.org/api-ref/baremetal/index.html
40 * Release Notes: https://docs.openstack.org/releasenotes/ironic/
41 * Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
42
43 Project status, bugs, and requests for feature enhancements (RFEs) are tracked
44 in StoryBoard:
45 https://storyboard.openstack.org/#!/project/943
46
47 For information on how to contribute to ironic, see
48 https://docs.openstack.org/ironic/latest/contributor
49
50
9Platform: UNKNOWN51Platform: UNKNOWN
10Classifier: Environment :: OpenStack52Classifier: Environment :: OpenStack
11Classifier: Intended Audience :: Information Technology53Classifier: Intended Audience :: Information Technology
@@ -23,48 +65,3 @@ Provides-Extra: devstack
23Provides-Extra: guru_meditation_reports65Provides-Extra: guru_meditation_reports
24Provides-Extra: i18n66Provides-Extra: i18n
25Provides-Extra: test67Provides-Extra: test
26License-File: LICENSE
27
28======
29Ironic
30======
31
32Team and repository tags
33------------------------
34
35.. image:: https://governance.openstack.org/tc/badges/ironic.svg
36 :target: https://governance.openstack.org/tc/reference/tags/index.html
37
38Overview
39--------
40
41Ironic consists of an API and plug-ins for managing and provisioning
42physical machines in a security-aware and fault-tolerant manner. It can be
43used with nova as a hypervisor driver, or standalone service using bifrost.
44By default, it will use PXE and IPMI to interact with bare metal machines.
45Ironic also supports vendor-specific plug-ins which may implement additional
46functionality.
47
48Ironic is distributed under the terms of the Apache License, Version 2.0. The
49full terms and conditions of this license are detailed in the LICENSE file.
50
51Project resources
52~~~~~~~~~~~~~~~~~
53
54* Documentation: https://docs.openstack.org/ironic/latest
55* Source: https://opendev.org/openstack/ironic
56* Bugs: https://bugs.launchpad.net/ironic/+bugs
57* Wiki: https://wiki.openstack.org/wiki/Ironic
58* APIs: https://docs.openstack.org/api-ref/baremetal/index.html
59* Release Notes: https://docs.openstack.org/releasenotes/ironic/
60* Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
61
62Project status, bugs, and requests for feature enhancements (RFEs) are tracked
63in StoryBoard:
64https://storyboard.openstack.org/#!/project/943
65
66For information on how to contribute to ironic, see
67https://docs.openstack.org/ironic/latest/contributor
68
69
70
diff --git a/api-ref/source/baremetal-api-v1-node-management.inc b/api-ref/source/baremetal-api-v1-node-management.inc
index 6ff275e..4d9bdaa 100644
--- a/api-ref/source/baremetal-api-v1-node-management.inc
+++ b/api-ref/source/baremetal-api-v1-node-management.inc
@@ -306,6 +306,10 @@ Change Node Boot Mode
306306
307Request a change to the Node's boot mode.307Request a change to the Node's boot mode.
308308
309.. note::
310 Depending on the driver and the underlying hardware, changing boot mode may
311 result in an automatic reboot.
312
309.. versionadded:: 1.76313.. versionadded:: 1.76
310 A change in node's boot mode can be requested.314 A change in node's boot mode can be requested.
311315
@@ -341,6 +345,10 @@ Change Node Secure Boot
341345
342Request a change to the Node's secure boot state.346Request a change to the Node's secure boot state.
343347
348.. note::
349 Depending on the driver and the underlying hardware, changing the secure
350 boot state may result in an automatic reboot.
351
344.. versionadded:: 1.76352.. versionadded:: 1.76
345 A change in node's secure boot state can be requested.353 A change in node's secure boot state can be requested.
346354
diff --git a/api-ref/source/baremetal-api-v1-nodes-firmware.inc b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
index ed17e0b..15ea992 100644
--- a/api-ref/source/baremetal-api-v1-nodes-firmware.inc
+++ b/api-ref/source/baremetal-api-v1-nodes-firmware.inc
@@ -4,7 +4,7 @@
4Node Firmware (nodes)4Node Firmware (nodes)
5=====================5=====================
66
7.. versionadded:: 1.847.. versionadded:: 1.86
88
9Given a Node identifier (``uuid`` or ``name``), the API exposes the list of9Given a Node identifier (``uuid`` or ``name``), the API exposes the list of
10all Firmware Components associated with that Node.10all Firmware Components associated with that Node.
diff --git a/debian/changelog b/debian/changelog
index 105559b..1783bf3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,10 @@
1ironic (1:22.1.0+git2023090714.985c7fdf-0ubuntu2) UNRELEASED; urgency=medium1ironic (1:23.0.0-0ubuntu1) mantic; urgency=medium
22
3 * d/watch: Drop major version.3 * d/watch: Drop major version.
4 * New upstream release for OpenStack Bobcat.
5 * d/control: Align (Build-)Depends with upstream.
46
5 -- Corey Bryant <corey.bryant@canonical.com> Wed, 04 Oct 2023 10:21:10 -04007 -- Corey Bryant <corey.bryant@canonical.com> Wed, 04 Oct 2023 10:27:14 -0400
68
7ironic (1:22.1.0+git2023090714.985c7fdf-0ubuntu1) mantic; urgency=medium9ironic (1:22.1.0+git2023090714.985c7fdf-0ubuntu1) mantic; urgency=medium
810
diff --git a/debian/control b/debian/control
index a66689c..1cfb645 100644
--- a/debian/control
+++ b/debian/control
@@ -11,6 +11,21 @@ Build-Depends:
11 python3-setuptools,11 python3-setuptools,
12 python3-sphinx (>= 2.0.0),12 python3-sphinx (>= 2.0.0),
13Build-Depends-Indep:13Build-Depends-Indep:
14*** new test-requirements.txt deps (start)
15 NOT_FOUND(pyasn1-lextudio)>=1.1.0,
16 NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
17 NOT_FOUND(pysnmp-lextudio)>=5.0.0,
18*** new test-requirements.txt deps (end) ***
19*** new driver-requirements.txt deps (start)
20 NOT_FOUND(pyasn1-lextudio)>=1.1.0,
21 NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
22 NOT_FOUND(pysnmp-lextudio)>=5.0.0,
23*** new driver-requirements.txt deps (end) ***
24*** new doc/requirements.txt deps (start)
25 NOT_FOUND(pyasn1-lextudio)>=1.1.0,
26 NOT_FOUND(pyasn1-modules-lextudio)>=0.2.0,
27 NOT_FOUND(pysnmp-lextudio)>=5.0.0,
28*** new doc/requirements.txt deps (end) ***
14 alembic (>= 0.9.6),29 alembic (>= 0.9.6),
15 crudini,30 crudini,
16 python3-alembic (>= 1.4.2),31 python3-alembic (>= 1.4.2),
diff --git a/devstack/lib/ironic b/devstack/lib/ironic
index 882b5f6..ffd6601 100644
--- a/devstack/lib/ironic
+++ b/devstack/lib/ironic
@@ -382,6 +382,7 @@ IRONIC_UWSGI=$IRONIC_BIN_DIR/ironic-api-wsgi
382382
383# Lets support IPv6 testing!383# Lets support IPv6 testing!
384IRONIC_IP_VERSION=${IRONIC_IP_VERSION:-${IP_VERSION:-4}}384IRONIC_IP_VERSION=${IRONIC_IP_VERSION:-${IP_VERSION:-4}}
385IRONIC_IPV6_ADDRESS_MODE=${IRONIC_IPV6_ADDRESS_MODE:-dhcpv6-stateless}
385386
386# Ironic connection info. Note the port must be specified.387# Ironic connection info. Note the port must be specified.
387if is_service_enabled tls-proxy; then388if is_service_enabled tls-proxy; then
@@ -1424,11 +1425,9 @@ function configure_ironic_provision_network {
1424 --subnet-range $IRONIC_PROVISION_SUBNET_PREFIX \1425 --subnet-range $IRONIC_PROVISION_SUBNET_PREFIX \
1425 --dns-nameserver 8.8.8.8 -f value -c id)"1426 --dns-nameserver 8.8.8.8 -f value -c id)"
1426 else1427 else
1427 # NOTE(TheJulia): Consider changing this to stateful to support UEFI once we move
1428 # CI to Ubuntu Jammy as it will support v6 and v4 UEFI firmware driven boot ops.
1429 subnet_id="$(openstack --os-cloud $OS_CLOUD subnet create --ip-version 6 \1428 subnet_id="$(openstack --os-cloud $OS_CLOUD subnet create --ip-version 6 \
1430 --ipv6-address-mode dhcpv6-stateless \1429 --ipv6-address-mode $IRONIC_IPV6_ADDRESS_MODE \
1431 --ipv6-ra-mode dhcpv6-stateless \1430 --ipv6-ra-mode $IRONIC_IPV6_ADDRESS_MODE \
1432 --dns-nameserver 2001:4860:4860::8888 \1431 --dns-nameserver 2001:4860:4860::8888 \
1433 ${net_segment_id:+--network-segment $net_segment_id} \1432 ${net_segment_id:+--network-segment $net_segment_id} \
1434 $IRONIC_PROVISION_PROVIDER_SUBNET_NAME \1433 $IRONIC_PROVISION_PROVIDER_SUBNET_NAME \
diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst
index 6628f48..c4694b8 100644
--- a/doc/source/admin/drivers/redfish.rst
+++ b/doc/source/admin/drivers/redfish.rst
@@ -140,6 +140,22 @@ Two clean and deploy steps are provided for key management:
140``management.clear_secure_boot_keys``140``management.clear_secure_boot_keys``
141 removes all secure boot keys from the node.141 removes all secure boot keys from the node.
142142
143Rebooting on boot mode changes
144~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
145
146While some hardware is able to change the boot mode or the `UEFI secure boot`_
147state immediately, other models may require a reboot for such a change to be
148applied. Furthermore, some hardware models cannot change the boot mode and the
149secure boot state simultaneously, requiring several reboots.
150
151The Bare Metal service refreshes the System resource after issuing a PATCH
152request against it. If the expected change is not observed, the node is
153rebooted, and the Bare Metal service waits until the change is applied. In the
154end, the previous power state is restored.
155
156This logic makes changing boot configuration more robust at the expense of
157several reboots in the worst case.
158
143Out-Of-Band inspection159Out-Of-Band inspection
144======================160======================
145161
diff --git a/doc/source/contributor/webapi-version-history.rst b/doc/source/contributor/webapi-version-history.rst
index efd34d7..e2f0a73 100644
--- a/doc/source/contributor/webapi-version-history.rst
+++ b/doc/source/contributor/webapi-version-history.rst
@@ -15,8 +15,8 @@ based resources, which indicates the current step.
1515
16Adds a ``firmware_interface`` field to the ``/v1/nodes`` resources.16Adds a ``firmware_interface`` field to the ``/v1/nodes`` resources.
1717
181.85 (Bobcat)181.85 (Bobcat, 22.1)
19-------------19-------------------
2020
21This version adds a new provision state change verb called ``unhold``21This version adds a new provision state change verb called ``unhold``
22to be utilized with the new ``provision_state`` values ``clean hold``22to be utilized with the new ``provision_state`` values ``clean hold``
@@ -24,14 +24,14 @@ and ``deploy hold``. The verb instructs Ironic to remove the node
24from it's present hold and to resume it's prior cleaning or24from it's present hold and to resume it's prior cleaning or
25deployment process.25deployment process.
2626
271.84 (Bobcat)271.84 (Bobcat, 22.1)
28-------------28-------------------
2929
30Add callback endpoint for in-band inspection ``/v1/continue_inspection``.30Add callback endpoint for in-band inspection ``/v1/continue_inspection``.
31This endpoint is not designed to be used by regular users.31This endpoint is not designed to be used by regular users.
3232
331.83 (Bobcat)331.83 (Bobcat, 22.0)
34-------------34-------------------
3535
36This version adds a concept of child nodes through the use of a36This version adds a concept of child nodes through the use of a
37``parent_node`` field which can be set on a node.37``parent_node`` field which can be set on a node.
@@ -51,8 +51,8 @@ Additionally:
51- Adds ``GET /v1/nodes?parent_node={node_ident}`` to explicitly request51- Adds ``GET /v1/nodes?parent_node={node_ident}`` to explicitly request
52 a detailed list of nodes by parent relationship.52 a detailed list of nodes by parent relationship.
5353
541.82 (Antelope)541.82 (Antelope, 21.4)
55----------------------55---------------------
5656
57This version signifies the addition of node sharding endpoints.57This version signifies the addition of node sharding endpoints.
5858
@@ -60,8 +60,8 @@ This version signifies the addition of node sharding endpoints.
60- Adds support for ``GET /v1/shards`` which returns a list of all shards and60- Adds support for ``GET /v1/shards`` which returns a list of all shards and
61 the count of nodes assigned to each.61 the count of nodes assigned to each.
6262
631.81 (Antelope)631.81 (Antelope, 21.3)
64----------------------64---------------------
6565
66Add endpoint to retrieve introspection data for nodes via the REST API.66Add endpoint to retrieve introspection data for nodes via the REST API.
6767
diff --git a/driver-requirements.txt b/driver-requirements.txt
index a26c3cf..b0852d0 100644
--- a/driver-requirements.txt
+++ b/driver-requirements.txt
@@ -4,7 +4,7 @@
4# python projects they should package as optional dependencies for Ironic.4# python projects they should package as optional dependencies for Ironic.
55
6# These are available on pypi6# These are available on pypi
7proliantutils>=2.14.07proliantutils>=2.16.0
8pysnmp-lextudio>=5.0.0 # BSD8pysnmp-lextudio>=5.0.0 # BSD
9pyasn1-lextudio>=1.1.0 # BSD9pyasn1-lextudio>=1.1.0 # BSD
10pyasn1-modules-lextudio>=0.2.0 # BSD10pyasn1-modules-lextudio>=0.2.0 # BSD
diff --git a/ironic.egg-info/PKG-INFO b/ironic.egg-info/PKG-INFO
index d87c849..b71bd51 100644
--- a/ironic.egg-info/PKG-INFO
+++ b/ironic.egg-info/PKG-INFO
@@ -1,11 +1,53 @@
1Metadata-Version: 2.11Metadata-Version: 2.1
2Name: ironic2Name: ironic
3Version: 22.1.1.dev363Version: 23.0.0
4Summary: OpenStack Bare Metal Provisioning4Summary: OpenStack Bare Metal Provisioning
5Home-page: https://docs.openstack.org/ironic/latest/5Home-page: https://docs.openstack.org/ironic/latest/
6Author: OpenStack6Author: OpenStack
7Author-email: openstack-discuss@lists.openstack.org7Author-email: openstack-discuss@lists.openstack.org
8License: UNKNOWN8License: UNKNOWN
9Description: ======
10 Ironic
11 ======
12
13 Team and repository tags
14 ------------------------
15
16 .. image:: https://governance.openstack.org/tc/badges/ironic.svg
17 :target: https://governance.openstack.org/tc/reference/tags/index.html
18
19 Overview
20 --------
21
22 Ironic consists of an API and plug-ins for managing and provisioning
23 physical machines in a security-aware and fault-tolerant manner. It can be
24 used with nova as a hypervisor driver, or standalone service using bifrost.
25 By default, it will use PXE and IPMI to interact with bare metal machines.
26 Ironic also supports vendor-specific plug-ins which may implement additional
27 functionality.
28
29 Ironic is distributed under the terms of the Apache License, Version 2.0. The
30 full terms and conditions of this license are detailed in the LICENSE file.
31
32 Project resources
33 ~~~~~~~~~~~~~~~~~
34
35 * Documentation: https://docs.openstack.org/ironic/latest
36 * Source: https://opendev.org/openstack/ironic
37 * Bugs: https://bugs.launchpad.net/ironic/+bugs
38 * Wiki: https://wiki.openstack.org/wiki/Ironic
39 * APIs: https://docs.openstack.org/api-ref/baremetal/index.html
40 * Release Notes: https://docs.openstack.org/releasenotes/ironic/
41 * Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
42
43 Project status, bugs, and requests for feature enhancements (RFEs) are tracked
44 in StoryBoard:
45 https://storyboard.openstack.org/#!/project/943
46
47 For information on how to contribute to ironic, see
48 https://docs.openstack.org/ironic/latest/contributor
49
50
9Platform: UNKNOWN51Platform: UNKNOWN
10Classifier: Environment :: OpenStack52Classifier: Environment :: OpenStack
11Classifier: Intended Audience :: Information Technology53Classifier: Intended Audience :: Information Technology
@@ -23,48 +65,3 @@ Provides-Extra: devstack
23Provides-Extra: guru_meditation_reports65Provides-Extra: guru_meditation_reports
24Provides-Extra: i18n66Provides-Extra: i18n
25Provides-Extra: test67Provides-Extra: test
26License-File: LICENSE
27
28======
29Ironic
30======
31
32Team and repository tags
33------------------------
34
35.. image:: https://governance.openstack.org/tc/badges/ironic.svg
36 :target: https://governance.openstack.org/tc/reference/tags/index.html
37
38Overview
39--------
40
41Ironic consists of an API and plug-ins for managing and provisioning
42physical machines in a security-aware and fault-tolerant manner. It can be
43used with nova as a hypervisor driver, or standalone service using bifrost.
44By default, it will use PXE and IPMI to interact with bare metal machines.
45Ironic also supports vendor-specific plug-ins which may implement additional
46functionality.
47
48Ironic is distributed under the terms of the Apache License, Version 2.0. The
49full terms and conditions of this license are detailed in the LICENSE file.
50
51Project resources
52~~~~~~~~~~~~~~~~~
53
54* Documentation: https://docs.openstack.org/ironic/latest
55* Source: https://opendev.org/openstack/ironic
56* Bugs: https://bugs.launchpad.net/ironic/+bugs
57* Wiki: https://wiki.openstack.org/wiki/Ironic
58* APIs: https://docs.openstack.org/api-ref/baremetal/index.html
59* Release Notes: https://docs.openstack.org/releasenotes/ironic/
60* Design Specifications: https://specs.openstack.org/openstack/ironic-specs/
61
62Project status, bugs, and requests for feature enhancements (RFEs) are tracked
63in StoryBoard:
64https://storyboard.openstack.org/#!/project/943
65
66For information on how to contribute to ironic, see
67https://docs.openstack.org/ironic/latest/contributor
68
69
70
diff --git a/ironic.egg-info/SOURCES.txt b/ironic.egg-info/SOURCES.txt
index e1d1e3c..7da3f81 100644
--- a/ironic.egg-info/SOURCES.txt
+++ b/ironic.egg-info/SOURCES.txt
@@ -695,6 +695,7 @@ ironic/drivers/modules/network/noop.py
695ironic/drivers/modules/redfish/__init__.py695ironic/drivers/modules/redfish/__init__.py
696ironic/drivers/modules/redfish/bios.py696ironic/drivers/modules/redfish/bios.py
697ironic/drivers/modules/redfish/boot.py697ironic/drivers/modules/redfish/boot.py
698ironic/drivers/modules/redfish/firmware.py
698ironic/drivers/modules/redfish/firmware_utils.py699ironic/drivers/modules/redfish/firmware_utils.py
699ironic/drivers/modules/redfish/inspect.py700ironic/drivers/modules/redfish/inspect.py
700ironic/drivers/modules/redfish/management.py701ironic/drivers/modules/redfish/management.py
@@ -975,6 +976,7 @@ ironic/tests/unit/drivers/modules/network/json_samples/network_data.json
975ironic/tests/unit/drivers/modules/redfish/__init__.py976ironic/tests/unit/drivers/modules/redfish/__init__.py
976ironic/tests/unit/drivers/modules/redfish/test_bios.py977ironic/tests/unit/drivers/modules/redfish/test_bios.py
977ironic/tests/unit/drivers/modules/redfish/test_boot.py978ironic/tests/unit/drivers/modules/redfish/test_boot.py
979ironic/tests/unit/drivers/modules/redfish/test_firmware.py
978ironic/tests/unit/drivers/modules/redfish/test_firmware_utils.py980ironic/tests/unit/drivers/modules/redfish/test_firmware_utils.py
979ironic/tests/unit/drivers/modules/redfish/test_inspect.py981ironic/tests/unit/drivers/modules/redfish/test_inspect.py
980ironic/tests/unit/drivers/modules/redfish/test_management.py982ironic/tests/unit/drivers/modules/redfish/test_management.py
@@ -1021,6 +1023,7 @@ releasenotes/config.yaml
1021releasenotes/notes/.placeholder1023releasenotes/notes/.placeholder
1022releasenotes/notes/18.2-prelude-3c8609692bab70a3.yaml1024releasenotes/notes/18.2-prelude-3c8609692bab70a3.yaml
1023releasenotes/notes/20.1-prelude-612672742f417477.yaml1025releasenotes/notes/20.1-prelude-612672742f417477.yaml
1026releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
1024releasenotes/notes/5.0-release-afb1fbbe595b6bc8.yaml1027releasenotes/notes/5.0-release-afb1fbbe595b6bc8.yaml
1025releasenotes/notes/Add-port-option-support-to-ipmitool-e125d07fe13c53e7.yaml1028releasenotes/notes/Add-port-option-support-to-ipmitool-e125d07fe13c53e7.yaml
1026releasenotes/notes/Cleanfail-power-off-13b5fdcc2727866a.yaml1029releasenotes/notes/Cleanfail-power-off-13b5fdcc2727866a.yaml
@@ -1265,6 +1268,7 @@ releasenotes/notes/bug-2007963-idrac-wsman-raid-apply-configuration-792ccf195057
1265releasenotes/notes/bug-2008058-fix-factory-reset-status.yaml-52a6119b46e33b37.yaml1268releasenotes/notes/bug-2008058-fix-factory-reset-status.yaml-52a6119b46e33b37.yaml
1266releasenotes/notes/bug-2009762-403eac24c4823d2d.yaml1269releasenotes/notes/bug-2009762-403eac24c4823d2d.yaml
1267releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml1270releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml
1271releasenotes/notes/bug-2036455-edd0e97335579684.yaml
1268releasenotes/notes/bug-30315-e46eafe5b575f3da.yaml1272releasenotes/notes/bug-30315-e46eafe5b575f3da.yaml
1269releasenotes/notes/bug-30316-8c53358681e464eb.yaml1273releasenotes/notes/bug-30316-8c53358681e464eb.yaml
1270releasenotes/notes/bug-30317-a972c8d879c98941.yaml1274releasenotes/notes/bug-30317-a972c8d879c98941.yaml
@@ -1453,6 +1457,7 @@ releasenotes/notes/fast-track-with-cleaning-438225116a11662d.yaml
1453releasenotes/notes/fifteen-0da3cca48dceab8b.yaml1457releasenotes/notes/fifteen-0da3cca48dceab8b.yaml
1454releasenotes/notes/file-name-too-long-72265bb3fec704f8.yaml1458releasenotes/notes/file-name-too-long-72265bb3fec704f8.yaml
1455releasenotes/notes/fips-hashlib-bca9beacc2b48fe7.yaml1459releasenotes/notes/fips-hashlib-bca9beacc2b48fe7.yaml
1460releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
1456releasenotes/notes/fix-agent-clean-up-9a25deb85bc53d9b.yaml1461releasenotes/notes/fix-agent-clean-up-9a25deb85bc53d9b.yaml
1457releasenotes/notes/fix-agent-ilo-temp-image-cleanup-711429d0e67807ae.yaml1462releasenotes/notes/fix-agent-ilo-temp-image-cleanup-711429d0e67807ae.yaml
1458releasenotes/notes/fix-anaconda-deploy-interface-bfa2cfca22b04680.yaml1463releasenotes/notes/fix-anaconda-deploy-interface-bfa2cfca22b04680.yaml
@@ -2002,6 +2007,7 @@ releasenotes/notes/releasenote-b3b25c13ea1e2844.yaml
2002releasenotes/notes/reloadable-301ec2aa421abf66.yaml2007releasenotes/notes/reloadable-301ec2aa421abf66.yaml
2003releasenotes/notes/rely-on-standalone-ports-supported-8153e1135787828b.yaml2008releasenotes/notes/rely-on-standalone-ports-supported-8153e1135787828b.yaml
2004releasenotes/notes/removal-pre-allocation-for-oneview-09310a215b3aaf3c.yaml2009releasenotes/notes/removal-pre-allocation-for-oneview-09310a215b3aaf3c.yaml
2010releasenotes/notes/remove-400a563030224c4f.yaml
2005releasenotes/notes/remove-DEPRECATED-options-from-[agent]-7b6cce21b5f52022.yaml2011releasenotes/notes/remove-DEPRECATED-options-from-[agent]-7b6cce21b5f52022.yaml
2006releasenotes/notes/remove-agent-heartbeat-timeout-abf8787b8477bae7.yaml2012releasenotes/notes/remove-agent-heartbeat-timeout-abf8787b8477bae7.yaml
2007releasenotes/notes/remove-agent-passthru-432b18e6c430cee6.yaml2013releasenotes/notes/remove-agent-passthru-432b18e6c430cee6.yaml
@@ -2149,6 +2155,7 @@ releasenotes/notes/token-reboot-b48b5981a58a30ae.yaml
2149releasenotes/notes/train-release-59ff1643ec92c10a.yaml2155releasenotes/notes/train-release-59ff1643ec92c10a.yaml
2150releasenotes/notes/transmit-all-ports-b570009d1a008067.yaml2156releasenotes/notes/transmit-all-ports-b570009d1a008067.yaml
2151releasenotes/notes/type-error-str-6826c53d7e5e1243.yaml2157releasenotes/notes/type-error-str-6826c53d7e5e1243.yaml
2158releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
2152releasenotes/notes/uefi-first-prepare-e7fa1e2a78b4af99.yaml2159releasenotes/notes/uefi-first-prepare-e7fa1e2a78b4af99.yaml
2153releasenotes/notes/uefi-grub2-by-default-6b797a9e690d2dd5.yaml2160releasenotes/notes/uefi-grub2-by-default-6b797a9e690d2dd5.yaml
2154releasenotes/notes/uefi-is-now-the-default-562b0d68adc59008.yaml2161releasenotes/notes/uefi-is-now-the-default-562b0d68adc59008.yaml
diff --git a/ironic.egg-info/entry_points.txt b/ironic.egg-info/entry_points.txt
index 7d3e573..7bb5a43 100644
--- a/ironic.egg-info/entry_points.txt
+++ b/ironic.egg-info/entry_points.txt
@@ -54,6 +54,7 @@ ramdisk = ironic.drivers.modules.ramdisk:RamdiskDeploy
54[ironic.hardware.interfaces.firmware]54[ironic.hardware.interfaces.firmware]
55fake = ironic.drivers.modules.fake:FakeFirmware55fake = ironic.drivers.modules.fake:FakeFirmware
56no-firmware = ironic.drivers.modules.noop:NoFirmware56no-firmware = ironic.drivers.modules.noop:NoFirmware
57redfish = ironic.drivers.modules.redfish.firmware:RedfishFirmware
5758
58[ironic.hardware.interfaces.inspect]59[ironic.hardware.interfaces.inspect]
59agent = ironic.drivers.modules.inspector:AgentInspect60agent = ironic.drivers.modules.inspector:AgentInspect
diff --git a/ironic.egg-info/pbr.json b/ironic.egg-info/pbr.json
index 3a310bb..21af2bb 100644
--- a/ironic.egg-info/pbr.json
+++ b/ironic.egg-info/pbr.json
@@ -1 +1 @@
1{"git_version": "985c7fdf2", "is_release": false}
2\ No newline at end of file1\ No newline at end of file
2{"git_version": "f78f87227", "is_release": true}
3\ No newline at end of file3\ No newline at end of file
diff --git a/ironic/api/controllers/v1/utils.py b/ironic/api/controllers/v1/utils.py
index 10c631d..3a7806e 100644
--- a/ironic/api/controllers/v1/utils.py
+++ b/ironic/api/controllers/v1/utils.py
@@ -2018,6 +2018,6 @@ def allow_continue_inspection_endpoint():
2018def allow_firmware_interface():2018def allow_firmware_interface():
2019 """Check if we should support firmware interface and endpoints.2019 """Check if we should support firmware interface and endpoints.
20202020
2021 Version 1.84 of the API added support for firmware interface.2021 Version 1.86 of the API added support for firmware interface.
2022 """2022 """
2023 return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE2023 return api.request.version.minor >= versions.MINOR_86_FIRMWARE_INTERFACE
diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py
index a4fd438..8c61e23 100644
--- a/ironic/common/pxe_utils.py
+++ b/ironic/common/pxe_utils.py
@@ -58,7 +58,6 @@ DHCPV6_BOOTFILE_NAME = '59' # rfc5970
58# DHCPV6_BOOTFILE_PARAMS = '60' # rfc597058# DHCPV6_BOOTFILE_PARAMS = '60' # rfc5970
59DHCP_TFTP_SERVER_ADDRESS = '150' # rfc585959DHCP_TFTP_SERVER_ADDRESS = '150' # rfc5859
60DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned60DHCP_IPXE_ENCAP_OPTS = '175' # Tentatively Assigned
61DHCP_TFTP_PATH_PREFIX = '210' # rfc5071
62DHCP_SERVER_IP_ADDRESS = '255' # dnsmasq server-ip-address61DHCP_SERVER_IP_ADDRESS = '255' # dnsmasq server-ip-address
6362
64DEPLOY_KERNEL_RAMDISK_LABELS = ['deploy_kernel', 'deploy_ramdisk']63DEPLOY_KERNEL_RAMDISK_LABELS = ['deploy_kernel', 'deploy_ramdisk']
@@ -562,16 +561,6 @@ def dhcp_options_for_instance(task, ipxe_enabled=False, url_boot=False,
562 else:561 else:
563 dhcp_opts.append({'opt_name': boot_file_param,562 dhcp_opts.append({'opt_name': boot_file_param,
564 'opt_value': boot_file})563 'opt_value': boot_file})
565 # 210 == tftp server path-prefix or tftp root, will be used to find
566 # pxelinux.cfg directory. The pxelinux.0 loader infers this information
567 # from it's own path, but Petitboot needs it to be specified by this
568 # option since it doesn't use pxelinux.0 loader.
569 if not url_boot:
570 # Enforce trailing slash
571 prefix = os.path.join(CONF.pxe.tftp_root, '')
572 dhcp_opts.append(
573 {'opt_name': DHCP_TFTP_PATH_PREFIX,
574 'opt_value': prefix})
575564
576 if not url_boot:565 if not url_boot:
577 dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,566 dhcp_opts.append({'opt_name': DHCP_TFTP_SERVER_NAME,
diff --git a/ironic/common/utils.py b/ironic/common/utils.py
index 6bae4e7..efd3049 100644
--- a/ironic/common/utils.py
+++ b/ironic/common/utils.py
@@ -578,8 +578,12 @@ def pop_node_nested_field(node, collection, field, default=None):
578578
579def wrap_ipv6(ip):579def wrap_ipv6(ip):
580 """Wrap the address in square brackets if it's an IPv6 address."""580 """Wrap the address in square brackets if it's an IPv6 address."""
581 if ipaddress.ip_address(ip).version == 6:581 try:
582 return "[%s]" % ip582 if ipaddress.ip_address(ip).version == 6:
583 return "[%s]" % ip
584 except ValueError:
585 pass
586
583 return ip587 return ip
584588
585589
diff --git a/ironic/conductor/cleaning.py b/ironic/conductor/cleaning.py
index 1e79f97..cd2d999 100644
--- a/ironic/conductor/cleaning.py
+++ b/ironic/conductor/cleaning.py
@@ -85,6 +85,8 @@ def do_node_clean(task, clean_steps=None, disable_ramdisk=False):
85 # Retrieve BIOS config settings for this node85 # Retrieve BIOS config settings for this node
86 utils.node_cache_bios_settings(task, node)86 utils.node_cache_bios_settings(task, node)
8787
88 # Retrieve Firmware Components for this node if possible
89 utils.node_cache_firmware_components(task)
88 # Allow the deploy driver to set up the ramdisk again (necessary for90 # Allow the deploy driver to set up the ramdisk again (necessary for
89 # IPA cleaning)91 # IPA cleaning)
90 try:92 try:
diff --git a/ironic/conductor/steps.py b/ironic/conductor/steps.py
index 3dfbb2e..dcdfa46 100644
--- a/ironic/conductor/steps.py
+++ b/ironic/conductor/steps.py
@@ -31,9 +31,10 @@ CLEANING_INTERFACE_PRIORITY = {
31 # by which interface is implementing the clean step. The clean step of the31 # by which interface is implementing the clean step. The clean step of the
32 # interface with the highest value here, will be executed first in that32 # interface with the highest value here, will be executed first in that
33 # case.33 # case.
34 'vendor': 6,34 'vendor': 7,
35 'power': 5,35 'power': 6,
36 'management': 4,36 'management': 5,
37 'firmware': 4,
37 'deploy': 3,38 'deploy': 3,
38 'bios': 2,39 'bios': 2,
39 'raid': 1,40 'raid': 1,
@@ -46,9 +47,10 @@ DEPLOYING_INTERFACE_PRIORITY = {
46 # TODO(rloo): If we think it makes sense to have the interface priorities47 # TODO(rloo): If we think it makes sense to have the interface priorities
47 # the same for cleaning & deploying, replace the two with one e.g.48 # the same for cleaning & deploying, replace the two with one e.g.
48 # 'INTERFACE_PRIORITIES'.49 # 'INTERFACE_PRIORITIES'.
49 'vendor': 6,50 'vendor': 7,
50 'power': 5,51 'power': 6,
51 'management': 4,52 'management': 5,
53 'firmware': 4,
52 'deploy': 3,54 'deploy': 3,
53 'bios': 2,55 'bios': 2,
54 'raid': 1,56 'raid': 1,
@@ -61,11 +63,12 @@ VERIFYING_INTERFACE_PRIORITY = {
61 # by which interface is implementing the verify step. The verifying step of63 # by which interface is implementing the verify step. The verifying step of
62 # the interface with the highest value here, will be executed first in64 # the interface with the highest value here, will be executed first in
63 # that case.65 # that case.
64 'power': 12,66 'power': 13,
65 'management': 11,67 'management': 12,
66 'boot': 8,68 'firmware': 11,
67 'inspect': 10,69 'inspect': 10,
68 'deploy': 9,70 'deploy': 9,
71 'boot': 8,
69 'bios': 7,72 'bios': 7,
70 'raid': 6,73 'raid': 6,
71 'vendor': 5,74 'vendor': 5,
diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py
index 1255c86..e79ce3d 100644
--- a/ironic/conductor/utils.py
+++ b/ironic/conductor/utils.py
@@ -1828,3 +1828,15 @@ def servicing_error_handler(task, logmsg, errmsg=None, traceback=False,
18281828
1829 if set_fail_state and node.provision_state != states.SERVICEFAIL:1829 if set_fail_state and node.provision_state != states.SERVICEFAIL:
1830 task.process_event('fail')1830 task.process_event('fail')
1831
1832
1833def node_cache_firmware_components(task):
1834 """Do caching of firmware components if supported by driver"""
1835
1836 try:
1837 LOG.debug('Getting Firmware Components for node %s', task.node.uuid)
1838 task.driver.firmware.validate(task)
1839 task.driver.firmware.cache_firmware_components(task)
1840 except exception.UnsupportedDriverExtension:
1841 LOG.warning('Firmware Components are not supported for node %s, '
1842 'skipping', task.node.uuid)
diff --git a/ironic/conf/inspector.py b/ironic/conf/inspector.py
index 6014fb3..434c863 100644
--- a/ironic/conf/inspector.py
+++ b/ironic/conf/inspector.py
@@ -20,13 +20,15 @@ from ironic.conf import auth
2020
21VALID_ADD_PORTS_VALUES = {21VALID_ADD_PORTS_VALUES = {
22 'all': _('all MAC addresses'),22 'all': _('all MAC addresses'),
23 'active': _('MAC addresses of NIC\'s with IP addresses'),23 'active': _('MAC addresses of NICs with IP addresses'),
24 'pxe': _('only the MAC address of the PXE NIC'),24 'pxe': _('only the MAC address of the PXE NIC'),
25 'disabled': _('do not create any ports'),25 'disabled': _('do not create any ports'),
26}26}
27VALID_KEEP_PORTS_VALUES = {27VALID_KEEP_PORTS_VALUES = {
28 'all': _('keep even ports with MAC\'s not present in the inventory'),28 'all': _('keep all ports, even ones with MAC addresses that are not '
29 'present': _('keep only ports with MAC\'s present in the inventory'),29 'present in the inventory'),
30 'present': _('keep only ports with MAC addresses present in '
31 'the inventory'),
30 'added': _('keep only ports determined by the add_ports option'),32 'added': _('keep only ports determined by the add_ports option'),
31}33}
3234
diff --git a/ironic/conf/redfish.py b/ironic/conf/redfish.py
index 68aa961..87a359d 100644
--- a/ironic/conf/redfish.py
+++ b/ironic/conf/redfish.py
@@ -115,6 +115,12 @@ opts = [
115 default=60,115 default=60,
116 help=_('Number of seconds to wait between checking for '116 help=_('Number of seconds to wait between checking for '
117 'failed raid config tasks')),117 'failed raid config tasks')),
118 cfg.IntOpt('boot_mode_config_timeout',
119 min=0,
120 default=900,
121 help=_('Number of seconds to wait for boot mode or secure '
122 'boot status change to take effect after a reboot. '
123 'Set to 0 to disable waiting.')),
118]124]
119125
120126
diff --git a/ironic/db/sqlalchemy/api.py b/ironic/db/sqlalchemy/api.py
index bf591ca..a6ab523 100644
--- a/ironic/db/sqlalchemy/api.py
+++ b/ironic/db/sqlalchemy/api.py
@@ -387,7 +387,7 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None,
387 return []387 return []
388 if return_base_tuple:388 if return_base_tuple:
389 # The caller expects a tuple, lets just give it to them.389 # The caller expects a tuple, lets just give it to them.
390 return res390 return [tuple(r) for r in res]
391 # Everything is a tuple in a resultset from the unified interface391 # Everything is a tuple in a resultset from the unified interface
392 # but for objects, our model expects just object access,392 # but for objects, our model expects just object access,
393 # so we extract and return them.393 # so we extract and return them.
@@ -585,16 +585,9 @@ class Connection(api.Connection):
585 # If we are not using sorting, or any other query magic,585 # If we are not using sorting, or any other query magic,
586 # we could likely just do a query execution and586 # we could likely just do a query execution and
587 # prepare the tuple responses.587 # prepare the tuple responses.
588 results = _paginate_query(models.Node, limit, marker,588 return _paginate_query(models.Node, limit, marker,
589 sort_key, sort_dir, query,589 sort_key, sort_dir, query,
590 return_base_tuple=True)590 return_base_tuple=True)
591 # Need to copy the data to close out the _paginate_query
592 # object.
593 new_result = [tuple([ent for ent in r]) for r in results]
594 # Explicitly free results so we don't hang on to it.
595 del results
596
597 return new_result
598591
599 def get_node_list(self, filters=None, limit=None, marker=None,592 def get_node_list(self, filters=None, limit=None, marker=None,
600 sort_key=None, sort_dir=None, fields=None):593 sort_key=None, sort_dir=None, fields=None):
@@ -714,14 +707,17 @@ class Connection(api.Connection):
714 values(reservation=tag).707 values(reservation=tag).
715 execution_options(synchronize_session=False))708 execution_options(synchronize_session=False))
716 session.flush()709 session.flush()
717 node = self._get_node_reservation(node.id)
718 # NOTE(TheJulia): In SQLAlchemy 2.0 style, we don't710 # NOTE(TheJulia): In SQLAlchemy 2.0 style, we don't
719 # magically get a changed node as they moved from the711 # magically get a changed node as they moved from the
720 # many ways to do things to singular ways to do things.712 # many ways to do things to singular ways to do things.
721 if res.rowcount != 1:713 if res.rowcount != 1:
722 # Nothing updated and node exists. Must already be714 # Nothing updated and node exists. Must already be
723 # locked.715 # locked. Identify who holds it and log.
724 raise exception.NodeLocked(node=node.uuid, host=node.reservation)716 if utils.is_ironic_using_sqlite():
717 lock_holder = CONF.hostname
718 else:
719 lock_holder = self._get_node_reservation(node.id).reservation
720 raise exception.NodeLocked(node=node.uuid, host=lock_holder)
725721
726 @oslo_db_api.retry_on_deadlock722 @oslo_db_api.retry_on_deadlock
727 def reserve_node(self, tag, node_id):723 def reserve_node(self, tag, node_id):
@@ -1481,7 +1477,8 @@ class Connection(api.Connection):
1481 result = session.query(1477 result = session.query(
1482 field1478 field
1483 ).filter(models.Conductor.online.is_(False))1479 ).filter(models.Conductor.online.is_(False))
1484 return [row[0] for row in result]1480 result = [row[0] for row in result]
1481 return result
14851482
1486 def get_online_conductors(self):1483 def get_online_conductors(self):
1487 with _session_for_read() as session:1484 with _session_for_read() as session:
diff --git a/ironic/drivers/modules/deploy_utils.py b/ironic/drivers/modules/deploy_utils.py
index 010e384..18a5068 100644
--- a/ironic/drivers/modules/deploy_utils.py
+++ b/ironic/drivers/modules/deploy_utils.py
@@ -1536,10 +1536,12 @@ def prepare_agent_boot(task):
1536 task.driver.boot.prepare_ramdisk(task, deploy_opts)1536 task.driver.boot.prepare_ramdisk(task, deploy_opts)
15371537
15381538
1539def reboot_to_finish_step(task):1539def reboot_to_finish_step(task, timeout=None):
1540 """Reboot the node into IPA to finish a deploy/clean step.1540 """Reboot the node into IPA to finish a deploy/clean step.
15411541
1542 :param task: a TaskManager instance.1542 :param task: a TaskManager instance.
1543 :param timeout: timeout (in seconds) positive integer (> 0) for any
1544 power state. ``None`` indicates to use default timeout.
1543 :returns: states.CLEANWAIT if cleaning operation in progress1545 :returns: states.CLEANWAIT if cleaning operation in progress
1544 or states.DEPLOYWAIT if deploy operation in progress.1546 or states.DEPLOYWAIT if deploy operation in progress.
1545 """1547 """
@@ -1552,7 +1554,7 @@ def reboot_to_finish_step(task):
1552 manager_utils.node_power_action(task, states.POWER_OFF)1554 manager_utils.node_power_action(task, states.POWER_OFF)
1553 prepare_agent_boot(task)1555 prepare_agent_boot(task)
15541556
1555 manager_utils.node_power_action(task, states.REBOOT)1557 manager_utils.node_power_action(task, states.REBOOT, timeout)
1556 return get_async_step_return_state(task.node)1558 return get_async_step_return_state(task.node)
15571559
15581560
diff --git a/ironic/drivers/modules/fake.py b/ironic/drivers/modules/fake.py
index 625ffbb..0889098 100644
--- a/ironic/drivers/modules/fake.py
+++ b/ironic/drivers/modules/fake.py
@@ -477,6 +477,8 @@ class FakeFirmware(base.FirmwareInterface):
477 'needs to contain a dictionary with name/value pairs'),477 'needs to contain a dictionary with name/value pairs'),
478 'required': True}})478 'required': True}})
479 def update(self, task, settings):479 def update(self, task, settings):
480 LOG.debug('Calling update clean step with settings %s.',
481 settings)
480 sleep(CONF.fake.firmware_delay)482 sleep(CONF.fake.firmware_delay)
481483
482 def cache_firmware_components(self, task):484 def cache_firmware_components(self, task):
diff --git a/ironic/drivers/modules/inspect_utils.py b/ironic/drivers/modules/inspect_utils.py
index 2a66edc..14caa05 100644
--- a/ironic/drivers/modules/inspect_utils.py
+++ b/ironic/drivers/modules/inspect_utils.py
@@ -342,6 +342,9 @@ def _get_bmc_addresses(node):
342 if '//' in address:342 if '//' in address:
343 address = urllib.parse.urlparse(address).hostname343 address = urllib.parse.urlparse(address).hostname
344344
345 # Strip brackets in case used on IPv6 address.
346 address = address.strip('[').strip(']')
347
345 try:348 try:
346 addrinfo = socket.getaddrinfo(address, None, proto=socket.SOL_TCP)349 addrinfo = socket.getaddrinfo(address, None, proto=socket.SOL_TCP)
347 except socket.gaierror as exc:350 except socket.gaierror as exc:
diff --git a/ironic/drivers/modules/redfish/firmware.py b/ironic/drivers/modules/redfish/firmware.py
348new file mode 100644351new file mode 100644
index 0000000..207c61a
--- /dev/null
+++ b/ironic/drivers/modules/redfish/firmware.py
@@ -0,0 +1,452 @@
1#
2# Licensed under the Apache License, Version 2.0 (the "License"); you may
3# not use this file except in compliance with the License. You may obtain
4# a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11# License for the specific language governing permissions and limitations
12# under the License.
13
14from urllib.parse import urlparse
15
16from ironic_lib import metrics_utils
17from oslo_log import log
18from oslo_utils import importutils
19from oslo_utils import timeutils
20
21from ironic.common import exception
22from ironic.common.i18n import _
23from ironic.common import states
24from ironic.conductor import periodics
25from ironic.conductor import utils as manager_utils
26from ironic.conf import CONF
27from ironic.drivers import base
28from ironic.drivers.modules import deploy_utils
29from ironic.drivers.modules.redfish import firmware_utils
30from ironic.drivers.modules.redfish import utils as redfish_utils
31from ironic import objects
32
33LOG = log.getLogger(__name__)
34
35METRICS = metrics_utils.get_metrics_logger(__name__)
36
37sushy = importutils.try_import('sushy')
38
39
40class RedfishFirmware(base.FirmwareInterface):
41
42 _FW_SETTINGS_ARGSINFO = {
43 'settings': {
44 'description': (
45 'A list of dicts with firmware components to be updated'
46 ),
47 'required': True
48 }
49 }
50
51 def __init__(self):
52 super(RedfishFirmware, self).__init__()
53 if sushy is None:
54 raise exception.DriverLoadError(
55 driver='redfish',
56 reason=_("Unable to import the sushy library"))
57
58 def get_properties(self):
59 """Return the properties of the interface.
60
61 :returns: dictionary of <property name>:<property description> entries.
62 """
63 return redfish_utils.COMMON_PROPERTIES.copy()
64
65 def validate(self, task):
66 """Validates the driver information needed by the redfish driver.
67
68 :param task: a TaskManager instance containing the node to act on.
69 :raises: InvalidParameterValue on malformed parameter(s)
70 :raises: MissingParameterValue on missing parameter(s)
71 """
72 redfish_utils.parse_driver_info(task.node)
73
74 def cache_firmware_components(self, task):
75 """Store or update Firmware Components on the given node.
76
77 This method stores Firmware Components to the firmware_information
78 table during 'cleaning' operation. It will also update the timestamp
79 of each Firmware Component.
80
81 :param task: a TaskManager instance.
82 :raises: UnsupportedDriverExtension, if the node's driver doesn't
83 support getting Firmware Components from bare metal.
84 """
85
86 node_id = task.node.id
87 settings = []
88 # NOTE(iurygregory): currently we will only retrieve BIOS and BMC
89 # firmware information trough the redfish system and manager.
90
91 system = redfish_utils.get_system(task.node)
92
93 bios_fw = {'component': 'bios',
94 'current_version': system.bios_version}
95 settings.append(bios_fw)
96
97 # NOTE(iurygregory): normally we only relay on the System to
98 # perform actions, but to retrieve the BMC Firmware we need to
99 # access the Manager.
100 try:
101 manager = redfish_utils.get_manager(task.node, system)
102 bmc_fw = {'component': 'bmc',
103 'current_version': manager.firmware_version}
104 settings.append(bmc_fw)
105 except exception.RedfishError:
106 LOG.warning('No manager available to retrieve Firmware '
107 'from the bmc of node %s', task.node.uuid)
108
109 if not settings:
110 error_msg = (_('Cannot retrieve firmware for node %s.')
111 % task.node.uuid)
112 LOG.error(error_msg)
113 raise exception.UnsupportedDriverExtension(error_msg)
114
115 create_list, update_list, nochange_list = (
116 objects.FirmwareComponentList.sync_firmware_components(
117 task.context, node_id, settings))
118
119 if create_list:
120 for new_fw in create_list:
121 new_fw_cmp = objects.FirmwareComponent(
122 task.context,
123 node_id=node_id,
124 component=new_fw['component'],
125 current_version=new_fw['current_version']
126 )
127 new_fw_cmp.create()
128 if update_list:
129 for up_fw in update_list:
130 up_fw_cmp = objects.FirmwareComponent.get(
131 task.context,
132 node_id=node_id,
133 name=up_fw['component']
134 )
135 up_fw_cmp.last_version_flashed = up_fw.get('current_version')
136 up_fw_cmp.current_version = up_fw.get('current_version')
137 up_fw_cmp.save()
138
139 @METRICS.timer('RedfishFirmware.update')
140 @base.deploy_step(priority=0, argsinfo=_FW_SETTINGS_ARGSINFO)
141 @base.clean_step(priority=0, abortable=False,
142 argsinfo=_FW_SETTINGS_ARGSINFO,
143 requires_ramdisk=True)
144 @base.cache_firmware_components
145 def update(self, task, settings):
146 """Update the Firmware on the node using the settings for components.
147
148 :param task: a TaskManager instance.
149 :param settings: a list of dictionaries, each dictionary contains the
150 component name and the url that will be used to update the
151 firmware.
152 :raises: UnsupportedDriverExtension, if the node's driver doesn't
153 support update via the interface.
154 :raises: InvalidParameterValue, if validation of the settings fails.
155 :raises: MissingParamterValue, if some required parameters are
156 missing.
157 :returns: states.CLEANWAIT if Firmware update with the settings is in
158 progress asynchronously of None if it is complete.
159 """
160 node = task.node
161
162 update_service = redfish_utils.get_update_service(node)
163
164 LOG.debug('Updating Firmware on node %(node_uuid)s with settings '
165 '%(settings)s',
166 {'node_uuid': node.uuid, 'settings': settings})
167
168 self._execute_firmware_update(node, update_service, settings)
169
170 fw_upd = settings[0]
171 wait_interval = fw_upd.get('wait')
172
173 deploy_utils.set_async_step_flags(
174 node,
175 reboot=True,
176 skip_current_step=True,
177 polling=True
178 )
179
180 return deploy_utils.reboot_to_finish_step(task, timeout=wait_interval)
181
182 def _execute_firmware_update(self, node, update_service, settings):
183 """Executes the next firmware update to the node
184
185 Executes the first firmware update in the settings list to the node.
186
187 :param node: the node that will have a firmware update executed.
188 :param update_service: the sushy firmware update service.
189 :param settings: remaining settings for firmware update that needs
190 to be executed.
191 """
192 fw_upd = settings[0]
193 component_url, cleanup = self._stage_firmware_file(node, fw_upd)
194
195 LOG.debug('Applying new firmware %(url)s for %(component)s on node '
196 '%(node_uuid)s',
197 {'url': fw_upd['url'], 'component': fw_upd['component'],
198 'node_uuid': node.uuid})
199
200 task_monitor = update_service.simple_update(component_url)
201
202 fw_upd['task_monitor'] = task_monitor.task_monitor_uri
203 node.set_driver_internal_info('redfish_fw_updates', settings)
204
205 if cleanup:
206 fw_clean = node.driver_internal_info.get('firmware_cleanup')
207 if not fw_clean:
208 fw_clean = [cleanup]
209 elif cleanup not in fw_clean:
210 fw_clean.append(cleanup)
211 node.set_driver_internal_info('firmware_cleanup', fw_clean)
212
213 def _continue_updates(self, task, update_service, settings):
214 """Continues processing the firmware updates
215
216 Continues to process the firmware updates on the node.
217
218 Note that the caller must have an exclusive lock on the node.
219
220 :param task: a TaskManager instance containing the node to act on.
221 :param update_service: the sushy firmware update service
222 :param settings: the remaining firmware updates to apply
223 """
224 node = task.node
225 fw_upd = settings[0]
226 wait_interval = fw_upd.get('wait')
227 if wait_interval:
228 time_now = str(timeutils.utcnow().isoformat())
229 fw_upd['wait_start_time'] = time_now
230
231 LOG.debug('Waiting at %(time)s for %(seconds)s seconds after '
232 '%(component)s firmware update %(url)s '
233 'on node %(node)s',
234 {'time': time_now,
235 'seconds': wait_interval,
236 'component': fw_upd['component'],
237 'url': fw_upd['url'],
238 'node': node.uuid})
239
240 node.set_driver_internal_info('redfish_fw_updates', settings)
241 node.save()
242 return
243
244 if len(settings) == 1:
245 self._clear_updates(node)
246
247 LOG.info('Firmware updates completed for node %(node)s',
248 {'node': node.uuid})
249
250 manager_utils.notify_conductor_resume_clean(task)
251 else:
252 settings.pop(0)
253 self._execute_firmware_update(node,
254 update_service,
255 settings)
256 node.save()
257 manager_utils.node_power_action(task, states.REBOOT)
258
259 def _clear_updates(self, node):
260 """Clears firmware updates artifacts
261
262 Clears firmware updates from driver_internal_info and any files
263 that were staged.
264
265 Note that the caller must have an exclusive lock on the node.
266
267 :param node: the node to clear the firmware updates from
268 """
269 firmware_utils.cleanup(node)
270 node.del_driver_internal_info('redfish_fw_updates')
271 node.del_driver_internal_info('firmware_cleanup')
272 node.save()
273
274 @METRICS.timer('RedfishFirmware._query_update_failed')
275 @periodics.node_periodic(
276 purpose='checking if async update of firmware component failed',
277 spacing=CONF.redfish.firmware_update_fail_interval,
278 filters={'reserved': False, 'provision_state': states.CLEANFAIL,
279 'maintenance': True},
280 predicate_extra_fields=['driver_internal_info'],
281 predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
282 )
283 def _query_update_failed(self, task, manager, context):
284
285 """Periodic job to check for failed firmware updates."""
286 # A firmware update failed. Discard any remaining firmware
287 # updates so when the user takes the node out of
288 # maintenance mode, pending firmware updates do not
289 # automatically continue.
290 LOG.error('Update firmware failed for node %(node)s. '
291 'Discarding remaining firmware updates.',
292 {'node': task.node.uuid})
293
294 task.upgrade_lock()
295 self._clear_updates(task.node)
296
297 @METRICS.timer('RedfishFirmware._query_update_status')
298 @periodics.node_periodic(
299 purpose='checking async update of firmware component',
300 spacing=CONF.redfish.firmware_update_fail_interval,
301 filters={'reserved': False, 'provision_state': states.CLEANWAIT},
302 predicate_extra_fields=['driver_internal_info'],
303 predicate=lambda n: n.driver_internal_info.get('redfish_fw_updates'),
304 )
305 def _query_update_status(self, task, manager, context):
306 """Periodic job to check firmware update tasks."""
307 self._check_node_redfish_firmware_update(task)
308
309 @METRICS.timer('RedfishFirmware._check_node_redfish_firmware_update')
310 def _check_node_redfish_firmware_update(self, task):
311 """Check the progress of running firmware update on a node."""
312
313 node = task.node
314
315 settings = node.driver_internal_info['redfish_fw_updates']
316 current_update = settings[0]
317
318 try:
319 update_service = redfish_utils.get_update_service(node)
320 except exception.RedfishConnectionError as e:
321 # If the BMC firmware is being updated, the BMC will be
322 # unavailable for some amount of time.
323 LOG.warning('Unable to communicate with firmware update service '
324 'on node %(node)s. Will try again on the next poll. '
325 'Error: %(error)s',
326 {'node': node.uuid,
327 'error': e})
328 return
329
330 wait_start_time = current_update.get('wait_start_time')
331 if wait_start_time:
332 wait_start = timeutils.parse_isotime(wait_start_time)
333
334 elapsed_time = timeutils.utcnow(True) - wait_start
335 if elapsed_time.seconds >= current_update['wait']:
336 LOG.debug('Finished waiting after firmware update '
337 '%(firmware_image)s on node %(node)s. '
338 'Elapsed time: %(seconds)s seconds',
339 {'firmware_image': current_update['url'],
340 'node': node.uuid,
341 'seconds': elapsed_time.seconds})
342 current_update.pop('wait', None)
343 current_update.pop('wait_start_time', None)
344
345 self._continue_updates(task, update_service, settings)
346 else:
347 LOG.debug('Continuing to wait after firmware update '
348 '%(firmware_image)s on node %(node)s. '
349 'Elapsed time: %(seconds)s seconds',
350 {'firmware_image': current_update['url'],
351 'node': node.uuid,
352 'seconds': elapsed_time.seconds})
353
354 return
355
356 try:
357 task_monitor = redfish_utils.get_task_monitor(
358 node, current_update['task_monitor'])
359 except exception.RedfishError:
360 # The BMC deleted the Task before we could query it
361 LOG.warning('Firmware update completed for node %(node)s, '
362 'firmware %(firmware_image)s, but success of the '
363 'update is unknown. Assuming update was successful.',
364 {'node': node.uuid,
365 'firmware_image': current_update['url']})
366 self._continue_updates(task, update_service, settings)
367 return
368
369 if not task_monitor.is_processing:
370 # The last response does not necessarily contain a Task,
371 # so get it
372 sushy_task = task_monitor.get_task()
373
374 # Only parse the messages if the BMC did not return parsed
375 # messages
376 messages = []
377 if sushy_task.messages and not sushy_task.messages[0].message:
378 sushy_task.parse_messages()
379
380 if sushy_task.messages is not None:
381 messages = [m.message for m in sushy_task.messages]
382
383 task.upgrade_lock()
384 if (sushy_task.task_state == sushy.TASK_STATE_COMPLETED
385 and sushy_task.task_status in
386 [sushy.HEALTH_OK, sushy.HEALTH_WARNING]):
387 LOG.info('Firmware update succeeded for node %(node)s, '
388 'firmware %(firmware_image)s: %(messages)s',
389 {'node': node.uuid,
390 'firmware_image': current_update['url'],
391 'messages': ", ".join(messages)})
392
393 self._continue_updates(task, update_service, settings)
394 else:
395 error_msg = (_('Firmware update failed for node %(node)s, '
396 'firmware %(firmware_image)s. '
397 'Error: %(errors)s') %
398 {'node': node.uuid,
399 'firmware_image': current_update['url'],
400 'errors': ", ".join(messages)})
401
402 self._clear_updates(node)
403 if task.node.clean_step:
404 manager_utils.cleaning_error_handler(task, error_msg)
405 else:
406 manager_utils.deploying_error_handler(task, error_msg)
407
408 else:
409 LOG.debug('Firmware update in progress for node %(node)s, '
410 'firmware %(firmware_image)s.',
411 {'node': node.uuid,
412 'firmware_image': current_update['url']})
413
414 def _stage_firmware_file(self, node, component_update):
415
416 try:
417 url = component_update['url']
418 name = component_update['component']
419 parsed_url = urlparse(url)
420 scheme = parsed_url.scheme.lower()
421 source = (CONF.redfish.firmware_source).lower()
422
423 # Keep it simple, in further processing TLS does not matter
424 if scheme == 'https':
425 scheme = 'http'
426
427 # If source and scheme is HTTP, then no staging,
428 # returning original location
429 if scheme == 'http' and source == scheme:
430 LOG.debug('For node %(node)s serving firmware for '
431 '%(component)s from original location %(url)s',
432 {'node': node.uuid, 'component': name, 'url': url})
433 return url, None
434
435 # If source and scheme is Swift, then not moving, but
436 # returning Swift temp URL
437 if scheme == 'swift' and source == scheme:
438 temp_url = firmware_utils.get_swift_temp_url(parsed_url)
439 LOG.debug('For node %(node)s serving original firmware at '
440 'for %(component)s at %(url)s via Swift temporary '
441 'url %(temp_url)s',
442 {'node': node.uuid, 'component': name, 'url': url,
443 'temp_url': temp_url})
444 return temp_url, None
445
446 # For remaining, download the image to temporary location
447 temp_file = firmware_utils.download_to_temp(node, url)
448
449 return firmware_utils.stage(node, source, temp_file)
450
451 except exception.IronicException:
452 firmware_utils.cleanup(node)
diff --git a/ironic/drivers/modules/redfish/firmware_utils.py b/ironic/drivers/modules/redfish/firmware_utils.py
index feeec2d..843597d 100644
--- a/ironic/drivers/modules/redfish/firmware_utils.py
+++ b/ironic/drivers/modules/redfish/firmware_utils.py
@@ -63,6 +63,36 @@ _UPDATE_FIRMWARE_SCHEMA = {
63 "additionalProperties": False63 "additionalProperties": False
64 }64 }
65}65}
66
67_FIRMWARE_INTERFACE_UPDATE_SCHEMA = {
68 "$schema": "http://json-schema.org/schema#",
69 "title": "update_firmware clean step schema",
70 "type": "array",
71 # list of firmware update images
72 "items": {
73 "type": "object",
74 "required": ["component", "url"],
75 "properties": {
76 "component": {
77 "description": "name of the firmware component to be updated",
78 "type": "string",
79 "minLenght": 1
80 },
81 "url": {
82 "description": "URL for firmware file",
83 "type": "string",
84 "minLength": 1
85 },
86 "wait": {
87 "description": "optional wait time for firmware update",
88 "type": "integer",
89 "minimum": 1
90 }
91 },
92 "additionalProperties": False
93 }
94}
95
66_FIRMWARE_SUBDIR = 'firmware'96_FIRMWARE_SUBDIR = 'firmware'
6797
6898
@@ -80,6 +110,20 @@ def validate_update_firmware_args(firmware_images):
80 % {'firmware_images': firmware_images, 'err': err})110 % {'firmware_images': firmware_images, 'err': err})
81111
82112
113def validate_firmware_interface_update_args(settings):
114 """Validate ``update`` step input argument
115
116 :param settings: args to validate.
117 :raises: InvalidParameterValue When argument is not valid
118 """
119 try:
120 jsonschema.validate(settings, _FIRMWARE_INTERFACE_UPDATE_SCHEMA)
121 except jsonschema.ValidationError as err:
122 raise exception.InvalidParameterValue(
123 _('Invalid firmware update %(settings)s. Errors: %(err)s')
124 % {'settings': settings, 'err': err})
125
126
83def get_swift_temp_url(parsed_url):127def get_swift_temp_url(parsed_url):
84 """Gets Swift temporary URL128 """Gets Swift temporary URL
85129
diff --git a/ironic/drivers/modules/redfish/management.py b/ironic/drivers/modules/redfish/management.py
index 7f7dbe7..d0b8045 100644
--- a/ironic/drivers/modules/redfish/management.py
+++ b/ironic/drivers/modules/redfish/management.py
@@ -14,6 +14,7 @@
14# under the License.14# under the License.
1515
16import collections16import collections
17import time
17from urllib.parse import urlparse18from urllib.parse import urlparse
1819
19from ironic_lib import metrics_utils20from ironic_lib import metrics_utils
@@ -44,6 +45,8 @@ METRICS = metrics_utils.get_metrics_logger(__name__)
4445
45sushy = importutils.try_import('sushy')46sushy = importutils.try_import('sushy')
4647
48BOOT_MODE_CONFIG_INTERVAL = 15
49
47if sushy:50if sushy:
48 BOOT_DEVICE_MAP = {51 BOOT_DEVICE_MAP = {
49 sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE,52 sushy.BOOT_SOURCE_TARGET_PXE: boot_devices.PXE,
@@ -327,9 +330,13 @@ class RedfishManagement(base.ManagementInterface):
327 """330 """
328 system = redfish_utils.get_system(task.node)331 system = redfish_utils.get_system(task.node)
329332
333 # NOTE(dtantsur): check the readability of the current mode before
334 # modifying anything. I suspect it can become None transiently after
335 # the update, while we need to know if it is supported *at all*.
336 get_mode_unsupported = (system.boot.get('mode') is None)
337
330 try:338 try:
331 system.set_system_boot_options(mode=BOOT_MODE_MAP_REV[mode])339 system.set_system_boot_options(mode=BOOT_MODE_MAP_REV[mode])
332
333 except sushy.exceptions.SushyError as e:340 except sushy.exceptions.SushyError as e:
334 error_msg = (_('Setting boot mode to %(mode)s '341 error_msg = (_('Setting boot mode to %(mode)s '
335 'failed for node %(node)s. '342 'failed for node %(node)s. '
@@ -342,7 +349,7 @@ class RedfishManagement(base.ManagementInterface):
342 # getting or setting the boot mode. When setting failed and the349 # getting or setting the boot mode. When setting failed and the
343 # mode attribute is missing from the boot field, raising350 # mode attribute is missing from the boot field, raising
344 # UnsupportedDriverExtension will allow the deploy to continue.351 # UnsupportedDriverExtension will allow the deploy to continue.
345 if system.boot.get('mode') is None:352 if get_mode_unsupported:
346 LOG.info(_('Attempt to set boot mode on node %(node)s '353 LOG.info(_('Attempt to set boot mode on node %(node)s '
347 'failed to set boot mode as the node does not '354 'failed to set boot mode as the node does not '
348 'appear to support overriding the boot mode. '355 'appear to support overriding the boot mode. '
@@ -352,6 +359,66 @@ class RedfishManagement(base.ManagementInterface):
352 driver=task.node.driver, extension='set_boot_mode')359 driver=task.node.driver, extension='set_boot_mode')
353 raise exception.RedfishError(error=error_msg)360 raise exception.RedfishError(error=error_msg)
354361
362 # NOTE(dtantsur): this case is rather hypothetical, but in our own
363 # emulator, it's possible that mode is constantly set to None, while
364 # the request to change the mode succeeds.
365 if get_mode_unsupported:
366 LOG.warning('The request to set boot mode for node %(node)s to '
367 '%(value)s has succeeded, but the current mode is '
368 'not known. Skipping reboot and assuming '
369 'the operation has succeeded.',
370 {'node': task.node.uuid, 'value': mode})
371 return
372
373 self._wait_for_boot_mode(task, system, mode)
374 LOG.info('Boot mode for node %(node)s has been set to '
375 '%(value)s', {'node': task.node.uuid, 'value': mode})
376
377 def _wait_for_boot_mode(self, task, system, mode):
378 system.refresh(force=True)
379
380 # NOTE(dtantsur/janders): at least Dell machines change boot mode via
381 # a BIOS configuration job. A reboot is needed to apply it.
382 if system.boot.get('mode') == BOOT_MODE_MAP_REV[mode]:
383 LOG.debug('Node %(node)s is already configured with requested '
384 'boot mode %(new_value)s.',
385 {'node': task.node.uuid,
386 'new_value': BOOT_MODE_MAP_REV[mode]})
387 return
388
389 LOG.info('Rebooting node %(node)s to change boot mode from '
390 '%(old_value)s to %(new_value)s',
391 {'node': task.node.uuid,
392 'old_value': system.boot.get('mode'),
393 'new_value': BOOT_MODE_MAP_REV[mode]})
394
395 old_power_state = task.driver.power.get_power_state(task)
396 manager_utils.node_power_action(task, states.REBOOT)
397
398 if CONF.redfish.boot_mode_config_timeout:
399 threshold = time.time() + CONF.redfish.boot_mode_config_timeout
400 while (time.time() <= threshold
401 and system.boot.get('mode') != BOOT_MODE_MAP_REV[mode]):
402 LOG.debug('Still waiting for boot mode of node %(node)s '
403 'to become %(value)s, current is %(current)s',
404 {'node': task.node.uuid,
405 'value': BOOT_MODE_MAP_REV[mode],
406 'current': system.boot.get('mode')})
407 time.sleep(BOOT_MODE_CONFIG_INTERVAL)
408 system.refresh(force=True)
409
410 if system.boot.get('mode') != BOOT_MODE_MAP_REV[mode]:
411 msg = (_('Timeout reached while waiting for boot mode of '
412 'node %(node)s to become %(value)s, '
413 'current is %(current)s')
414 % {'node': task.node.uuid,
415 'value': BOOT_MODE_MAP_REV[mode],
416 'current': system.boot.get('mode')})
417 LOG.error(msg)
418 raise exception.RedfishError(error=msg)
419
420 manager_utils.node_power_action(task, old_power_state)
421
355 def get_boot_mode(self, task):422 def get_boot_mode(self, task):
356 """Get the current boot mode for a node.423 """Get the current boot mode for a node.
357424
@@ -1142,9 +1209,45 @@ class RedfishManagement(base.ManagementInterface):
1142 % {'node': task.node.uuid, 'value': state, 'exc': exc})1209 % {'node': task.node.uuid, 'value': state, 'exc': exc})
1143 LOG.error(msg)1210 LOG.error(msg)
1144 raise exception.RedfishError(error=msg)1211 raise exception.RedfishError(error=msg)
1145 else:1212
1146 LOG.info('Secure boot state for node %(node)s has been set to '1213 self._wait_for_secure_boot(task, sb, state)
1147 '%(value)s', {'node': task.node.uuid, 'value': state})1214 LOG.info('Secure boot state for node %(node)s has been set to '
1215 '%(value)s', {'node': task.node.uuid, 'value': state})
1216
1217 def _wait_for_secure_boot(self, task, sb, state):
1218 # NOTE(dtantsur): at least Dell machines change secure boot status via
1219 # a BIOS configuration job. A reboot is needed to apply it.
1220 sb.refresh(force=True)
1221 if sb.enabled == state:
1222 return
1223
1224 LOG.info('Rebooting node %(node)s to change secure boot state to '
1225 '%(value)s', {'node': task.node.uuid, 'value': state})
1226
1227 old_power_state = task.driver.power.get_power_state(task)
1228 manager_utils.node_power_action(task, states.REBOOT)
1229
1230 if CONF.redfish.boot_mode_config_timeout:
1231 threshold = time.time() + CONF.redfish.boot_mode_config_timeout
1232 while time.time() <= threshold and sb.enabled != state:
1233 LOG.debug(
1234 'Still waiting for secure boot state of node %(node)s '
1235 'to become %(value)s, current is %(current)s',
1236 {'node': task.node.uuid, 'value': state,
1237 'current': sb.enabled})
1238 time.sleep(BOOT_MODE_CONFIG_INTERVAL)
1239 sb.refresh(force=True)
1240
1241 if sb.enabled != state:
1242 msg = (_('Timeout reached while waiting for secure boot state '
1243 'of node %(node)s to become %(state)s, '
1244 'current is %(current)s')
1245 % {'node': task.node.uuid, 'state': state,
1246 'current': sb.enabled})
1247 LOG.error(msg)
1248 raise exception.RedfishError(error=msg)
1249
1250 manager_utils.node_power_action(task, old_power_state)
11481251
1149 def _reset_keys(self, task, reset_type):1252 def _reset_keys(self, task, reset_type):
1150 system = redfish_utils.get_system(task.node)1253 system = redfish_utils.get_system(task.node)
diff --git a/ironic/drivers/modules/redfish/utils.py b/ironic/drivers/modules/redfish/utils.py
index e85e2ec..4182c84 100644
--- a/ironic/drivers/modules/redfish/utils.py
+++ b/ironic/drivers/modules/redfish/utils.py
@@ -28,6 +28,7 @@ import tenacity
2828
29from ironic.common import exception29from ironic.common import exception
30from ironic.common.i18n import _30from ironic.common.i18n import _
31from ironic.common import utils
31from ironic.conf import CONF32from ironic.conf import CONF
3233
33sushy = importutils.try_import('sushy')34sushy = importutils.try_import('sushy')
@@ -97,7 +98,7 @@ def parse_driver_info(node):
97 'info': missing_info})98 'info': missing_info})
9899
99 # Validate the Redfish address100 # Validate the Redfish address
100 address = driver_info['redfish_address']101 address = utils.wrap_ipv6(driver_info['redfish_address'])
101 try:102 try:
102 parsed = rfc3986.uri_reference(address)103 parsed = rfc3986.uri_reference(address)
103 except TypeError:104 except TypeError:
@@ -474,3 +475,39 @@ def wait_until_get_system_ready(node):
474 driver_info = parse_driver_info(node)475 driver_info = parse_driver_info(node)
475 system_id = driver_info['system_id']476 system_id = driver_info['system_id']
476 return _get_system(driver_info, system_id)477 return _get_system(driver_info, system_id)
478
479
480def get_manager(node, system, manager_id=None):
481 """Get a node's manager.
482
483 :param system: a Sushy system object
484 :param manager_id: the id of the manager
485 :return: a sushy Manager
486 :raises: RedfishError when the System doesn't have Managers associated
487 """
488
489 try:
490 sushy_manager = None
491 available_managers = system.managers
492 if available_managers:
493 if manager_id is None:
494 sushy_manager = available_managers[0]
495 else:
496 for manager in available_managers:
497 if manager.identity == manager_id:
498 sushy_manager = manager
499 if sushy_manager is None:
500 raise Exception("Couldn't find any Sushy Manager")
501 return sushy_manager
502 except sushy.exceptions.MissingAttributeError as e:
503 LOG.error('Redfish Managers for node %(node)s are not associated '
504 'with system %(system)s. Error %(error)s',
505 {'system': system.identity,
506 'node': node.uuid, 'error': e})
507 raise exception.RedfishError(error=e)
508 except Exception as exc:
509 LOG.error('Redfish Manager was not found for '
510 'node %(node)s under system %(system)s. Error %(error)s',
511 {'system': system.identity,
512 'node': node.uuid, 'error': exc})
513 raise exception.RedfishError(error=exc)
diff --git a/ironic/drivers/redfish.py b/ironic/drivers/redfish.py
index 3852cd3..094119e 100644
--- a/ironic/drivers/redfish.py
+++ b/ironic/drivers/redfish.py
@@ -21,6 +21,7 @@ from ironic.drivers.modules import noop_mgmt
21from ironic.drivers.modules import pxe21from ironic.drivers.modules import pxe
22from ironic.drivers.modules.redfish import bios as redfish_bios22from ironic.drivers.modules.redfish import bios as redfish_bios
23from ironic.drivers.modules.redfish import boot as redfish_boot23from ironic.drivers.modules.redfish import boot as redfish_boot
24from ironic.drivers.modules.redfish import firmware as redfish_firmware
24from ironic.drivers.modules.redfish import inspect as redfish_inspect25from ironic.drivers.modules.redfish import inspect as redfish_inspect
25from ironic.drivers.modules.redfish import management as redfish_mgmt26from ironic.drivers.modules.redfish import management as redfish_mgmt
26from ironic.drivers.modules.redfish import power as redfish_power27from ironic.drivers.modules.redfish import power as redfish_power
@@ -69,3 +70,7 @@ class RedfishHardware(generic.GenericHardware):
69 def supported_raid_interfaces(self):70 def supported_raid_interfaces(self):
70 """List of supported raid interfaces."""71 """List of supported raid interfaces."""
71 return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]72 return [redfish_raid.RedfishRAID, noop.NoRAID, agent.AgentRAID]
73
74 @property
75 def supported_firmware_interfaces(self):
76 return [redfish_firmware.RedfishFirmware, noop.NoFirmware]
diff --git a/ironic/objects/firmware.py b/ironic/objects/firmware.py
index 2a0f5f2..d30bc16 100644
--- a/ironic/objects/firmware.py
+++ b/ironic/objects/firmware.py
@@ -145,11 +145,15 @@ class FirmwareComponentList(base.IronicObjectListBase, base.IronicObject):
145 for cmp in components:145 for cmp in components:
146 if cmp['component'] in current_components_dict:146 if cmp['component'] in current_components_dict:
147 values = current_components_dict[cmp['component']]147 values = current_components_dict[cmp['component']]
148148 if values.get('last_version_flashed') is None:
149 cv_changed = cmp['current_version'] \149 lvf_changed = False
150 != values.get('current_version')150 cv_changed = cmp['current_version'] \
151 lvf_changed = cmp['last_version_flashed'] \151 != values.get('current_version')
152 != values.get('last_version_flashed')152 else:
153 lvf_changed = cmp['current_version'] \
154 != values.get('last_version_flashed')
155 cv_changed = cmp['current_version'] \
156 != values.get('current_version')
153157
154 if cv_changed or lvf_changed:158 if cv_changed or lvf_changed:
155 update_list.append(cmp)159 update_list.append(cmp)
diff --git a/ironic/tests/unit/api/base.py b/ironic/tests/unit/api/base.py
index 5f53e30..7a0638f 100644
--- a/ironic/tests/unit/api/base.py
+++ b/ironic/tests/unit/api/base.py
@@ -104,7 +104,6 @@ class BaseApiTest(db_base.DbTestCase):
104 :param path_prefix: prefix of the url path104 :param path_prefix: prefix of the url path
105 """105 """
106 full_path = path_prefix + path106 full_path = path_prefix + path
107 print('%s: %s %s' % (method.upper(), full_path, params))
108 response = getattr(self.app, "%s_json" % method)(107 response = getattr(self.app, "%s_json" % method)(
109 str(full_path),108 str(full_path),
110 params=params,109 params=params,
@@ -113,7 +112,7 @@ class BaseApiTest(db_base.DbTestCase):
113 extra_environ=extra_environ,112 extra_environ=extra_environ,
114 expect_errors=expect_errors113 expect_errors=expect_errors
115 )114 )
116 print('GOT:%s' % response)115 print(method.upper(), full_path, "WITH", params, "GOT", str(response))
117 return response116 return response
118117
119 def put_json(self, path, params, expect_errors=False, headers=None,118 def put_json(self, path, params, expect_errors=False, headers=None,
@@ -187,13 +186,12 @@ class BaseApiTest(db_base.DbTestCase):
187 :param path_prefix: prefix of the url path186 :param path_prefix: prefix of the url path
188 """187 """
189 full_path = path_prefix + path188 full_path = path_prefix + path
190 print('DELETE: %s' % (full_path))
191 response = self.app.delete(str(full_path),189 response = self.app.delete(str(full_path),
192 headers=headers,190 headers=headers,
193 status=status,191 status=status,
194 extra_environ=extra_environ,192 extra_environ=extra_environ,
195 expect_errors=expect_errors)193 expect_errors=expect_errors)
196 print('GOT:%s' % response)194 print("DELETE", full_path, "GOT", str(response))
197 return response195 return response
198196
199 def get_json(self, path, expect_errors=False, headers=None,197 def get_json(self, path, expect_errors=False, headers=None,
@@ -225,15 +223,14 @@ class BaseApiTest(db_base.DbTestCase):
225 all_params.update(params)223 all_params.update(params)
226 if q:224 if q:
227 all_params.update(query_params)225 all_params.update(query_params)
228 print('GET: %s %r' % (full_path, all_params))
229 response = self.app.get(full_path,226 response = self.app.get(full_path,
230 params=all_params,227 params=all_params,
231 headers=headers,228 headers=headers,
232 extra_environ=extra_environ,229 extra_environ=extra_environ,
233 expect_errors=expect_errors)230 expect_errors=expect_errors)
231 print("GET", full_path, "WITH", params, "GOT", str(response))
234 if not expect_errors:232 if not expect_errors:
235 response = response.json233 response = response.json
236 print('GOT:%s' % response)
237 return response234 return response
238235
239 def validate_link(self, link, bookmark=False, headers=None):236 def validate_link(self, link, bookmark=False, headers=None):
diff --git a/ironic/tests/unit/api/controllers/v1/test_port.py b/ironic/tests/unit/api/controllers/v1/test_port.py
index 31885f4..088fe3c 100644
--- a/ironic/tests/unit/api/controllers/v1/test_port.py
+++ b/ironic/tests/unit/api/controllers/v1/test_port.py
@@ -1114,7 +1114,6 @@ class TestListPortsByShard(test_api_base.BaseApiTest):
11141114
1115 res = self.get_json('/ports?shard=shard1,shard2', headers=self.headers)1115 res = self.get_json('/ports?shard=shard1,shard2', headers=self.headers)
1116 self.assertEqual(2, len(res['ports']))1116 self.assertEqual(2, len(res['ports']))
1117 print(res['ports'][0])
1118 self.assertNotEqual(res['ports'][0]['address'], bad_shard_address)1117 self.assertNotEqual(res['ports'][0]['address'], bad_shard_address)
1119 self.assertNotEqual(res['ports'][1]['address'], bad_shard_address)1118 self.assertNotEqual(res['ports'][1]['address'], bad_shard_address)
11201119
diff --git a/ironic/tests/unit/api/test_acl.py b/ironic/tests/unit/api/test_acl.py
index 0481843..ded1d0b 100644
--- a/ironic/tests/unit/api/test_acl.py
+++ b/ironic/tests/unit/api/test_acl.py
@@ -102,7 +102,6 @@ class TestACLBase(base.BaseApiTest):
102 # in troubleshooting ACL testing. This is a pattern102 # in troubleshooting ACL testing. This is a pattern
103 # followed in API unit testing in ironic, and103 # followed in API unit testing in ironic, and
104 # really does help.104 # really does help.
105 print('API ACL Testing Path %s %s' % (method, path))
106 if headers:105 if headers:
107 for k, v in headers.items():106 for k, v in headers.items():
108 rheaders[k] = v.format(**self.format_data)107 rheaders[k] = v.format(**self.format_data)
@@ -185,8 +184,6 @@ class TestACLBase(base.BaseApiTest):
185 if assert_dict_contains:184 if assert_dict_contains:
186 for k, v in assert_dict_contains.items():185 for k, v in assert_dict_contains.items():
187 self.assertIn(k, response)186 self.assertIn(k, response)
188 print(k)
189 print(v)
190 if str(v) == "None":187 if str(v) == "None":
191 # Compare since the variable loaded from the188 # Compare since the variable loaded from the
192 # json ends up being null in json or None.189 # json ends up being null in json or None.
@@ -219,13 +216,6 @@ class TestACLBase(base.BaseApiTest):
219 # a filtered view in these cases.216 # a filtered view in these cases.
220 self.assertEqual(0, len(items))217 self.assertEqual(0, len(items))
221218
222 # NOTE(TheJulia): API tests in Ironic tend to have a pattern
223 # to print request and response data to aid in development
224 # and troubleshooting. As such the prints should remain,
225 # at least until we are through primary development of the
226 # this test suite.
227 print('ACL Test GOT %s' % response)
228
229219
230@ddt.ddt220@ddt.ddt
231class TestRBACBasic(TestACLBase):221class TestRBACBasic(TestACLBase):
diff --git a/ironic/tests/unit/common/test_neutron.py b/ironic/tests/unit/common/test_neutron.py
index c3595a1..fe238df 100644
--- a/ironic/tests/unit/common/test_neutron.py
+++ b/ironic/tests/unit/common/test_neutron.py
@@ -678,7 +678,6 @@ class TestNeutronNetworkActions(db_base.DbTestCase):
678678
679 network_data = neutron.get_neutron_port_data('port1', 'vif1')679 network_data = neutron.get_neutron_port_data('port1', 'vif1')
680680
681 print(network_data)
682 expected_port = {681 expected_port = {
683 'id': 'port1',682 'id': 'port1',
684 'type': 'vif',683 'type': 'vif',
diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py
index 190bdfb..a57441f 100644
--- a/ironic/tests/unit/common/test_pxe_utils.py
+++ b/ironic/tests/unit/common/test_pxe_utils.py
@@ -895,9 +895,6 @@ class TestPXEUtils(db_base.DbTestCase):
895 expected_info = [{'opt_name': '67',895 expected_info = [{'opt_name': '67',
896 'opt_value': bootfile,896 'opt_value': bootfile,
897 'ip_version': ip_version},897 'ip_version': ip_version},
898 {'opt_name': '210',
899 'opt_value': '/tftp-path/',
900 'ip_version': ip_version},
901 {'opt_name': '66',898 {'opt_name': '66',
902 'opt_value': '192.0.2.1',899 'opt_value': '192.0.2.1',
903 'ip_version': ip_version},900 'ip_version': ip_version},
@@ -2165,8 +2162,6 @@ class iPXEBuildConfigOptionsTestCase(db_base.DbTestCase):
2165 if iso_boot:2162 if iso_boot:
2166 self.node.instance_info = {'boot_iso': 'http://test.url/file.iso'}2163 self.node.instance_info = {'boot_iso': 'http://test.url/file.iso'}
2167 self.node.save()2164 self.node.save()
2168 print(expected_options)
2169 print(image_info)
2170 iso_url = os.path.join(http_url, self.node.uuid, 'boot_iso')2165 iso_url = os.path.join(http_url, self.node.uuid, 'boot_iso')
2171 expected_options.update(2166 expected_options.update(
2172 {2167 {
diff --git a/ironic/tests/unit/common/test_utils.py b/ironic/tests/unit/common/test_utils.py
index 8d8e4aa..96c6612 100644
--- a/ironic/tests/unit/common/test_utils.py
+++ b/ironic/tests/unit/common/test_utils.py
@@ -321,6 +321,12 @@ class GenericUtilsTestCase(base.TestCase):
321 self.assertFalse(utils.is_fips_enabled())321 self.assertFalse(utils.is_fips_enabled())
322 m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')322 m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')
323323
324 def test_wrap_ipv6(self):
325 self.assertEqual('[2001:DB8::1]', utils.wrap_ipv6('2001:DB8::1'))
326 self.assertEqual('example.com', utils.wrap_ipv6('example.com'))
327 self.assertEqual('192.168.24.1', utils.wrap_ipv6('192.168.24.1'))
328 self.assertEqual('[2001:DB8::1]', utils.wrap_ipv6('[2001:DB8::1]'))
329
324330
325class TempFilesTestCase(base.TestCase):331class TempFilesTestCase(base.TestCase):
326332
diff --git a/ironic/tests/unit/conductor/test_steps.py b/ironic/tests/unit/conductor/test_steps.py
index 09d267a..64317c1 100644
--- a/ironic/tests/unit/conductor/test_steps.py
+++ b/ironic/tests/unit/conductor/test_steps.py
@@ -585,6 +585,11 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
585 'abortable': False, 'argsinfo': None, 'interface': 'vendor',585 'abortable': False, 'argsinfo': None, 'interface': 'vendor',
586 'priority': 1, 'requires_ramdisk': True,586 'priority': 1, 'requires_ramdisk': True,
587 'step': 'log_passthrough'}587 'step': 'log_passthrough'}
588 self.firmware_step = {
589 'abortable': False, 'argsinfo': {}, 'interface': 'firmware',
590 'priority': 0, 'requires_ramdisk': True,
591 'step': 'update'
592 }
588593
589 # Automated cleaning should be executed in this order594 # Automated cleaning should be executed in this order
590 self.clean_steps = [self.deploy_erase, self.power_update,595 self.clean_steps = [self.deploy_erase, self.power_update,
@@ -595,6 +600,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
595 'argsinfo': {'arg1': {'description': 'desc1', 'required': True},600 'argsinfo': {'arg1': {'description': 'desc1', 'required': True},
596 'arg2': {'description': 'desc2'}}}601 'arg2': {'description': 'desc2'}}}
597602
603 @mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
604 lambda self, taks: [])
598 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',605 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
599 lambda self, task: [])606 lambda self, task: [])
600 @mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps',607 @mock.patch('ironic.drivers.modules.fake.FakeDeploy.get_clean_steps',
@@ -619,6 +626,8 @@ class NodeCleaningStepsTestCase(db_base.DbTestCase):
619626
620 self.assertEqual(self.clean_steps, steps)627 self.assertEqual(self.clean_steps, steps)
621628
629 @mock.patch('ironic.drivers.modules.fake.FakeFirmware.get_clean_steps',
630 lambda self, task: [])
622 @mock.patch('ironic.drivers.modules.fake.FakeVendorB.get_clean_steps',631 @mock.patch('ironic.drivers.modules.fake.FakeVendorB.get_clean_steps',
623 lambda self, task: [])632 lambda self, task: [])
624 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',633 @mock.patch('ironic.drivers.modules.fake.FakeBIOS.get_clean_steps',
diff --git a/ironic/tests/unit/db/test_nodes.py b/ironic/tests/unit/db/test_nodes.py
index 3dee718..b10d367 100644
--- a/ironic/tests/unit/db/test_nodes.py
+++ b/ironic/tests/unit/db/test_nodes.py
@@ -15,6 +15,7 @@
1515
16"""Tests for manipulating Nodes via the DB API"""16"""Tests for manipulating Nodes via the DB API"""
1717
18import copy
18import datetime19import datetime
19from unittest import mock20from unittest import mock
2021
@@ -29,6 +30,7 @@ from sqlalchemy.orm import exc as sa_orm_exc
2930
30from ironic.common import exception31from ironic.common import exception
31from ironic.common import states32from ironic.common import states
33from ironic.common import utils as common_utils
32from ironic.db.sqlalchemy import api as dbapi34from ironic.db.sqlalchemy import api as dbapi
33from ironic.db.sqlalchemy.api import Connection as db_conn35from ironic.db.sqlalchemy.api import Connection as db_conn
34from ironic.db.sqlalchemy.models import NodeInventory36from ironic.db.sqlalchemy.models import NodeInventory
@@ -1037,6 +1039,39 @@ class DbNodeTestCase(base.DbTestCase):
1037 res = self.dbapi.get_node_by_uuid(uuid)1039 res = self.dbapi.get_node_by_uuid(uuid)
1038 self.assertEqual(r1, res.reservation)1040 self.assertEqual(r1, res.reservation)
10391041
1042 def test_reserve_node_reads_reservation_once_sqlite(self):
1043 node = utils.create_test_node()
1044 uuid = node.uuid
1045
1046 r1 = 'fake-reservation'
1047
1048 with mock.patch.object(db_conn, '_get_node_reservation',
1049 autospec=True) as mock_get_res:
1050 mock_get_res.return_value = node
1051 self.dbapi.reserve_node(r1, uuid)
1052 mock_get_res.assert_called_once_with(mock.ANY, node.uuid)
1053
1054 @mock.patch.object(common_utils, 'is_ironic_using_sqlite', autospec=True)
1055 def test_reserve_node_reads_reservation_twice(self, is_sqlite_mock):
1056 # Ensure we re-query for who holds the reservation *when* lock fails
1057 # to trigger.
1058 node = utils.create_test_node()
1059 uuid = node.uuid
1060 is_sqlite_mock.return_value = False
1061 r1 = 'fake-reservation'
1062 self.dbapi.update_node(node.id, {'reservation': r1})
1063 locked_node = copy.copy(node)
1064 locked_node.reservation = r1
1065 with mock.patch.object(db_conn, '_get_node_reservation',
1066 autospec=True) as mock_get_res:
1067 mock_get_res.side_effect = [node, locked_node]
1068 self.assertRaisesRegex(exception.NodeLocked,
1069 'locked by host fake-reservation',
1070 self.dbapi.reserve_node, r1, uuid)
1071 mock_get_res.assert_has_calls([
1072 mock.call(mock.ANY, node.uuid),
1073 mock.call(mock.ANY, node.id)])
1074
1040 def test_release_reservation(self):1075 def test_release_reservation(self):
1041 node = utils.create_test_node()1076 node = utils.create_test_node()
1042 uuid = node.uuid1077 uuid = node.uuid
diff --git a/ironic/tests/unit/drivers/modules/ilo/test_management.py b/ironic/tests/unit/drivers/modules/ilo/test_management.py
index f087c4d..9379954 100644
--- a/ironic/tests/unit/drivers/modules/ilo/test_management.py
+++ b/ironic/tests/unit/drivers/modules/ilo/test_management.py
@@ -1681,7 +1681,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1681 ilo_mock_object.do_disk_erase.assert_called_once_with(1681 ilo_mock_object.do_disk_erase.assert_called_once_with(
1682 'HDD', 'overwrite')1682 'HDD', 'overwrite')
1683 self.assertEqual(states.CLEANWAIT, result)1683 self.assertEqual(states.CLEANWAIT, result)
1684 mock_power.assert_called_once_with(task, states.REBOOT)1684 mock_power.assert_called_once_with(task, states.REBOOT, None)
16851685
1686 @mock.patch.object(deploy_utils, 'build_agent_options',1686 @mock.patch.object(deploy_utils, 'build_agent_options',
1687 autospec=True)1687 autospec=True)
@@ -1712,7 +1712,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1712 ilo_mock_object.do_disk_erase.assert_called_once_with(1712 ilo_mock_object.do_disk_erase.assert_called_once_with(
1713 'SSD', 'block')1713 'SSD', 'block')
1714 self.assertEqual(states.CLEANWAIT, result)1714 self.assertEqual(states.CLEANWAIT, result)
1715 mock_power.assert_called_once_with(task, states.REBOOT)1715 mock_power.assert_called_once_with(task, states.REBOOT, None)
17161716
1717 @mock.patch.object(deploy_utils, 'build_agent_options',1717 @mock.patch.object(deploy_utils, 'build_agent_options',
1718 autospec=True)1718 autospec=True)
@@ -1746,7 +1746,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1746 ilo_mock_object.do_disk_erase.assert_called_once_with(1746 ilo_mock_object.do_disk_erase.assert_called_once_with(
1747 'SSD', 'block')1747 'SSD', 'block')
1748 self.assertEqual(states.CLEANWAIT, result)1748 self.assertEqual(states.CLEANWAIT, result)
1749 mock_power.assert_called_once_with(task, states.REBOOT)1749 mock_power.assert_called_once_with(task, states.REBOOT, None)
17501750
1751 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)1751 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)
1752 @mock.patch.object(ilo_management.Ilo5Management,1752 @mock.patch.object(ilo_management.Ilo5Management,
@@ -1802,7 +1802,7 @@ class Ilo5ManagementTestCase(db_base.DbTestCase):
1802 ilo_mock_object.do_disk_erase.assert_called_once_with(1802 ilo_mock_object.do_disk_erase.assert_called_once_with(
1803 'HDD', 'zero')1803 'HDD', 'zero')
1804 self.assertEqual(states.CLEANWAIT, result)1804 self.assertEqual(states.CLEANWAIT, result)
1805 mock_power.assert_called_once_with(task, states.REBOOT)1805 mock_power.assert_called_once_with(task, states.REBOOT, None)
18061806
1807 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)1807 @mock.patch.object(ilo_management.LOG, 'info', autospec=True)
1808 @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True)1808 @mock.patch.object(ilo_common, 'get_ilo_object', autospec=True)
diff --git a/ironic/tests/unit/drivers/modules/ilo/test_raid.py b/ironic/tests/unit/drivers/modules/ilo/test_raid.py
index fcb0314..c102a5d 100644
--- a/ironic/tests/unit/drivers/modules/ilo/test_raid.py
+++ b/ironic/tests/unit/drivers/modules/ilo/test_raid.py
@@ -84,7 +84,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
84 self.assertFalse(84 self.assertFalse(
85 task.node.driver_internal_info.get(85 task.node.driver_internal_info.get(
86 'skip_current_deploy_step'))86 'skip_current_deploy_step'))
87 mock_reboot.assert_called_once_with(task, states.REBOOT)87 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
8888
89 def test__prepare_for_read_raid_create_raid_cleaning(self):89 def test__prepare_for_read_raid_create_raid_cleaning(self):
90 self.node.clean_step = {'step': 'create_configuration',90 self.node.clean_step = {'step': 'create_configuration',
@@ -122,7 +122,7 @@ class Ilo5RAIDTestCase(db_base.DbTestCase):
122 self.assertEqual(122 self.assertEqual(
123 task.node.driver_internal_info.get(123 task.node.driver_internal_info.get(
124 'skip_current_deploy_step'), False)124 'skip_current_deploy_step'), False)
125 mock_reboot.assert_called_once_with(task, states.REBOOT)125 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
126126
127 def test__prepare_for_read_raid_delete_raid_cleaning(self):127 def test__prepare_for_read_raid_delete_raid_cleaning(self):
128 self.node.clean_step = {'step': 'create_configuration',128 self.node.clean_step = {'step': 'create_configuration',
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_bios.py b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
index 1d30730..bccaad0 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_bios.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_bios.py
@@ -203,10 +203,11 @@ class RedfishBiosTestCase(db_base.DbTestCase):
203 if fast_track:203 if fast_track:
204 mock_power_action.assert_has_calls([204 mock_power_action.assert_has_calls([
205 mock.call(task, states.POWER_OFF),205 mock.call(task, states.POWER_OFF),
206 mock.call(task, states.REBOOT),206 mock.call(task, states.REBOOT, None),
207 ])207 ])
208 else:208 else:
209 mock_power_action.assert_called_once_with(task, states.REBOOT)209 mock_power_action.assert_called_once_with(
210 task, states.REBOOT, None)
210 if step == 'factory_reset':211 if step == 'factory_reset':
211 bios.reset_bios.assert_called_once()212 bios.reset_bios.assert_called_once()
212 if step == 'apply_configuration':213 if step == 'apply_configuration':
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_firmware.py b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py
213new file mode 100644214new file mode 100644
index 0000000..c3e984c
--- /dev/null
+++ b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py
@@ -0,0 +1,40 @@
1#
2# Licensed under the Apache License, Version 2.0 (the "License"); you may
3# not use this file except in compliance with the License. You may obtain
4# a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11# License for the specific language governing permissions and limitations
12# under the License.
13
14from unittest import mock
15
16from oslo_utils import importutils
17
18from ironic.tests.unit.db import base as db_base
19from ironic.tests.unit.db import utils as db_utils
20from ironic.tests.unit.objects import utils as obj_utils
21
22sushy = importutils.try_import('sushy')
23
24INFO_DICT = db_utils.get_test_redfish_info()
25
26
27@mock.patch('oslo_utils.eventletutils.EventletEvent.wait',
28 lambda *args, **kwargs: None)
29class RedfishFirmwareTestCase(db_base.DbTestCase):
30
31 def setUp(self):
32 super(RedfishFirmwareTestCase, self).setUp()
33 self.config(enabled_bios_interfaces=['redfish'],
34 enabled_hardware_types=['redfish'],
35 enabled_power_interfaces=['redfish'],
36 enabled_boot_interfaces=['redfish-virtual-media'],
37 enabled_management_interfaces=['redfish'],
38 enabled_firmware_interfaces=['redfish'])
39 self.node = obj_utils.create_test_node(
40 self.context, driver='redfish', driver_info=INFO_DICT)
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_management.py b/ironic/tests/unit/drivers/modules/redfish/test_management.py
index 1d752d9..3a9dca2 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_management.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_management.py
@@ -28,10 +28,12 @@ from ironic.common import states
28from ironic.conductor import task_manager28from ironic.conductor import task_manager
29from ironic.conductor import utils as manager_utils29from ironic.conductor import utils as manager_utils
30from ironic.conf import CONF30from ironic.conf import CONF
31from ironic.drivers.modules import boot_mode_utils
31from ironic.drivers.modules import deploy_utils32from ironic.drivers.modules import deploy_utils
32from ironic.drivers.modules.redfish import boot as redfish_boot33from ironic.drivers.modules.redfish import boot as redfish_boot
33from ironic.drivers.modules.redfish import firmware_utils34from ironic.drivers.modules.redfish import firmware_utils
34from ironic.drivers.modules.redfish import management as redfish_mgmt35from ironic.drivers.modules.redfish import management as redfish_mgmt
36from ironic.drivers.modules.redfish import power as redfish_power
35from ironic.drivers.modules.redfish import utils as redfish_utils37from ironic.drivers.modules.redfish import utils as redfish_utils
36from ironic.tests.unit.db import base as db_base38from ironic.tests.unit.db import base as db_base
37from ironic.tests.unit.db import utils as db_utils39from ironic.tests.unit.db import utils as db_utils
@@ -268,8 +270,10 @@ class RedfishManagementTestCase(db_base.DbTestCase):
268 boot_devices.PXE,270 boot_devices.PXE,
269 task.node.driver_internal_info['redfish_boot_device'])271 task.node.driver_internal_info['redfish_boot_device'])
270272
273 @mock.patch.object(boot_mode_utils, 'sync_boot_mode', autospec=True)
271 @mock.patch.object(redfish_utils, 'get_system', autospec=True)274 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
272 def test_set_boot_device_persistency_vendor(self, mock_get_system):275 def test_set_boot_device_persistency_vendor(self, mock_get_system,
276 mock_sync_boot_mode):
273 fake_system = mock_get_system.return_value277 fake_system = mock_get_system.return_value
274 fake_system.boot.get.return_value = \278 fake_system.boot.get.return_value = \
275 sushy.BOOT_SOURCE_ENABLED_CONTINUOUS279 sushy.BOOT_SOURCE_ENABLED_CONTINUOUS
@@ -288,18 +292,16 @@ class RedfishManagementTestCase(db_base.DbTestCase):
288 shared=False) as task:292 shared=False) as task:
289 task.driver.management.set_boot_device(293 task.driver.management.set_boot_device(
290 task, boot_devices.PXE, persistent=True)294 task, boot_devices.PXE, persistent=True)
295 fake_system.set_system_boot_options.assert_called_once_with(
296 sushy.BOOT_SOURCE_TARGET_PXE, enabled=expected)
291 if vendor == 'SuperMicro':297 if vendor == 'SuperMicro':
292 fake_system.set_system_boot_options.assert_has_calls(298 mock_sync_boot_mode.assert_called_once_with(task)
293 [mock.call(sushy.BOOT_SOURCE_TARGET_PXE,
294 enabled=expected),
295 mock.call(mode=sushy.BOOT_SOURCE_MODE_UEFI)])
296 else:299 else:
297 fake_system.set_system_boot_options.assert_has_calls(300 mock_sync_boot_mode.assert_not_called()
298 [mock.call(sushy.BOOT_SOURCE_TARGET_PXE,
299 enabled=expected)])
300301
301 # Reset mocks302 # Reset mocks
302 fake_system.set_system_boot_options.reset_mock()303 fake_system.set_system_boot_options.reset_mock()
304 mock_sync_boot_mode.reset_mock()
303 mock_get_system.reset_mock()305 mock_get_system.reset_mock()
304306
305 def test_restore_boot_device(self):307 def test_restore_boot_device(self):
@@ -391,34 +393,46 @@ class RedfishManagementTestCase(db_base.DbTestCase):
391 self.assertEqual(list(redfish_mgmt.BOOT_MODE_MAP_REV),393 self.assertEqual(list(redfish_mgmt.BOOT_MODE_MAP_REV),
392 supported_boot_modes)394 supported_boot_modes)
393395
396 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_boot_mode',
397 autospec=True)
394 @mock.patch.object(redfish_utils, 'get_system', autospec=True)398 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
395 def test_set_boot_mode(self, mock_get_system):399 def test_set_boot_mode(self, mock_get_system, mock_wait):
396 boot_attribute = {400 boot_attribute = {
397 'target': sushy.BOOT_SOURCE_TARGET_PXE,401 'target': sushy.BOOT_SOURCE_TARGET_PXE,
398 'enabled': sushy.BOOT_SOURCE_ENABLED_CONTINUOUS,402 'enabled': sushy.BOOT_SOURCE_ENABLED_CONTINUOUS,
399 'mode': sushy.BOOT_SOURCE_MODE_BIOS,403 'mode': None,
400 }404 }
401 fake_system = mock.Mock(boot=boot_attribute)405 fake_system = mock.Mock(boot=boot_attribute)
402 fake_system = mock.Mock()
403 mock_get_system.return_value = fake_system406 mock_get_system.return_value = fake_system
404 with task_manager.acquire(self.context, self.node.uuid,407 with task_manager.acquire(self.context, self.node.uuid,
405 shared=False) as task:408 shared=False) as task:
406 expected_values = [409 expected_values = [
407 (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS),410 (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS,
408 (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI)411 sushy.BOOT_SOURCE_MODE_UEFI),
412 (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI,
413 sushy.BOOT_SOURCE_MODE_BIOS),
414 (boot_modes.LEGACY_BIOS, sushy.BOOT_SOURCE_MODE_BIOS, None),
415 (boot_modes.UEFI, sushy.BOOT_SOURCE_MODE_UEFI, None),
409 ]416 ]
410417
411 for mode, expected in expected_values:418 for mode, expected, current in expected_values:
419 boot_attribute['mode'] = current
412 task.driver.management.set_boot_mode(task, mode=mode)420 task.driver.management.set_boot_mode(task, mode=mode)
413421
414 # Asserts422 # Asserts
415 fake_system.set_system_boot_options.assert_called_once_with(423 fake_system.set_system_boot_options.assert_called_once_with(
416 mode=expected)424 mode=expected)
417 mock_get_system.assert_called_once_with(task.node)425 mock_get_system.assert_called_once_with(task.node)
426 if current is not None:
427 mock_wait.assert_called_once_with(task.driver.management,
428 task, fake_system, mode)
429 else:
430 mock_wait.assert_not_called()
418431
419 # Reset mocks432 # Reset mocks
420 fake_system.set_system_boot_options.reset_mock()433 fake_system.set_system_boot_options.reset_mock()
421 mock_get_system.reset_mock()434 mock_get_system.reset_mock()
435 mock_wait.reset_mock()
422436
423 @mock.patch.object(sushy, 'Sushy', autospec=True)437 @mock.patch.object(sushy, 'Sushy', autospec=True)
424 @mock.patch.object(redfish_utils, 'get_system', autospec=True)438 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
@@ -462,6 +476,44 @@ class RedfishManagementTestCase(db_base.DbTestCase):
462 mode=sushy.BOOT_SOURCE_MODE_UEFI)476 mode=sushy.BOOT_SOURCE_MODE_UEFI)
463 mock_get_system.assert_called_once_with(task.node)477 mock_get_system.assert_called_once_with(task.node)
464478
479 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
480 def test_wait_for_boot_mode_immediate(self, mock_power):
481 fake_system = mock.Mock(spec=['boot', 'refresh'],
482 boot={'mode': sushy.BOOT_SOURCE_MODE_UEFI})
483 with task_manager.acquire(self.context, self.node.uuid,
484 shared=False) as task:
485 task.driver.management._wait_for_boot_mode(
486 task, fake_system, boot_modes.UEFI)
487 fake_system.refresh.assert_called_once_with(force=True)
488 mock_power.assert_not_called()
489
490 @mock.patch('time.sleep', lambda _: None)
491 @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
492 autospec=True)
493 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
494 def test_wait_for_boot_mode(self, mock_power, mock_get_power):
495 attempts = 3
496
497 def side_effect(force):
498 nonlocal attempts
499 attempts -= 1
500 if attempts <= 0:
501 fake_system.boot['mode'] = sushy.BOOT_SOURCE_MODE_UEFI
502
503 fake_system = mock.Mock(spec=['boot', 'refresh'],
504 boot={'mode': sushy.BOOT_SOURCE_MODE_BIOS})
505 fake_system.refresh.side_effect = side_effect
506 with task_manager.acquire(self.context, self.node.uuid,
507 shared=False) as task:
508 task.driver.management._wait_for_boot_mode(
509 task, fake_system, boot_modes.UEFI)
510 fake_system.refresh.assert_called_with(force=True)
511 self.assertEqual(3, fake_system.refresh.call_count)
512 mock_power.assert_has_calls([
513 mock.call(task, states.REBOOT),
514 mock.call(task, mock_get_power.return_value),
515 ])
516
465 @mock.patch.object(redfish_utils, 'get_system', autospec=True)517 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
466 def test_get_boot_mode(self, mock_get_system):518 def test_get_boot_mode(self, mock_get_system):
467 boot_attribute = {519 boot_attribute = {
@@ -865,7 +917,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
865 task.node, reboot=True, skip_current_step=True, polling=True)917 task.node, reboot=True, skip_current_step=True, polling=True)
866 mock_get_async_step_return_state.assert_called_once_with(918 mock_get_async_step_return_state.assert_called_once_with(
867 task.node)919 task.node)
868 mock_node_power_action.assert_called_once_with(task, states.REBOOT)920 mock_node_power_action.assert_called_once_with(
921 task, states.REBOOT, None)
869922
870 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',923 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
871 autospec=True)924 autospec=True)
@@ -919,7 +972,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
919 task.node, reboot=True, skip_current_step=True, polling=True)972 task.node, reboot=True, skip_current_step=True, polling=True)
920 mock_get_async_step_return_state.assert_called_once_with(973 mock_get_async_step_return_state.assert_called_once_with(
921 task.node)974 task.node)
922 mock_node_power_action.assert_called_once_with(task, states.REBOOT)975 mock_node_power_action.assert_called_once_with(
976 task, states.REBOOT, None)
923977
924 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',978 @mock.patch.object(redfish_mgmt.RedfishManagement, '_stage_firmware_file',
925 autospec=True)979 autospec=True)
@@ -979,7 +1033,8 @@ class RedfishManagementTestCase(db_base.DbTestCase):
979 task.node, reboot=True, skip_current_step=True, polling=True)1033 task.node, reboot=True, skip_current_step=True, polling=True)
980 mock_get_async_step_return_state.assert_called_once_with(1034 mock_get_async_step_return_state.assert_called_once_with(
981 task.node)1035 task.node)
982 mock_node_power_action.assert_called_once_with(task, states.REBOOT)1036 mock_node_power_action.assert_called_once_with(
1037 task, states.REBOOT, None)
9831038
984 def test_update_firmware_invalid_args(self):1039 def test_update_firmware_invalid_args(self):
985 with task_manager.acquire(self.context, self.node.uuid,1040 with task_manager.acquire(self.context, self.node.uuid,
@@ -1462,61 +1517,84 @@ class RedfishManagementTestCase(db_base.DbTestCase):
1462 task.driver.management.get_secure_boot_state,1517 task.driver.management.get_secure_boot_state,
1463 task)1518 task)
14641519
1520 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
1521 autospec=True)
1465 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1522 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1466 def test_set_secure_boot_state(self, mock_get_system):1523 def test_set_secure_boot_state(self, mock_get_system, mock_wait):
1467 fake_system = mock_get_system.return_value1524 fake_system = mock_get_system.return_value
1468 fake_system.secure_boot.enabled = False1525 fake_system.secure_boot.enabled = False
1469 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}1526 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}
1470 with task_manager.acquire(self.context, self.node.uuid,1527 with task_manager.acquire(self.context, self.node.uuid,
1471 shared=True) as task:1528 shared=False) as task:
1472 task.driver.management.set_secure_boot_state(task, True)1529 task.driver.management.set_secure_boot_state(task, True)
1473 fake_system.secure_boot.set_enabled.assert_called_once_with(True)1530 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
1531 mock_wait.assert_called_once_with(task.driver.management,
1532 task, fake_system.secure_boot,
1533 True)
14741534
1535 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
1536 autospec=True)
1475 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1537 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1476 def test_set_secure_boot_state_boot_mode_unknown(self, mock_get_system):1538 def test_set_secure_boot_state_boot_mode_unknown(self, mock_get_system,
1539 mock_wait):
1477 fake_system = mock_get_system.return_value1540 fake_system = mock_get_system.return_value
1478 fake_system.secure_boot.enabled = False1541 fake_system.secure_boot.enabled = False
1479 fake_system.boot = {}1542 fake_system.boot = {}
1480 with task_manager.acquire(self.context, self.node.uuid,1543 with task_manager.acquire(self.context, self.node.uuid,
1481 shared=True) as task:1544 shared=False) as task:
1482 task.driver.management.set_secure_boot_state(task, True)1545 task.driver.management.set_secure_boot_state(task, True)
1483 fake_system.secure_boot.set_enabled.assert_called_once_with(True)1546 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
1547 mock_wait.assert_called_once_with(task.driver.management,
1548 task, fake_system.secure_boot,
1549 True)
14841550
1551 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
1552 autospec=True)
1485 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1553 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1486 def test_set_secure_boot_state_boot_mode_no_change(self, mock_get_system):1554 def test_set_secure_boot_state_boot_mode_no_change(self, mock_get_system,
1555 mock_wait):
1487 fake_system = mock_get_system.return_value1556 fake_system = mock_get_system.return_value
1488 fake_system.secure_boot.enabled = False1557 fake_system.secure_boot.enabled = False
1489 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}1558 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}
1490 with task_manager.acquire(self.context, self.node.uuid,1559 with task_manager.acquire(self.context, self.node.uuid,
1491 shared=True) as task:1560 shared=False) as task:
1492 task.driver.management.set_secure_boot_state(task, False)1561 task.driver.management.set_secure_boot_state(task, False)
1493 self.assertFalse(fake_system.secure_boot.set_enabled.called)1562 fake_system.secure_boot.set_enabled.assert_not_called()
1563 mock_wait.assert_not_called()
14941564
1565 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
1566 autospec=True)
1495 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1567 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1496 def test_set_secure_boot_state_boot_mode_incorrect(self, mock_get_system):1568 def test_set_secure_boot_state_boot_mode_incorrect(self, mock_get_system,
1569 mock_wait):
1497 fake_system = mock_get_system.return_value1570 fake_system = mock_get_system.return_value
1498 fake_system.secure_boot.enabled = False1571 fake_system.secure_boot.enabled = False
1499 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}1572 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_BIOS}
1500 with task_manager.acquire(self.context, self.node.uuid,1573 with task_manager.acquire(self.context, self.node.uuid,
1501 shared=True) as task:1574 shared=False) as task:
1502 self.assertRaisesRegex(1575 self.assertRaisesRegex(
1503 exception.RedfishError, 'requires UEFI',1576 exception.RedfishError, 'requires UEFI',
1504 task.driver.management.set_secure_boot_state, task, True)1577 task.driver.management.set_secure_boot_state, task, True)
1505 self.assertFalse(fake_system.secure_boot.set_enabled.called)1578 fake_system.secure_boot.set_enabled.assert_not_called()
1579 mock_wait.assert_not_called()
15061580
1581 @mock.patch.object(redfish_mgmt.RedfishManagement, '_wait_for_secure_boot',
1582 autospec=True)
1507 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1583 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1508 def test_set_secure_boot_state_boot_mode_fails(self, mock_get_system):1584 def test_set_secure_boot_state_boot_mode_fails(self, mock_get_system,
1585 mock_wait):
1509 fake_system = mock_get_system.return_value1586 fake_system = mock_get_system.return_value
1510 fake_system.secure_boot.enabled = False1587 fake_system.secure_boot.enabled = False
1511 fake_system.secure_boot.set_enabled.side_effect = \1588 fake_system.secure_boot.set_enabled.side_effect = \
1512 sushy.exceptions.SushyError1589 sushy.exceptions.SushyError
1513 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}1590 fake_system.boot = {'mode': sushy.BOOT_SOURCE_MODE_UEFI}
1514 with task_manager.acquire(self.context, self.node.uuid,1591 with task_manager.acquire(self.context, self.node.uuid,
1515 shared=True) as task:1592 shared=False) as task:
1516 self.assertRaisesRegex(1593 self.assertRaisesRegex(
1517 exception.RedfishError, 'Failed to set secure boot',1594 exception.RedfishError, 'Failed to set secure boot',
1518 task.driver.management.set_secure_boot_state, task, True)1595 task.driver.management.set_secure_boot_state, task, True)
1519 fake_system.secure_boot.set_enabled.assert_called_once_with(True)1596 fake_system.secure_boot.set_enabled.assert_called_once_with(True)
1597 mock_wait.assert_not_called()
15201598
1521 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1599 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1522 def test_set_secure_boot_state_not_implemented(self, mock_get_system):1600 def test_set_secure_boot_state_not_implemented(self, mock_get_system):
@@ -1528,11 +1606,76 @@ class RedfishManagementTestCase(db_base.DbTestCase):
15281606
1529 mock_get_system.return_value = NoSecureBoot()1607 mock_get_system.return_value = NoSecureBoot()
1530 with task_manager.acquire(self.context, self.node.uuid,1608 with task_manager.acquire(self.context, self.node.uuid,
1531 shared=True) as task:1609 shared=False) as task:
1532 self.assertRaises(exception.UnsupportedDriverExtension,1610 self.assertRaises(exception.UnsupportedDriverExtension,
1533 task.driver.management.set_secure_boot_state,1611 task.driver.management.set_secure_boot_state,
1534 task, True)1612 task, True)
15351613
1614 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
1615 def test_wait_for_secure_boot_immediate(self, mock_power):
1616 fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=True)
1617 with task_manager.acquire(self.context, self.node.uuid,
1618 shared=False) as task:
1619 task.driver.management._wait_for_secure_boot(task, fake_sb, True)
1620 fake_sb.refresh.assert_called_once_with(force=True)
1621 mock_power.assert_not_called()
1622
1623 @mock.patch('time.sleep', lambda _: None)
1624 @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
1625 autospec=True)
1626 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
1627 def test_wait_for_secure_boot(self, mock_power, mock_get_power):
1628 attempts = 3
1629
1630 def side_effect(force):
1631 nonlocal attempts
1632 attempts -= 1
1633 if attempts <= 0:
1634 fake_sb.enabled = True
1635
1636 fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
1637 fake_sb.refresh.side_effect = side_effect
1638 with task_manager.acquire(self.context, self.node.uuid,
1639 shared=False) as task:
1640 task.driver.management._wait_for_secure_boot(task, fake_sb, True)
1641 fake_sb.refresh.assert_called_with(force=True)
1642 self.assertEqual(3, fake_sb.refresh.call_count)
1643 mock_power.assert_has_calls([
1644 mock.call(task, states.REBOOT),
1645 mock.call(task, mock_get_power.return_value),
1646 ])
1647
1648 @mock.patch.object(redfish_mgmt, 'BOOT_MODE_CONFIG_INTERVAL', 0.1)
1649 @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
1650 autospec=True)
1651 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
1652 def test_wait_for_secure_boot_timeout(self, mock_power, mock_get_power):
1653 CONF.set_override('boot_mode_config_timeout', 1, group='redfish')
1654 fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
1655 with task_manager.acquire(self.context, self.node.uuid,
1656 shared=False) as task:
1657 self.assertRaisesRegex(
1658 exception.RedfishError, 'Timeout reached',
1659 task.driver.management._wait_for_secure_boot,
1660 task, fake_sb, True)
1661 fake_sb.refresh.assert_called_with(force=True)
1662 mock_power.assert_called_once_with(task, states.REBOOT)
1663
1664 @mock.patch.object(redfish_power.RedfishPower, 'get_power_state',
1665 autospec=True)
1666 @mock.patch.object(manager_utils, 'node_power_action', autospec=True)
1667 def test_wait_for_secure_boot_no_wait(self, mock_power, mock_get_power):
1668 CONF.set_override('boot_mode_config_timeout', 0, group='redfish')
1669 fake_sb = mock.Mock(spec=['enabled', 'refresh'], enabled=False)
1670 with task_manager.acquire(self.context, self.node.uuid,
1671 shared=False) as task:
1672 task.driver.management._wait_for_secure_boot(task, fake_sb, True)
1673 fake_sb.refresh.assert_called_once_with(force=True)
1674 mock_power.assert_has_calls([
1675 mock.call(task, states.REBOOT),
1676 mock.call(task, mock_get_power.return_value),
1677 ])
1678
1536 @mock.patch.object(redfish_utils, 'get_system', autospec=True)1679 @mock.patch.object(redfish_utils, 'get_system', autospec=True)
1537 def test_reset_secure_boot_to_default(self, mock_get_system):1680 def test_reset_secure_boot_to_default(self, mock_get_system):
1538 with task_manager.acquire(self.context, self.node.uuid,1681 with task_manager.acquire(self.context, self.node.uuid,
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_raid.py b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
index 843be73..e651d8b 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_raid.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_raid.py
@@ -406,7 +406,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
406 task.node, reboot=True, skip_current_step=True, polling=True)406 task.node, reboot=True, skip_current_step=True, polling=True)
407 mock_get_async_step_return_state.assert_called_once_with(407 mock_get_async_step_return_state.assert_called_once_with(
408 task.node)408 task.node)
409 mock_node_power_action.assert_called_once_with(task, states.REBOOT)409 mock_node_power_action.assert_called_once_with(
410 task, states.REBOOT, None)
410 mock_build_agent_options.assert_called_once_with(task.node)411 mock_build_agent_options.assert_called_once_with(task.node)
411 self.assertEqual(mock_prepare_ramdisk.call_count, 1)412 self.assertEqual(mock_prepare_ramdisk.call_count, 1)
412 # Async operation, raid_config shouldn't be updated yet413 # Async operation, raid_config shouldn't be updated yet
@@ -1123,7 +1124,8 @@ class RedfishRAIDTestCase(db_base.DbTestCase):
1123 task.node, reboot=True, skip_current_step=True, polling=True)1124 task.node, reboot=True, skip_current_step=True, polling=True)
1124 mock_get_async_step_return_state.assert_called_once_with(1125 mock_get_async_step_return_state.assert_called_once_with(
1125 task.node)1126 task.node)
1126 mock_node_power_action.assert_called_once_with(task, states.REBOOT)1127 mock_node_power_action.assert_called_once_with(
1128 task, states.REBOOT, None)
1127 mock_build_agent_options.assert_called_once_with(task.node)1129 mock_build_agent_options.assert_called_once_with(task.node)
1128 self.assertEqual(mock_prepare_ramdisk.call_count, 1)1130 self.assertEqual(mock_prepare_ramdisk.call_count, 1)
1129 self.assertEqual(1131 self.assertEqual(
diff --git a/ironic/tests/unit/drivers/modules/redfish/test_utils.py b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
index 01b7089..5e91c15 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_utils.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_utils.py
@@ -168,6 +168,14 @@ class RedfishUtilsTestCase(db_base.DbTestCase):
168 response = redfish_utils.parse_driver_info(self.node)168 response = redfish_utils.parse_driver_info(self.node)
169 self.assertEqual(self.parsed_driver_info, response)169 self.assertEqual(self.parsed_driver_info, response)
170170
171 def test_parse_driver_info_default_scheme_ipv6_brackets_added(self):
172 test_redfish_address = '2001:DB8::1'
173 self.node.driver_info['redfish_address'] = test_redfish_address
174 response = redfish_utils.parse_driver_info(self.node)
175 self.parsed_driver_info['address'] = ("https://[%s]"
176 % test_redfish_address)
177 self.assertEqual(self.parsed_driver_info, response)
178
171 def test_get_task_monitor(self):179 def test_get_task_monitor(self):
172 redfish_utils._get_connection = mock.Mock()180 redfish_utils._get_connection = mock.Mock()
173 fake_monitor = mock.Mock()181 fake_monitor = mock.Mock()
diff --git a/ironic/tests/unit/drivers/modules/test_agent_base.py b/ironic/tests/unit/drivers/modules/test_agent_base.py
index c589d52..6d285cb 100644
--- a/ironic/tests/unit/drivers/modules/test_agent_base.py
+++ b/ironic/tests/unit/drivers/modules/test_agent_base.py
@@ -1593,7 +1593,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1593 agent_base._post_step_reboot(task, 'clean')1593 agent_base._post_step_reboot(task, 'clean')
1594 self.assertTrue(mock_build_opt.called)1594 self.assertTrue(mock_build_opt.called)
1595 self.assertTrue(mock_prepare.called)1595 self.assertTrue(mock_prepare.called)
1596 mock_reboot.assert_called_once_with(task, states.REBOOT)1596 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1597 self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])1597 self.assertTrue(task.node.driver_internal_info['cleaning_reboot'])
1598 self.assertNotIn('agent_secret_token',1598 self.assertNotIn('agent_secret_token',
1599 task.node.driver_internal_info)1599 task.node.driver_internal_info)
@@ -1612,7 +1612,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1612 agent_base._post_step_reboot(task, 'deploy')1612 agent_base._post_step_reboot(task, 'deploy')
1613 self.assertTrue(mock_build_opt.called)1613 self.assertTrue(mock_build_opt.called)
1614 self.assertTrue(mock_prepare.called)1614 self.assertTrue(mock_prepare.called)
1615 mock_reboot.assert_called_once_with(task, states.REBOOT)1615 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1616 self.assertTrue(1616 self.assertTrue(
1617 task.node.driver_internal_info['deployment_reboot'])1617 task.node.driver_internal_info['deployment_reboot'])
1618 self.assertNotIn('agent_secret_token',1618 self.assertNotIn('agent_secret_token',
@@ -1633,7 +1633,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1633 agent_base._post_step_reboot(task, 'clean')1633 agent_base._post_step_reboot(task, 'clean')
1634 self.assertTrue(mock_build_opt.called)1634 self.assertTrue(mock_build_opt.called)
1635 self.assertTrue(mock_prepare.called)1635 self.assertTrue(mock_prepare.called)
1636 mock_reboot.assert_called_once_with(task, states.REBOOT)1636 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1637 self.assertIn('agent_secret_token',1637 self.assertIn('agent_secret_token',
1638 task.node.driver_internal_info)1638 task.node.driver_internal_info)
16391639
@@ -1649,7 +1649,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1649 with task_manager.acquire(self.context, self.node['uuid'],1649 with task_manager.acquire(self.context, self.node['uuid'],
1650 shared=False) as task:1650 shared=False) as task:
1651 agent_base._post_step_reboot(task, 'clean')1651 agent_base._post_step_reboot(task, 'clean')
1652 mock_reboot.assert_called_once_with(task, states.REBOOT)1652 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1653 mock_handler.assert_called_once_with(task, mock.ANY,1653 mock_handler.assert_called_once_with(task, mock.ANY,
1654 traceback=True)1654 traceback=True)
1655 self.assertNotIn('cleaning_reboot',1655 self.assertNotIn('cleaning_reboot',
@@ -1667,7 +1667,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1667 with task_manager.acquire(self.context, self.node['uuid'],1667 with task_manager.acquire(self.context, self.node['uuid'],
1668 shared=False) as task:1668 shared=False) as task:
1669 agent_base._post_step_reboot(task, 'deploy')1669 agent_base._post_step_reboot(task, 'deploy')
1670 mock_reboot.assert_called_once_with(task, states.REBOOT)1670 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1671 mock_handler.assert_called_once_with(task, mock.ANY,1671 mock_handler.assert_called_once_with(task, mock.ANY,
1672 traceback=True)1672 traceback=True)
1673 self.assertNotIn('deployment_reboot',1673 self.assertNotIn('deployment_reboot',
@@ -1686,7 +1686,7 @@ class AgentDeployMixinTest(AgentDeployMixinBaseTest):
1686 with task_manager.acquire(self.context, self.node['uuid'],1686 with task_manager.acquire(self.context, self.node['uuid'],
1687 shared=False) as task:1687 shared=False) as task:
1688 agent_base._post_step_reboot(task, 'service')1688 agent_base._post_step_reboot(task, 'service')
1689 mock_reboot.assert_called_once_with(task, states.REBOOT)1689 mock_reboot.assert_called_once_with(task, states.REBOOT, None)
1690 mock_handler.assert_called_once_with(task, mock.ANY,1690 mock_handler.assert_called_once_with(task, mock.ANY,
1691 traceback=True)1691 traceback=True)
1692 self.assertNotIn('servicing_reboot',1692 self.assertNotIn('servicing_reboot',
@@ -1829,7 +1829,7 @@ class ContinueCleaningTest(AgentDeployMixinBaseTest):
1829 with task_manager.acquire(self.context, self.node['uuid'],1829 with task_manager.acquire(self.context, self.node['uuid'],
1830 shared=False) as task:1830 shared=False) as task:
1831 self.deploy.continue_cleaning(task)1831 self.deploy.continue_cleaning(task)
1832 reboot_mock.assert_called_once_with(task, states.REBOOT)1832 reboot_mock.assert_called_once_with(task, states.REBOOT, None)
18331833
1834 @mock.patch.object(cleaning, 'continue_node_clean', autospec=True)1834 @mock.patch.object(cleaning, 'continue_node_clean', autospec=True)
1835 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',1835 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
@@ -2147,7 +2147,7 @@ class ContinueServiceTest(AgentDeployMixinBaseTest):
2147 with task_manager.acquire(self.context, self.node['uuid'],2147 with task_manager.acquire(self.context, self.node['uuid'],
2148 shared=False) as task:2148 shared=False) as task:
2149 self.deploy.continue_servicing(task)2149 self.deploy.continue_servicing(task)
2150 reboot_mock.assert_called_once_with(task, states.REBOOT)2150 reboot_mock.assert_called_once_with(task, states.REBOOT, None)
21512151
2152 @mock.patch.object(servicing, 'continue_node_service', autospec=True)2152 @mock.patch.object(servicing, 'continue_node_service', autospec=True)
2153 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',2153 @mock.patch.object(agent_client.AgentClient, 'get_commands_status',
diff --git a/ironic/tests/unit/drivers/modules/test_inspect_utils.py b/ironic/tests/unit/drivers/modules/test_inspect_utils.py
index f6f5ab0..a7b2ac2 100644
--- a/ironic/tests/unit/drivers/modules/test_inspect_utils.py
+++ b/ironic/tests/unit/drivers/modules/test_inspect_utils.py
@@ -442,6 +442,12 @@ class GetBMCAddressesTestCase(db_base.DbTestCase):
442 driver_info={'redfish_address': 'https://192.0.2.1/redfish'})442 driver_info={'redfish_address': 'https://192.0.2.1/redfish'})
443 self.assertEqual({'192.0.2.1'}, utils._get_bmc_addresses(node))443 self.assertEqual({'192.0.2.1'}, utils._get_bmc_addresses(node))
444444
445 def test_normal_ipv6_as_url(self):
446 node = obj_utils.create_test_node(
447 self.context,
448 driver_info={'redfish_address': 'https://[2001:db8::42]/redfish'})
449 self.assertEqual({'2001:db8::42'}, utils._get_bmc_addresses(node))
450
445 @mock.patch.object(socket, 'getaddrinfo', autospec=True)451 @mock.patch.object(socket, 'getaddrinfo', autospec=True)
446 def test_resolved_host(self, mock_getaddrinfo):452 def test_resolved_host(self, mock_getaddrinfo):
447 mock_getaddrinfo.return_value = [453 mock_getaddrinfo.return_value = [
@@ -474,6 +480,12 @@ class GetBMCAddressesTestCase(db_base.DbTestCase):
474 mock_getaddrinfo.assert_called_once_with(480 mock_getaddrinfo.assert_called_once_with(
475 'example.com', None, proto=socket.SOL_TCP)481 'example.com', None, proto=socket.SOL_TCP)
476482
483 def test_redfish_bmc_address_ipv6_brackets_no_scheme(self):
484 node = obj_utils.create_test_node(
485 self.context,
486 driver_info={'redfish_address': '[2001:db8::42]'})
487 self.assertEqual({'2001:db8::42'}, utils._get_bmc_addresses(node))
488
477489
478class LookupCacheTestCase(db_base.DbTestCase):490class LookupCacheTestCase(db_base.DbTestCase):
479491
diff --git a/ironic/tests/unit/drivers/test_redfish.py b/ironic/tests/unit/drivers/test_redfish.py
index b692c61..be34fbf 100644
--- a/ironic/tests/unit/drivers/test_redfish.py
+++ b/ironic/tests/unit/drivers/test_redfish.py
@@ -33,7 +33,8 @@ class RedfishHardwareTestCase(db_base.DbTestCase):
33 enabled_boot_interfaces=['redfish-virtual-media'],33 enabled_boot_interfaces=['redfish-virtual-media'],
34 enabled_management_interfaces=['redfish'],34 enabled_management_interfaces=['redfish'],
35 enabled_inspect_interfaces=['redfish'],35 enabled_inspect_interfaces=['redfish'],
36 enabled_bios_interfaces=['redfish'])36 enabled_bios_interfaces=['redfish'],
37 enabled_firmware_interfaces=['redfish'])
3738
38 def test_default_interfaces(self):39 def test_default_interfaces(self):
39 node = obj_utils.create_test_node(self.context, driver='redfish')40 node = obj_utils.create_test_node(self.context, driver='redfish')
diff --git a/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml b/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
40new file mode 10064441new file mode 100644
index 0000000..1c27277
--- /dev/null
+++ b/releasenotes/notes/23.0-prelude-bobcat-ad7c24f666c22ebf.yaml
@@ -0,0 +1,17 @@
1---
2prelude: |
3 Ironic is proud to announce the release of 23.0, the capstone release of a
4 six month OpenStack 2023.2 (Bobcat) cycle.
5
6 Our focus this cycle has been on improving the ability for operators to
7 secure and service their Ironic nodes. There are also, as always, a myriad
8 of quality of life fixes, including improvements to sqlite support,
9 and graceful shutdown of conductors.
10
11 We hope the latest release of Ironic serves you well!
12upgrade: |
13 Ironic 23.0 is part of the OpenStack 2023.2 (Bobcat) release. This a
14 non-SLURP release, meaning users of a 2023.1 (Antelope) cycle Ironic release
15 can upgrade directly to the release accompanying 2024.1 (Caracal) when
16 available. For more information, please visit
17 `Release Cadence Adjustment <https://governance.openstack.org/tc/resolutions/20220210-release-cadence-adjustment.html?_ga=2.126966605.1175089434.1694620440-1981816456.1685478379>`_.
diff --git a/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml b/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
index 5eec68b..521e581 100644
--- a/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
+++ b/releasenotes/notes/add-hold-states-7be5804d6f3a119a.yaml
@@ -11,7 +11,7 @@ features:
11 to start over.11 to start over.
12 - |12 - |
13 Adds the ability to send an ``unhold`` provision state verb utilizing13 Adds the ability to send an ``unhold`` provision state verb utilizing
14 API version *1.84*.14 API version *1.85*.
15other:15other:
16 - |16 - |
17 Fixes the generated state machine diagram and updates it to match the17 Fixes the generated state machine diagram and updates it to match the
diff --git a/releasenotes/notes/bug-2036455-edd0e97335579684.yaml b/releasenotes/notes/bug-2036455-edd0e97335579684.yaml
18new file mode 10064418new file mode 100644
index 0000000..72a000b
--- /dev/null
+++ b/releasenotes/notes/bug-2036455-edd0e97335579684.yaml
@@ -0,0 +1,6 @@
1---
2fixes:
3 - |
4 Fixes an issue where inspection would fail if an IPv6 address wrapped in
5 brackets is used for the redfish BMC address. See bug:
6 `2036455 <https://bugs.launchpad.net/ironic/+bug/2036455>`_.
diff --git a/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml b/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
0new file mode 1006447new file mode 100644
index 0000000..06964b1
--- /dev/null
+++ b/releasenotes/notes/firmware-interface-8ad6f91aa1f746a0.yaml
@@ -0,0 +1,31 @@
1---
2features:
3 - |
4 Adds Firmware Interface support to ironic, we would like to receive
5 feedback since this is a new feature we introduced and we as a developer
6 community have limited hardware access, reach out to us in case of any
7 unexpected behavior.
8
9 - Adds version 1.86 of the Bare Metal API, which includes:
10
11 * List all firmware components of a node via the
12 ``GET /v1/nodes/{node_ident}/firmware`` API.
13
14 * The ``firmware_interface`` field of the node resource. A firmware
15 interface can be set when creating or updating a node.
16
17 * The ``default_firmware_interface`` and ``enabled_firmware_interface``
18 fields of the driver resource.
19
20 - Adds new configuration options for the firmware interface feature:
21
22 * Firmware interfaces are enabled via
23 ``[DEFAULT]/enabled_firmware_interfaces``. A default firmware
24 interface to use when creating or updating nodes can be specified with
25 ``[DEFAULT]/default_firmware_interface``.
26
27 - Available interfaces: ``redfish``, ``no-firmware`` and ``fake``.
28
29 - Support to update firmware of BIOS and BMC via ``update`` step, can be
30 done via clean or deploy steps, the node should be using the
31 ``redfish`` driver and set the ``firmware_interface``.
diff --git a/releasenotes/notes/remove-400a563030224c4f.yaml b/releasenotes/notes/remove-400a563030224c4f.yaml
0new file mode 10064432new file mode 100644
index 0000000..1ce686a
--- /dev/null
+++ b/releasenotes/notes/remove-400a563030224c4f.yaml
@@ -0,0 +1,9 @@
1---
2other:
3 - |
4 While investigating `bug 2033430 <https://bugs.launchpad.net/ironic/+bug/2033430>`_
5 we discovered we were emitting DHCP option 210 *only* with OVN, and never
6 emitted it with dnsmasq because it was not being set previously. Our
7 internal notes also indicated this was for PXELinux support, but was
8 never actually needed. As it was excess, and redundant configuration
9 being provided to Neutron, it has been removed.
diff --git a/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml b/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
0new file mode 10064410new file mode 100644
index 0000000..44ae3c6
--- /dev/null
+++ b/releasenotes/notes/uefi-and-secureboot-waits-a783215327164e2c.yaml
@@ -0,0 +1,20 @@
1---
2fixes:
3 - |
4 While updating boot mode or secure boot state in the Redfish driver,
5 the node is now rebooted if the change is not detected on the System
6 resource refresh. Ironic then waits up to
7 ``[redfish]boot_mode_config_timeout`` seconds until the change is applied.
8upgrade:
9 - |
10 Changing the boot mode or the secure boot state via the direct API
11 (``/v1/nodes/{node_ident}/states/boot_mode`` and
12 ``/v1/nodes/{node_ident}/states/secure_boot`` accordingly) may now
13 result in a reboot. This happens when the change cannot be applied
14 immediately. Previously, the change would be applied whenever the next
15 reboot happens for any unrelated reason, causing inconsistent behavior.
16issues:
17 - |
18 When boot mode needs to be changed during provisioning, an additional
19 reboot may happen on certain hardware. This is to ensure consistent
20 behavior when any boot setting change results in a separate internal job.
diff --git a/releasenotes/source/2023.1.rst b/releasenotes/source/2023.1.rst
index d123847..dc91336 100644
--- a/releasenotes/source/2023.1.rst
+++ b/releasenotes/source/2023.1.rst
@@ -1,6 +1,6 @@
1===========================1=============================================
22023.1 Series Release Notes22023.1 Series (21.2.0 - 21.4.x) Release Notes
3===========================3=============================================
44
5.. release-notes::5.. release-notes::
6 :branch: stable/2023.16 :branch: stable/2023.1
diff --git a/setup.cfg b/setup.cfg
index c14a312..9f31ef9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -86,6 +86,7 @@ ironic.hardware.interfaces.deploy =
86ironic.hardware.interfaces.firmware = 86ironic.hardware.interfaces.firmware =
87 fake = ironic.drivers.modules.fake:FakeFirmware87 fake = ironic.drivers.modules.fake:FakeFirmware
88 no-firmware = ironic.drivers.modules.noop:NoFirmware88 no-firmware = ironic.drivers.modules.noop:NoFirmware
89 redfish = ironic.drivers.modules.redfish.firmware:RedfishFirmware
89ironic.hardware.interfaces.inspect = 90ironic.hardware.interfaces.inspect =
90 agent = ironic.drivers.modules.inspector:AgentInspect91 agent = ironic.drivers.modules.inspector:AgentInspect
91 fake = ironic.drivers.modules.fake:FakeInspect92 fake = ironic.drivers.modules.fake:FakeInspect
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 120a06e..ea1446a 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -60,7 +60,7 @@
60 voting: false60 voting: false
61 - ironic-inspector-tempest-rbac-scope-enforced:61 - ironic-inspector-tempest-rbac-scope-enforced:
62 voting: false62 voting: false
63 - bifrost-integration-tinyipa-ubuntu-focal:63 - bifrost-integration-tinyipa-ubuntu-jammy:
64 voting: false64 voting: false
65 - bifrost-integration-redfish-vmedia-uefi-centos-9:65 - bifrost-integration-redfish-vmedia-uefi-centos-9:
66 voting: false66 voting: false

Subscribers

People subscribed via source and target branches