summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--devstack/lib/ironic41
-rw-r--r--doc/source/admin/drivers/ilo.rst75
-rw-r--r--doc/source/admin/drivers/redfish.rst2
-rw-r--r--doc/source/admin/drivers/snmp.rst74
-rw-r--r--driver-requirements.txt4
-rw-r--r--ironic/common/pxe_utils.py77
-rw-r--r--ironic/conf/deploy.py4
-rw-r--r--ironic/conf/ilo.py5
-rw-r--r--ironic/drivers/ilo.py3
-rw-r--r--ironic/drivers/modules/ilo/common.py42
-rw-r--r--ironic/drivers/modules/ilo/management.py79
-rw-r--r--ironic/drivers/modules/ilo/vendor.py43
-rw-r--r--ironic/drivers/modules/snmp.py339
-rw-r--r--ironic/tests/unit/common/test_pxe_utils.py104
-rw-r--r--ironic/tests/unit/drivers/modules/ilo/test_common.py52
-rw-r--r--ironic/tests/unit/drivers/modules/ilo/test_management.py115
-rw-r--r--ironic/tests/unit/drivers/modules/ilo/test_vendor.py71
-rw-r--r--ironic/tests/unit/drivers/modules/test_snmp.py80
-rw-r--r--releasenotes/notes/additonal-snmp-drivers-ae1174e6bd6ee3a6.yaml5
-rw-r--r--releasenotes/notes/correct-source-path-handling-lookups-4ce2023a56372f10.yaml16
-rw-r--r--releasenotes/notes/create_csr_clean_step-a720932f61b42118.yaml7
-rw-r--r--releasenotes/notes/ilo-event-subscription-0dadf136411bd16a.yaml7
-rw-r--r--requirements.txt2
-rw-r--r--zuul.d/ironic-jobs.yaml42
-rw-r--r--zuul.d/project.yaml2
25 files changed, 1153 insertions, 138 deletions
diff --git a/devstack/lib/ironic b/devstack/lib/ironic
index ab96638c0..08cccce7a 100644
--- a/devstack/lib/ironic
+++ b/devstack/lib/ironic
@@ -1332,6 +1332,17 @@ function configure_ironic_networks {
configure_ironic_cleaning_network
echo_summary "Configuring Ironic rescue network"
configure_ironic_rescue_network
+ echo_summary "Configuring Neutron Private Subnet, if needed."
+ configure_ironic_private_subnet
+}
+
+function configure_ironic_private_subnet {
+ if [[ "${IRONIC_ANACONDA_IMAGE_REF:-}" != "" ]]; then
+ # NOTE(TheJulia): Anaconda needs DNS for FQDN resolution
+ # and devstack doesn't create this network with dns.
+ subnet_id=$(openstack --os-cloud $OS_CLOUD subnet show private-subnet -f value -c id)
+ openstack --os-cloud $OS_CLOUD subnet set --dns-nameserver 8.8.8.8 $subnet_id
+ fi
}
function configure_ironic_cleaning_network {
@@ -1405,7 +1416,8 @@ function configure_ironic_provision_network {
${net_segment_id:+--network-segment $net_segment_id} \
$IRONIC_PROVISION_PROVIDER_SUBNET_NAME \
--gateway $IRONIC_PROVISION_SUBNET_GATEWAY --network $net_id \
- --subnet-range $IRONIC_PROVISION_SUBNET_PREFIX -f value -c id)"
+ --subnet-range $IRONIC_PROVISION_SUBNET_PREFIX \
+ --dns-nameserver 8.8.8.8 -f value -c id)"
else
# NOTE(TheJulia): Consider changing this to stateful to support UEFI once we move
# CI to Ubuntu Jammy as it will support v6 and v4 UEFI firmware driven boot ops.
@@ -3057,6 +3069,16 @@ function upload_baremetal_ironic_deploy {
iniset $IRONIC_CONF_FILE conductor deploy_ramdisk $IRONIC_DEPLOY_RAMDISK_ID
iniset $IRONIC_CONF_FILE conductor rescue_kernel $IRONIC_DEPLOY_KERNEL_ID
iniset $IRONIC_CONF_FILE conductor rescue_ramdisk $IRONIC_DEPLOY_RAMDISK_ID
+
+ if [[ "${IRONIC_ANACONDA_INSECURE_HEARTBEAT:-}" != "" ]]; then
+ iniset $IRONIC_CONF_FILE anaconda insecure_heartbeat ${IRONIC_ANACONDA_INSECURE_HEARTBEAT:-}
+ fi
+ # NOTE(TheJulia): Compared to an image deploy, anaconda is relatively
+ # slow as it installs packages one at a time. As such, we need an option
+ # to extend.
+ if [[ "${IRONIC_DEPLOY_CALLBACK_WAIT_TIMEOUT:-}" != "" ]]; then
+ iniset $IRONIC_CONF_FILE conductor deploy_callback_timeout ${IRONIC_DEPLOY_CALLBACK_WAIT_TIMEOUT:-}
+ fi
}
function prepare_baremetal_basic_ops {
@@ -3221,6 +3243,23 @@ function ironic_configure_tempest {
if [[ "$IRONIC_RAMDISK_IMAGE" != "" ]]; then
iniset $TEMPEST_CONFIG baremetal ramdisk_iso_image_ref "$IRONIC_RAMDISK_IMAGE"
fi
+ if [[ "${IRONIC_ANACONDA_IMAGE_REF:-}" != "" ]]; then
+ # In a perfect world we would use *just* the opendev repo
+ # mirror, and let things be magical, but OpenDev Infra cannot
+ # mirror the /images path with the limited storage space.
+ iniset $TEMPEST_CONFIG baremetal anaconda_image_ref ${IRONIC_ANACONDA_IMAGE_REF:-}
+ fi
+ if [[ "${IRONIC_ANACONDA_KERNEL_REF:-}" != "" ]]; then
+ iniset $TEMPEST_CONFIG baremetal anaconda_kernel_ref ${IRONIC_ANACONDA_KERNEL_REF:-}
+ fi
+ if [[ "${IRONIC_ANACONDA_RAMDISK_REF:-}" != "" ]]; then
+ iniset $TEMPEST_CONFIG baremetal anaconda_initial_ramdisk_ref ${IRONIC_ANACONDA_RAMDISK_REF:-}
+ fi
+ if [[ "${IRONIC_ANACONDA_STAGE2_REF:-}" != "" ]]; then
+ iniset $TEMPEST_CONFIG baremetal anaconda_stage2_ramdisk_ref ${IRONIC_ANACONDA_STAGE2_REF:-}
+
+ fi
+
# NOTE(dtantsur): keep this option here until the defaults change in
# ironic-tempest-plugin to disable classic drivers testing.
iniset $TEMPEST_CONFIG baremetal enabled_drivers ""
diff --git a/doc/source/admin/drivers/ilo.rst b/doc/source/admin/drivers/ilo.rst
index f764a6d89..65ff4f6da 100644
--- a/doc/source/admin/drivers/ilo.rst
+++ b/doc/source/admin/drivers/ilo.rst
@@ -55,6 +55,8 @@ The hardware type ``ilo`` supports following HPE server features:
* `Updating security parameters as manual clean step`_
* `Update Minimum Password Length security parameter as manual clean step`_
* `Update Authentication Failure Logging security parameter as manual clean step`_
+* `Create Certificate Signing Request(CSR) as manual clean step`_
+* `Add HTTPS Certificate as manual clean step`_
* `Activating iLO Advanced license as manual clean step`_
* `Removing CA certificates from iLO as manual clean step`_
* `Firmware based UEFI iSCSI boot from volume support`_
@@ -65,6 +67,7 @@ The hardware type ``ilo`` supports following HPE server features:
* `BIOS configuration support`_
* `IPv6 support`_
* `Layer 3 or DHCP-less ramdisk booting`_
+* `Events subscription`_
Apart from above features hardware type ``ilo5`` also supports following
features:
@@ -200,6 +203,18 @@ The ``ilo`` hardware type supports following hardware interfaces:
enabled_hardware_types = ilo
enabled_rescue_interfaces = agent,no-rescue
+* vendor
+ Supports ``ilo``, ``ilo-redfish`` and ``no-vendor``. The default is
+ ``ilo``. They can be enabled by using the
+ ``[DEFAULT]enabled_vendor_interfaces`` option in ``ironic.conf`` as given
+ below:
+
+ .. code-block:: ini
+
+ [DEFAULT]
+ enabled_hardware_types = ilo
+ enabled_vendor_interfaces = ilo,ilo-redfish,no-vendor
+
The ``ilo5`` hardware type supports all the ``ilo`` interfaces described above,
except for ``boot`` and ``raid`` interfaces. The details of ``boot`` and
@@ -751,6 +766,12 @@ Supported **Manual** Cleaning Operations
``update_auth_failure_logging_threshold``:
Updates the Authentication Failure Logging security parameter. See
`Update Authentication Failure Logging security parameter as manual clean step`_ for user guidance on usage.
+ ``create_csr``:
+ Creates the certificate signing request. See `Create Certificate Signing Request(CSR) as manual clean step`_
+ for user guidance on usage.
+ ``add_https_certificate``:
+ Adds the signed HTTPS certificate to the iLO. See `Add HTTPS Certificate as manual clean step`_ for user
+ guidance on usage.
* iLO with firmware version 1.5 is minimally required to support all the
operations.
@@ -1648,6 +1669,54 @@ Both the arguments ``logging_threshold`` and ``ignore`` are optional. The accept
value be False. If user passes the value of logging_threshold as 0, the Authentication Failure Logging security
parameter will be disabled.
+Create Certificate Signing Request(CSR) as manual clean step
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+iLO driver can invoke ``create_csr`` request as a manual clean step. This step is only supported for iLO5 based hardware.
+
+An example of a manual clean step with ``create_csr`` as the only clean step could be::
+
+ "clean_steps": [{
+ "interface": "management",
+ "step": "create_csr",
+ "args": {
+ "csr_params": {
+ "City": "Bengaluru",
+ "CommonName": "1.1.1.1",
+ "Country": "India",
+ "OrgName": "HPE",
+ "State": "Karnataka"
+ }
+ }
+ }]
+
+The ``[ilo]cert_path`` option in ``ironic.conf`` is used as the directory path for
+creating the CSR, which defaults to ``/var/lib/ironic/ilo``. The CSR is created in the directory location
+given in ``[ilo]cert_path`` in ``node_uuid`` directory as <node_uuid>.csr.
+
+
+Add HTTPS Certificate as manual clean step
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+iLO driver can invoke ``add_https_certificate`` request as a manual clean step. This step is only supported for
+iLO5 based hardware.
+
+An example of a manual clean step with ``add_https_certificate`` as the only clean step could be::
+
+ "clean_steps": [{
+ "interface": "management",
+ "step": "add_https_certificate",
+ "args": {
+ "cert_file": "/test1/iLO.crt"
+ }
+ }]
+
+Argument ``cert_file`` is mandatory. The ``cert_file`` takes the path or url of the certificate file.
+The url schemes supported are: ``file``, ``http`` and ``https``.
+The CSR generated in step ``create_csr`` needs to be signed by a valid CA and the resultant HTTPS certificate should
+be provided in ``cert_file``. It copies the ``cert_file`` to ``[ilo]cert_path`` under ``node.uuid`` as <node_uuid>.crt
+before adding it to iLO.
+
RAID Support
^^^^^^^^^^^^
@@ -2136,6 +2205,12 @@ DHCP-less deploy is supported by ``ilo`` and ``ilo5`` hardware types.
However it would work only with ilo-virtual-media boot interface. See
:doc:`/admin/dhcp-less` for more information.
+Events subscription
+^^^^^^^^^^^^^^^^^^^
+Events subscription is supported by ``ilo`` and ``ilo5`` hardware types with
+``ilo`` vendor interface for Gen10 and Gen10 Plus servers. See
+:ref:`node-vendor-passthru-methods` for more information.
+
.. _`ssacli documentation`: https://support.hpe.com/hpsc/doc/public/display?docId=c03909334
.. _`proliant-tools`: https://docs.openstack.org/diskimage-builder/latest/elements/proliant-tools/README.html
.. _`HPE iLO4 User Guide`: https://h20566.www2.hpe.com/hpsc/doc/public/display?docId=c03334051
diff --git a/doc/source/admin/drivers/redfish.rst b/doc/source/admin/drivers/redfish.rst
index 899ef98a2..eb1f561f4 100644
--- a/doc/source/admin/drivers/redfish.rst
+++ b/doc/source/admin/drivers/redfish.rst
@@ -543,6 +543,8 @@ settings. The following fields will be returned in the BIOS API
"``unique``", "The setting is specific to this node"
"``reset_required``", "After changing this setting a node reboot is required"
+.. _node-vendor-passthru-methods:
+
Node Vendor Passthru Methods
============================
diff --git a/doc/source/admin/drivers/snmp.rst b/doc/source/admin/drivers/snmp.rst
index 1c402ab9b..eed4ed794 100644
--- a/doc/source/admin/drivers/snmp.rst
+++ b/doc/source/admin/drivers/snmp.rst
@@ -22,39 +22,47 @@ this table could possibly work using a similar driver.
Please report any device status.
-============== ========== ========== =====================
-Manufacturer Model Supported? Driver name
-============== ========== ========== =====================
-APC AP7920 Yes apc_masterswitch
-APC AP9606 Yes apc_masterswitch
-APC AP9225 Yes apc_masterswitchplus
-APC AP7155 Yes apc_rackpdu
-APC AP7900 Yes apc_rackpdu
-APC AP7901 Yes apc_rackpdu
-APC AP7902 Yes apc_rackpdu
-APC AP7911a Yes apc_rackpdu
-APC AP7921 Yes apc_rackpdu
-APC AP7922 Yes apc_rackpdu
-APC AP7930 Yes apc_rackpdu
-APC AP7931 Yes apc_rackpdu
-APC AP7932 Yes apc_rackpdu
-APC AP7940 Yes apc_rackpdu
-APC AP7941 Yes apc_rackpdu
-APC AP7951 Yes apc_rackpdu
-APC AP7960 Yes apc_rackpdu
-APC AP7990 Yes apc_rackpdu
-APC AP7998 Yes apc_rackpdu
-APC AP8941 Yes apc_rackpdu
-APC AP8953 Yes apc_rackpdu
-APC AP8959 Yes apc_rackpdu
-APC AP8961 Yes apc_rackpdu
-APC AP8965 Yes apc_rackpdu
-Aten all? Yes aten
-CyberPower all? Untested cyberpower
-EatonPower all? Untested eatonpower
-Teltronix all? Yes teltronix
-BayTech MRP27 Yes baytech_mrp27
-============== ========== ========== =====================
+============== ============== ========== =====================
+Manufacturer Model Supported? Driver name
+============== ============== ========== =====================
+APC AP7920 Yes apc_masterswitch
+APC AP9606 Yes apc_masterswitch
+APC AP9225 Yes apc_masterswitchplus
+APC AP7155 Yes apc_rackpdu
+APC AP7900 Yes apc_rackpdu
+APC AP7901 Yes apc_rackpdu
+APC AP7902 Yes apc_rackpdu
+APC AP7911a Yes apc_rackpdu
+APC AP7921 Yes apc_rackpdu
+APC AP7922 Yes apc_rackpdu
+APC AP7930 Yes apc_rackpdu
+APC AP7931 Yes apc_rackpdu
+APC AP7932 Yes apc_rackpdu
+APC AP7940 Yes apc_rackpdu
+APC AP7941 Yes apc_rackpdu
+APC AP7951 Yes apc_rackpdu
+APC AP7960 Yes apc_rackpdu
+APC AP7990 Yes apc_rackpdu
+APC AP7998 Yes apc_rackpdu
+APC AP8941 Yes apc_rackpdu
+APC AP8953 Yes apc_rackpdu
+APC AP8959 Yes apc_rackpdu
+APC AP8961 Yes apc_rackpdu
+APC AP8965 Yes apc_rackpdu
+Aten all? Yes aten
+CyberPower all? Untested cyberpower
+EatonPower all? Untested eatonpower
+Teltronix all? Yes teltronix
+BayTech MRP27 Yes baytech_mrp27
+Raritan PX3-5547V-V2 Yes raritan_pdu2
+Raritan PX3-5726V Yes raritan_pdu2
+Raritan PX3-5776U-N2 Yes raritan_pdu2
+Raritan PX3-5969U-V2 Yes raritan_pdu2
+Raritan PX3-5961I2U-V2 Yes raritan_pdu2
+Vertiv NU30212 Yes vertivgeist_pdu
+ServerTech CW-16VE-P32M Yes servertech_sentry3
+ServerTech C2WG24SN Yes servertech_sentry4
+============== ============== ========== =====================
Software Requirements
diff --git a/driver-requirements.txt b/driver-requirements.txt
index 5333dbd4f..3725c3ddf 100644
--- a/driver-requirements.txt
+++ b/driver-requirements.txt
@@ -4,7 +4,7 @@
# python projects they should package as optional dependencies for Ironic.
# These are available on pypi
-proliantutils>=2.13.0
+proliantutils>=2.14.0
pysnmp>=4.3.0,<5.0.0
python-scciclient>=0.12.2
python-dracclient>=5.1.0,<9.0.0
@@ -17,4 +17,4 @@ ansible>=2.7
python-ibmcclient>=0.2.2,<0.3.0
# Dell EMC iDRAC sushy OEM extension
-sushy-oem-idrac>=4.0.0,<5.0.0
+sushy-oem-idrac>=4.0.0,<6.0.0
diff --git a/ironic/common/pxe_utils.py b/ironic/common/pxe_utils.py
index ec0719b75..33a24ecb6 100644
--- a/ironic/common/pxe_utils.py
+++ b/ironic/common/pxe_utils.py
@@ -674,20 +674,33 @@ def get_instance_image_info(task, ipxe_enabled=False):
os.path.join(root_dir, node.uuid, 'boot_iso'))
return image_info
-
image_properties = None
d_info = deploy_utils.get_image_instance_info(node)
+ isap = node.driver_internal_info.get('is_source_a_path')
def _get_image_properties():
- nonlocal image_properties
- if not image_properties:
+ nonlocal image_properties, isap
+ if not image_properties and not isap:
i_service = service.get_image_service(
d_info['image_source'],
context=ctx)
image_properties = i_service.show(
d_info['image_source'])['properties']
+ # TODO(TheJulia): At some point, we should teach this code
+ # to understand that with a path, it *can* retrieve the
+ # manifest from the HTTP(S) endpoint, which can populate
+ # image_properties, and drive path to variable population
+ # like is done with basically Glance.
labels = ('kernel', 'ramdisk')
+ if not isap:
+ anaconda_labels = ('stage2', 'ks_template', 'ks_cfg')
+ else:
+ # When a path is used, a stage2 ramdisk can be determiend
+ # automatically by anaconda, so it is not an explicit
+ # requirement.
+ anaconda_labels = ('ks_template', 'ks_cfg')
+
if not (i_info.get('kernel') and i_info.get('ramdisk')):
# NOTE(rloo): If both are not specified in instance_info
# we won't use any of them. We'll use the values specified
@@ -700,20 +713,13 @@ def get_instance_image_info(task, ipxe_enabled=False):
i_info[label] = str(image_properties[label + '_id'])
node.instance_info = i_info
node.save()
+ # TODO(TheJulia): Add functionality to look/grab the hints file
+ # for anaconda and just run with the entire path.
- anaconda_labels = ()
- if deploy_utils.get_boot_option(node) == 'kickstart':
- isap = node.driver_internal_info.get('is_source_a_path')
# stage2: installer stage2 squashfs image
# ks_template: anaconda kickstart template
# ks_cfg - rendered ks_template
- if not isap:
- anaconda_labels = ('stage2', 'ks_template', 'ks_cfg')
- else:
- # When a path is used, a stage2 ramdisk can be determiend
- # automatically by anaconda, so it is not an explicit
- # requirement.
- anaconda_labels = ('ks_template', 'ks_cfg')
+
# NOTE(rloo): We save stage2 & ks_template values in case they
# are changed by the user after we start using them and to
# prevent re-computing them again.
@@ -733,26 +739,31 @@ def get_instance_image_info(task, ipxe_enabled=False):
else:
node.set_driver_internal_info(
'stage2', str(image_properties['stage2_id']))
- # NOTE(TheJulia): A kickstart template is entirely independent
- # of the stage2 ramdisk. In the end, it was the configuration which
- # told anaconda how to execute.
- if i_info.get('ks_template'):
- # If the value is set, we always overwrite it, in the event
- # a rebuild is occuring or something along those lines.
- node.set_driver_internal_info('ks_template',
- i_info['ks_template'])
+ # NOTE(TheJulia): A kickstart template is entirely independent
+ # of the stage2 ramdisk. In the end, it was the configuration which
+ # told anaconda how to execute.
+ if i_info.get('ks_template'):
+ # If the value is set, we always overwrite it, in the event
+ # a rebuild is occuring or something along those lines.
+ node.set_driver_internal_info('ks_template',
+ i_info['ks_template'])
+ else:
+ _get_image_properties()
+ # ks_template is an optional property on the image
+ if image_properties and 'ks_template' in image_properties:
+ node.set_driver_internal_info(
+ 'ks_template', str(image_properties['ks_template']))
else:
- _get_image_properties()
- # ks_template is an optional property on the image
- if 'ks_template' not in image_properties:
- # If not defined, default to the overall system default
- # kickstart template, as opposed to a user supplied
- # template.
- node.set_driver_internal_info(
- 'ks_template', CONF.anaconda.default_ks_template)
- else:
- node.set_driver_internal_info(
- 'ks_template', str(image_properties['ks_template']))
+ # If not defined, default to the overall system default
+ # kickstart template, as opposed to a user supplied
+ # template.
+ node.set_driver_internal_info(
+ 'ks_template',
+ 'file://' + os.path.abspath(
+ CONF.anaconda.default_ks_template
+ )
+ )
+
node.save()
for label in labels + anaconda_labels:
@@ -1253,6 +1264,8 @@ def cache_ramdisk_kernel(task, pxe_info, ipxe_enabled=False):
CONF.deploy.http_root,
'stage2')
ensure_tree(os.path.dirname(file_path))
+
+ if 'ks_cfg' in pxe_info:
# ks_cfg is rendered later by the driver using ks_template. It cannot
# be fetched and cached.
t_pxe_info.pop('ks_cfg')
diff --git a/ironic/conf/deploy.py b/ironic/conf/deploy.py
index 6ae080c83..198e0ac95 100644
--- a/ironic/conf/deploy.py
+++ b/ironic/conf/deploy.py
@@ -133,9 +133,7 @@ opts = [
'to set an explicit value for this option, and if the '
'setting or default differs from nodes, to ensure that '
'nodes are configured specifically for their desired '
- 'boot mode. This option '
- 'only has effect when management interface supports '
- 'boot mode management') % {
+ 'boot mode.') % {
'bios': boot_modes.LEGACY_BIOS,
'uefi': boot_modes.UEFI}),
cfg.BoolOpt('configdrive_use_object_store',
diff --git a/ironic/conf/ilo.py b/ironic/conf/ilo.py
index 364c64c81..197378ce7 100644
--- a/ironic/conf/ilo.py
+++ b/ironic/conf/ilo.py
@@ -120,6 +120,11 @@ opts = [
'/proc/cmdline. Mind severe cmdline size limit! Can be '
'overridden by `instance_info/kernel_append_params` '
'property.')),
+ cfg.StrOpt('cert_path',
+ default='/var/lib/ironic/ilo/',
+ help=_('On the ironic-conductor node, directory where ilo '
+ 'driver stores the CSR and the cert.')),
+
]
diff --git a/ironic/drivers/ilo.py b/ironic/drivers/ilo.py
index d8bbafb9e..b6e189ee9 100644
--- a/ironic/drivers/ilo.py
+++ b/ironic/drivers/ilo.py
@@ -1,3 +1,4 @@
+# Copyright 2022 Hewlett Packard Enterprise Development LP
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -67,7 +68,7 @@ class IloHardware(generic.GenericHardware):
@property
def supported_vendor_interfaces(self):
- """List of supported power interfaces."""
+ """List of supported vendor interfaces."""
return [vendor.VendorPassthru, noop.NoVendor]
diff --git a/ironic/drivers/modules/ilo/common.py b/ironic/drivers/modules/ilo/common.py
index 2b5b8c0db..13f975c67 100644
--- a/ironic/drivers/modules/ilo/common.py
+++ b/ironic/drivers/modules/ilo/common.py
@@ -1,3 +1,4 @@
+# Copyright 2022 Hewlett Packard Enterprise Development LP
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -31,6 +32,7 @@ from ironic.common import boot_devices
from ironic.common import exception
from ironic.common.glance_service import service_utils
from ironic.common.i18n import _
+from ironic.common import image_service
from ironic.common import images
from ironic.common import swift
from ironic.common import utils
@@ -494,6 +496,26 @@ def update_ipmi_properties(task):
task.node.driver_info = info
+def update_redfish_properties(task):
+ """Update redfish properties to node driver_info
+
+ This method updates the node's driver info with redfish driver driver_info.
+ :param task: a task from TaskManager.
+ """
+ node = task.node
+ info = node.driver_info
+
+ # updating redfish credentials
+ info['redfish_address'] = info.get('ilo_address')
+ info['redfish_username'] = info.get('ilo_username')
+ info['redfish_password'] = info.get('ilo_password')
+ info['redfish_verify_ca'] = info.get('ilo_verify_ca')
+ info['redfish_system_id'] = '/redfish/v1/Systems/1'
+
+ # saving redfish credentials to task object
+ task.node.driver_info = info
+
+
def _get_floppy_image_name(node):
"""Returns the floppy image name for a given node.
@@ -1126,3 +1148,23 @@ def setup_uefi_https(task, iso, persistent=False):
except ilo_error.IloError as ilo_exception:
raise exception.IloOperationError(operation=operation,
error=ilo_exception)
+
+
+def download(target_file, file_url):
+ """Downloads file based on the scheme.
+
+ It downloads the file (url) to given location.
+ The supported url schemes are file, http, and https.
+ :param target_file: target file for copying the downloaded file.
+ :param file_url: source file url from where file needs to be downloaded.
+ :raises: ImageDownloadFailed, on failure to download the file.
+ """
+ parsed_url = urlparse.urlparse(file_url)
+ if parsed_url.scheme == "file":
+ src_file = parsed_url.path
+ with open(target_file, 'wb') as fd:
+ image_service.FileImageService().download(src_file, fd)
+ elif parsed_url.scheme in ('http', 'https'):
+ src_file = parsed_url.geturl()
+ with open(target_file, 'wb') as fd:
+ image_service.HttpImageService().download(src_file, fd)
diff --git a/ironic/drivers/modules/ilo/management.py b/ironic/drivers/modules/ilo/management.py
index c9a8259e6..5c4f03fb6 100644
--- a/ironic/drivers/modules/ilo/management.py
+++ b/ironic/drivers/modules/ilo/management.py
@@ -14,7 +14,8 @@
"""
iLO Management Interface
"""
-
+import os
+import shutil
from urllib import parse as urlparse
from ironic_lib import metrics_utils
@@ -79,6 +80,27 @@ _RESET_ILO_CREDENTIALS_ARGSINFO = {
}
}
+_CREATE_CSR_ARGSINFO = {
+ 'csr_params': {
+ 'description': (
+ "This arguments represents the information needed "
+ "to create the CSR certificate. The keys to be provided are "
+ "City, CommonName, OrgName, State."
+ ),
+ 'required': True
+ }
+}
+
+_ADD_HTTPS_CERT_ARGSINFO = {
+ 'cert_file': {
+ 'description': (
+ "This argument represents the path to the signed HTTPS "
+ "certificate which will be added to the iLO."
+ ),
+ 'required': True
+ }
+}
+
_SECURITY_PARAMETER_UPDATE_ARGSINFO = {
'security_parameters': {
'description': (
@@ -574,6 +596,61 @@ class IloManagement(base.ManagementInterface):
"parameter for node %(node)s is updated",
{'node': node.uuid})
+ @METRICS.timer('IloManagement.create_csr')
+ @base.clean_step(priority=0, abortable=False,
+ argsinfo=_CREATE_CSR_ARGSINFO)
+ def create_csr(self, task, **kwargs):
+ """Creates the CSR.
+
+ :param task: a TaskManager object.
+ """
+ node = task.node
+ csr_params = kwargs.get('csr_params')
+ csr_path = CONF.ilo.cert_path
+ path = os.path.join(csr_path, task.node.uuid)
+ if not os.path.exists(path):
+ os.makedirs(path, 0o755)
+
+ LOG.debug("Creating CSR for node %(node)s ..",
+ {'node': node.uuid})
+ _execute_ilo_step(node, 'create_csr', path, csr_params)
+ LOG.info("Creation of CSR for node %(node)s is "
+ "completed.", {'node': node.uuid})
+
+ @METRICS.timer('IloManagement.add_https_certificate')
+ @base.clean_step(priority=0, abortable=False,
+ argsinfo=_ADD_HTTPS_CERT_ARGSINFO)
+ def add_https_certificate(self, task, **kwargs):
+ """Adds the signed HTTPS certificate to the iLO.
+
+ :param task: a TaskManager object.
+ """
+ node = task.node
+ csr_path = CONF.ilo.cert_path
+ path = os.path.join(csr_path, task.node.uuid)
+ if not os.path.exists(path):
+ os.makedirs(path, 0o755)
+ cert_file_name = node.uuid + ".crt"
+ cert_file_path = os.path.join(path, cert_file_name)
+ cert_file = kwargs.get('cert_file')
+ url_scheme = urlparse.urlparse(cert_file).scheme
+ if url_scheme == '':
+ shutil.copy(cert_file, cert_file_path)
+ elif url_scheme in ('http', 'https', 'file'):
+ ilo_common.download(cert_file_path, cert_file)
+ else:
+ msg = (_("The url scheme %(scheme)s not supported with clean step "
+ "%(step)s") % {'scheme': url_scheme,
+ 'step': 'add_https_certificate'})
+ raise exception.IloOperationNotSupported(operation='clean step',
+ error=msg)
+
+ LOG.debug("Adding the signed HTTPS certificate to the "
+ "node %(node)s ..", {'node': node.uuid})
+ _execute_ilo_step(node, 'add_https_certificate', cert_file_path)
+ LOG.info("Adding of HTTPS certificate to the node %(node)s "
+ "is completed.", {'node': node.uuid})
+
@METRICS.timer('IloManagement.update_firmware')
@base.deploy_step(priority=0, argsinfo=_FIRMWARE_UPDATE_ARGSINFO)
@base.clean_step(priority=0, abortable=False,
diff --git a/ironic/drivers/modules/ilo/vendor.py b/ironic/drivers/modules/ilo/vendor.py
index 2f4986a2f..fa0400703 100644
--- a/ironic/drivers/modules/ilo/vendor.py
+++ b/ironic/drivers/modules/ilo/vendor.py
@@ -1,3 +1,4 @@
+# Copyright 2022 Hewlett Packard Enterprise Development LP
# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -25,16 +26,14 @@ from ironic.conductor import utils as manager_utils
from ironic.drivers import base
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules.ilo import common as ilo_common
+from ironic.drivers.modules.redfish import vendor as redfish_vendor
METRICS = metrics_utils.get_metrics_logger(__name__)
-class VendorPassthru(base.VendorInterface):
+class VendorPassthru(redfish_vendor.RedfishVendorPassthru):
"""Vendor-specific interfaces for iLO deploy drivers."""
- def get_properties(self):
- return {}
-
@METRICS.timer('IloVendorPassthru.validate')
def validate(self, task, method, **kwargs):
"""Validate vendor-specific actions.
@@ -50,10 +49,26 @@ class VendorPassthru(base.VendorInterface):
passed.
:raises: InvalidParameterValue, if any of the parameters have invalid
value.
+ :raises: IloOperationNotSupported, if the driver does not support the
+ given operation with ilo vendor interface.
"""
if method == 'boot_into_iso':
self._validate_boot_into_iso(task, kwargs)
return
+ redfish_event_methods = ['create_subscription',
+ 'delete_subscription',
+ 'get_all_subscriptions', 'get_subscription']
+ if method in redfish_event_methods:
+ self._validate_is_it_a_supported_system(task)
+ ilo_common.parse_driver_info(task.node)
+ ilo_common.update_redfish_properties(task)
+ if method == 'eject_vmedia':
+ error_message = _(method + (
+ " can not be performed as the driver does not support "
+ "eject_vmedia through ilo vendor interface"))
+ raise exception.IloOperationNotSupported(operation=method,
+ error=error_message)
+
super(VendorPassthru, self).validate(task, method, **kwargs)
def _validate_boot_into_iso(self, task, kwargs):
@@ -99,3 +114,23 @@ class VendorPassthru(base.VendorInterface):
ilo_common.setup_vmedia(task, kwargs['boot_iso_href'],
ramdisk_options=None)
manager_utils.node_power_action(task, states.REBOOT)
+
+ def _validate_is_it_a_supported_system(self, task):
+ """Verify and raise an exception if it is not a supported system.
+
+ :param task: A TaskManager object.
+ :param kwargs: The arguments sent with vendor passthru.
+ :raises: IloOperationNotSupported, if the node is not a Gen10 or
+ Gen10 Plus system.
+ """
+
+ node = task.node
+ ilo_object = ilo_common.get_ilo_object(node)
+ product_name = ilo_object.get_product_name()
+ operation = _("Event methods")
+ error_message = _(operation + (
+ " can not be performed as the driver does not support Event "
+ "methods on the given node"))
+ if 'Gen10' not in product_name:
+ raise exception.IloOperationNotSupported(operation=operation,
+ error=error_message)
diff --git a/ironic/drivers/modules/snmp.py b/ironic/drivers/modules/snmp.py
index 4e700c6f8..d544d5687 100644
--- a/ironic/drivers/modules/snmp.py
+++ b/ironic/drivers/modules/snmp.py
@@ -799,6 +799,341 @@ class SNMPDriverBaytechMRP27(SNMPDriverSimple):
value_power_on = 1
+class SNMPDriverServerTechSentry3(SNMPDriverBase):
+ """SNMP driver class for Server Technology Sentry 3 PDUs.
+
+ ftp://ftp.servertech.com/Pub/SNMP/sentry3/Sentry3.mib
+
+ SNMP objects for Server Technology Power PDU.
+ 1.3.6.1.4.1.1718.3.2.3.1.5.1.1.<outlet ID> outletStatus
+ Read 0=off, 1=on, 2=off wait, 3=on wait, [...more options follow]
+ 1.3.6.1.4.1.1718.3.2.3.1.11.1.1.<outlet ID> outletControlAction
+ Write 0=no action, 1=on, 2=off, 3=reboot
+ """
+
+ oid_device = (1718, 3, 2, 3, 1)
+ oid_tower_infeed_idx = (1, 1, )
+ oid_power_status = (5,)
+ oid_power_action = (11,)
+
+ status_off = 0
+ status_on = 1
+ status_off_wait = 2
+ status_on_wait = 3
+
+ value_power_on = 1
+ value_power_off = 2
+
+ def __init__(self, *args, **kwargs):
+ super(SNMPDriverServerTechSentry3, self).__init__(*args, **kwargs)
+ # Due to its use of different OIDs for different actions, we only form
+ # an OID that holds the common substring of the OIDs for power
+ # operations.
+ self.oid_base = self.oid_enterprise + self.oid_device
+
+ def _snmp_oid(self, oid):
+ """Return the OID for one of the outlet control objects.
+
+ :param oid: The action-dependent portion of the OID, as a tuple of
+ integers.
+ :returns: The full OID as a tuple of integers.
+ """
+
+ outlet = self.snmp_info['outlet']
+ full_oid = self.oid_base + oid + self.oid_tower_infeed_idx + (outlet,)
+ return full_oid
+
+ def _snmp_power_state(self):
+ oid = self._snmp_oid(self.oid_power_status)
+ state = self.client.get(oid)
+
+ # Translate the state to an Ironic power state.
+ if state in (self.status_on, self.status_off_wait):
+ power_state = states.POWER_ON
+ elif state in (self.status_off, self.status_on_wait):
+ power_state = states.POWER_OFF
+ else:
+ LOG.warning("SeverTech Sentry3 PDU %(addr)s oid %(oid) outlet "
+ "%(outlet)s: unrecognised power state %(state)s.",
+ {'addr': self.snmp_info['address'],
+ 'oid': oid,
+ 'outlet': self.snmp_info['outlet'],
+ 'state': state})
+ power_state = states.ERROR
+
+ return power_state
+
+ def _snmp_power_on(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_on)
+ self.client.set(oid, value)
+
+ def _snmp_power_off(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_off)
+ self.client.set(oid, value)
+
+
+class SNMPDriverServerTechSentry4(SNMPDriverBase):
+ """SNMP driver class for Server Technology Sentry 4 PDUs.
+
+ https://www.servertech.com/support/sentry-mib-oid-tree-downloads
+
+ SNMP objects for Server Technology Power PDU.
+ 1.3.6.1.4.1.1718.4.1.8.5.1.1<outlet ID> outletStatus
+ notSet (0) fixedOn (1) idleOff (2) idleOn (3) [...more options follow]
+ pendOn (8) pendOff (9) off (10) on (11) [...more options follow]
+ eventOff (16) eventOn (17) eventReboot (18) eventShutdown (19)
+ 1.3.6.1.4.1.1718.4.1.8.5.1.2.<outlet ID> outletControlAction
+ Write 0=no action, 1=on, 2=off, 3=reboot
+ """
+
+ oid_device = (1718, 4, 1, 8, 5, 1)
+ oid_tower_infeed_idx = (1, 1, )
+ oid_power_status = (1,)
+ oid_power_action = (2,)
+
+ notSet = 0
+ fixedOn = 1
+ idleOff = 2
+ idleOn = 3
+ wakeOff = 4
+ wakeOn = 5
+ ocpOff = 6
+ ocpOn = 7
+ status_pendOn = 8
+ status_pendOff = 9
+ status_off = 10
+ status_on = 11
+ reboot = 12
+ shutdown = 13
+ lockedOff = 14
+ lockedOn = 15
+
+ value_power_on = 1
+ value_power_off = 2
+
+ def __init__(self, *args, **kwargs):
+ super(SNMPDriverServerTechSentry4, self).__init__(*args, **kwargs)
+ # Due to its use of different OIDs for different actions, we only form
+ # an OID that holds the common substring of the OIDs for power
+ # operations.
+ self.oid_base = self.oid_enterprise + self.oid_device
+
+ def _snmp_oid(self, oid):
+ """Return the OID for one of the outlet control objects.
+
+ :param oid: The action-dependent portion of the OID, as a tuple of
+ integers.
+ :returns: The full OID as a tuple of integers.
+ """
+
+ outlet = self.snmp_info['outlet']
+ full_oid = self.oid_base + oid + self.oid_tower_infeed_idx + (outlet,)
+ return full_oid
+
+ def _snmp_power_state(self):
+ oid = self._snmp_oid(self.oid_power_status)
+ state = self.client.get(oid)
+
+ # Translate the state to an Ironic power state.
+ if state in (self.status_on, self.status_pendOn, self.idleOn):
+ power_state = states.POWER_ON
+ elif state in (self.status_off, self.status_pendOff):
+ power_state = states.POWER_OFF
+ else:
+ LOG.warning("ServerTech Sentry4 PDU %(addr)s oid %(oid)s outlet "
+ "%(outlet)s: unrecognised power state %(state)s.",
+ {'addr': self.snmp_info['address'],
+ 'oid': oid,
+ 'outlet': self.snmp_info['outlet'],
+ 'state': state})
+ power_state = states.ERROR
+
+ return power_state
+
+ def _snmp_power_on(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_on)
+ self.client.set(oid, value)
+
+ def _snmp_power_off(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_off)
+ self.client.set(oid, value)
+
+
+class SNMPDriverRaritanPDU2(SNMPDriverBase):
+ """SNMP driver class for Raritan PDU2 PDUs.
+
+ http://support.raritan.com/px2/version-2.4.1/mibs/pdu2-mib-020400-39592.txt
+ http://cdn.raritan.com/download/PX/v1.5.20/PDU-MIB.txt
+
+ Command:
+ snmpset -v2c -c private -m+PDU2-MIB <pdu IP address> \
+ PDU2-MIB::switchingOperation.1.4 = cycle
+ snmpset -v2c -c private <pdu IP address> \
+ .1.3.6.1.4.1.13742.6.4.1.2.1.2.1.4 i 2
+ Output:
+ PDU2-MIB::switchingOperation.1.4 = INTEGER: cycle(2)
+ """
+
+ oid_device = (13742, 6, 4, 1, 2, 1)
+ oid_power_action = (2, )
+ oid_power_status = (3, )
+ oid_tower_infeed_idx = (1, )
+
+ unavailable = -1
+ status_open = 0
+ status_closed = 1
+ belowLowerCritical = 2
+ belowLowerWarning = 3
+ status_normal = 4
+ aboveUpperWarning = 5
+ aboveUpperCritical = 6
+ status_on = 7
+ status_off = 8
+ detected = 9
+ notDetected = 10
+ alarmed = 11
+ ok = 12
+ marginal = 13
+ fail = 14
+ yes = 15
+ no = 16
+ standby = 17
+ one = 18
+ two = 19
+ inSync = 20
+ outOfSync = 21
+
+ value_power_on = 1
+ value_power_off = 0
+
+ def __init__(self, *args, **kwargs):
+ super(SNMPDriverRaritanPDU2, self).__init__(*args, **kwargs)
+ # Due to its use of different OIDs for different actions, we only form
+ # an OID that holds the common substring of the OIDs for power
+ # operations.
+ self.oid_base = self.oid_enterprise + self.oid_device
+
+ def _snmp_oid(self, oid):
+ """Return the OID for one of the outlet control objects.
+
+ :param oid: The action-dependent portion of the OID, as a tuple of
+ integers.
+ :returns: The full OID as a tuple of integers.
+ """
+
+ outlet = self.snmp_info['outlet']
+ full_oid = self.oid_base + oid + self.oid_tower_infeed_idx + (outlet,)
+ return full_oid
+
+ def _snmp_power_state(self):
+ oid = self._snmp_oid(self.oid_power_status)
+ state = self.client.get(oid)
+
+ # Translate the state to an Ironic power state.
+ if state == self.status_on:
+ power_state = states.POWER_ON
+ elif state == self.status_off:
+ power_state = states.POWER_OFF
+ else:
+ LOG.warning("Raritan PDU2 PDU %(addr)s oid %(oid)s outlet "
+ "%(outlet)s: unrecognised power state %(state)s.",
+ {'addr': self.snmp_info['address'],
+ 'oid': oid,
+ 'outlet': self.snmp_info['outlet'],
+ 'state': state})
+ power_state = states.ERROR
+
+ return power_state
+
+ def _snmp_power_on(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_on)
+ self.client.set(oid, value)
+
+ def _snmp_power_off(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_off)
+ self.client.set(oid, value)
+
+
+class SNMPDriverVertivGeistPDU(SNMPDriverBase):
+ """SNMP driver class for VertivGeist NU30017L/NU30019L PDU.
+
+ https://mibs.observium.org/mib/GEIST-V5-MIB/
+
+ """
+
+ oid_device = (21239, 5, 2, 3, 5, 1)
+ oid_power_action = (6, )
+ oid_power_status = (4, )
+ oid_tower_infeed_idx = (1, )
+
+ on = 1
+ off = 2
+ on2off = 3
+ off2on = 4
+ rebootOn = 5
+ rebootOff = 5
+ unavailable = 7
+
+ value_power_on = 2
+ value_power_off = 4
+
+ def __init__(self, *args, **kwargs):
+ super(SNMPDriverVertivGeistPDU, self).__init__(*args, **kwargs)
+ # Due to its use of different OIDs for different actions, we only form
+ # an OID that holds the common substring of the OIDs for power
+ # operations.
+ self.oid_base = self.oid_enterprise + self.oid_device
+
+ def _snmp_oid(self, oid):
+ """Return the OID for one of the outlet control objects.
+
+ :param oid: The action-dependent portion of the OID, as a tuple of
+ integers.
+
+ :returns: The full OID as a tuple of integers.
+ """
+
+ outlet = self.snmp_info['outlet']
+ full_oid = self.oid_base + oid + (outlet,)
+ return full_oid
+
+ def _snmp_power_state(self):
+ oid = self._snmp_oid(self.oid_power_status)
+ state = self.client.get(oid)
+
+ # Translate the state to an Ironic power state.
+ if state in (self.on, self.on2off):
+ power_state = states.POWER_ON
+ elif state in (self.off, self.off2on):
+ power_state = states.POWER_OFF
+ else:
+ LOG.warning("Vertiv Geist PDU %(addr)s oid %(oid)s outlet "
+ "%(outlet)s: unrecognised power state %(state)s.",
+ {'addr': self.snmp_info['address'],
+ 'oid': oid,
+ 'outlet': self.snmp_info['outlet'],
+ 'state': state})
+ power_state = states.ERROR
+
+ return power_state
+
+ def _snmp_power_on(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_on)
+ self.client.set(oid, value)
+
+ def _snmp_power_off(self):
+ oid = self._snmp_oid(self.oid_power_action)
+ value = snmp.Integer(self.value_power_off)
+ self.client.set(oid, value)
+
+
class SNMPDriverAuto(SNMPDriverBase):
SYS_OBJ_OID = (1, 3, 6, 1, 2, 1, 1, 2)
@@ -878,6 +1213,10 @@ DRIVER_CLASSES = {
'eatonpower': SNMPDriverEatonPower,
'teltronix': SNMPDriverTeltronix,
'baytech_mrp27': SNMPDriverBaytechMRP27,
+ 'servertech_sentry3': SNMPDriverServerTechSentry3,
+ 'servertech_sentry4': SNMPDriverServerTechSentry4,
+ 'raritan_pdu2': SNMPDriverRaritanPDU2,
+ 'vertivgeist_pdu': SNMPDriverVertivGeistPDU,
'auto': SNMPDriverAuto,
}
diff --git a/ironic/tests/unit/common/test_pxe_utils.py b/ironic/tests/unit/common/test_pxe_utils.py
index 3e57b83ed..4d4fbb5b5 100644
--- a/ironic/tests/unit/common/test_pxe_utils.py
+++ b/ironic/tests/unit/common/test_pxe_utils.py
@@ -1357,7 +1357,7 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
'LiveOS',
'squashfs.img')),
'ks_template':
- (CONF.anaconda.default_ks_template,
+ ('file://' + CONF.anaconda.default_ks_template,
os.path.join(CONF.deploy.http_root,
self.node.uuid,
'ks.cfg.template')),
@@ -1375,63 +1375,7 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
self.assertEqual(expected_info, image_info)
# In the absense of kickstart template in both instance_info and
# image default kickstart template is used
- self.assertEqual(CONF.anaconda.default_ks_template,
- image_info['ks_template'][0])
- calls = [mock.call(task.node), mock.call(task.node)]
- boot_opt_mock.assert_has_calls(calls)
- # Instance info gets presedence over kickstart template on the
- # image
- properties['properties'] = {'ks_template': 'glance://template_id'}
- task.node.instance_info['ks_template'] = 'https://server/fake.tmpl'
- image_show_mock.return_value = properties
- image_info = pxe_utils.get_instance_image_info(
- task, ipxe_enabled=False)
- self.assertEqual('https://server/fake.tmpl',
- image_info['ks_template'][0])
-
- @mock.patch('ironic.drivers.modules.deploy_utils.get_boot_option',
- return_value='kickstart', autospec=True)
- @mock.patch.object(image_service.GlanceImageService, 'show', autospec=True)
- def test_get_instance_image_info_with_kickstart_url(
- self, image_show_mock, boot_opt_mock):
- properties = {'properties': {u'kernel_id': u'instance_kernel_uuid',
- u'ramdisk_id': u'instance_ramdisk_uuid',
- u'image_source': u'http://path/to/os/'}}
-
- expected_info = {'ramdisk':
- ('instance_ramdisk_uuid',
- os.path.join(CONF.pxe.tftp_root,
- self.node.uuid,
- 'ramdisk')),
- 'kernel':
- ('instance_kernel_uuid',
- os.path.join(CONF.pxe.tftp_root,
- self.node.uuid,
- 'kernel')),
- 'ks_template':
- (CONF.anaconda.default_ks_template,
- os.path.join(CONF.deploy.http_root,
- self.node.uuid,
- 'ks.cfg.template')),
- 'ks_cfg':
- ('',
- os.path.join(CONF.deploy.http_root,
- self.node.uuid,
- 'ks.cfg'))}
- image_show_mock.return_value = properties
- self.context.auth_token = 'fake'
- with task_manager.acquire(self.context, self.node.uuid,
- shared=True) as task:
- dii = task.node.driver_internal_info
- dii['is_source_a_path'] = True
- task.node.driver_internal_info = dii
- task.node.save()
- image_info = pxe_utils.get_instance_image_info(
- task, ipxe_enabled=False)
- self.assertEqual(expected_info, image_info)
- # In the absense of kickstart template in both instance_info and
- # image default kickstart template is used
- self.assertEqual(CONF.anaconda.default_ks_template,
+ self.assertEqual('file://' + CONF.anaconda.default_ks_template,
image_info['ks_template'][0])
calls = [mock.call(task.node), mock.call(task.node)]
boot_opt_mock.assert_has_calls(calls)
@@ -1463,7 +1407,7 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
self.node.uuid,
'kernel')),
'ks_template':
- (CONF.anaconda.default_ks_template,
+ ('file://' + CONF.anaconda.default_ks_template,
os.path.join(CONF.deploy.http_root,
self.node.uuid,
'ks.cfg.template')),
@@ -1490,7 +1434,7 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
self.assertEqual(expected_info, image_info)
# In the absense of kickstart template in both instance_info and
# image default kickstart template is used
- self.assertEqual(CONF.anaconda.default_ks_template,
+ self.assertEqual('file://' + CONF.anaconda.default_ks_template,
image_info['ks_template'][0])
calls = [mock.call(task.node), mock.call(task.node)]
boot_opt_mock.assert_has_calls(calls)
@@ -1577,6 +1521,46 @@ class PXEInterfacesTestCase(db_base.DbTestCase):
list(fake_pxe_info.values()),
True)
+ @mock.patch.object(os, 'chmod', autospec=True)
+ @mock.patch.object(pxe_utils, 'TFTPImageCache', lambda: None)
+ @mock.patch.object(pxe_utils, 'ensure_tree', autospec=True)
+ @mock.patch.object(deploy_utils, 'fetch_images', autospec=True)
+ def test_cache_ramdisk_kernel_ipxe_anaconda(self, mock_fetch_image,
+ mock_ensure_tree, mock_chmod):
+ expected_path = os.path.join(CONF.deploy.http_root,
+ self.node.uuid)
+ fake_pxe_info = {'ramdisk':
+ ('instance_ramdisk_uuid',
+ os.path.join(CONF.pxe.tftp_root,
+ self.node.uuid,
+ 'ramdisk')),
+ 'kernel':
+ ('instance_kernel_uuid',
+ os.path.join(CONF.pxe.tftp_root,
+ self.node.uuid,
+ 'kernel')),
+ 'ks_template':
+ ('file://' + CONF.anaconda.default_ks_template,
+ os.path.join(CONF.deploy.http_root,
+ self.node.uuid,
+ 'ks.cfg.template')),
+ 'ks_cfg':
+ ('',
+ os.path.join(CONF.deploy.http_root,
+ self.node.uuid,
+ 'ks.cfg'))}
+ expected = fake_pxe_info.copy()
+ expected.pop('ks_cfg')
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ pxe_utils.cache_ramdisk_kernel(task, fake_pxe_info,
+ ipxe_enabled=True)
+ mock_ensure_tree.assert_called_with(expected_path)
+ mock_fetch_image.assert_called_once_with(self.context, mock.ANY,
+ list(expected.values()),
+ True)
+
@mock.patch.object(pxe.PXEBoot, '__init__', lambda self: None)
class PXEBuildKickstartConfigOptionsTestCase(db_base.DbTestCase):
diff --git a/ironic/tests/unit/drivers/modules/ilo/test_common.py b/ironic/tests/unit/drivers/modules/ilo/test_common.py
index 352eb0837..c3e22453f 100644
--- a/ironic/tests/unit/drivers/modules/ilo/test_common.py
+++ b/ironic/tests/unit/drivers/modules/ilo/test_common.py
@@ -1,3 +1,4 @@
+# Copyright 2022 Hewlett Packard Enterprise Development LP
# Copyright 2014 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
@@ -30,6 +31,7 @@ from oslo_utils import uuidutils
from ironic.common import boot_devices
from ironic.common import exception
+from ironic.common import image_service
from ironic.common import images
from ironic.common import swift
from ironic.conductor import task_manager
@@ -374,6 +376,22 @@ class IloCommonMethodsTestCase(BaseIloTest):
expected_info = dict(self.info, **ipmi_info)
self.assertEqual(expected_info, actual_info)
+ def test_update_redfish_properties(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ redfish_info = {
+ "redfish_address": "1.2.3.4",
+ "redfish_username": "admin",
+ "redfish_password": "fake",
+ "redfish_verify_ca": None,
+ "redfish_system_id": "/redfish/v1/Systems/1"
+ }
+ task.node.driver_info = self.info
+ ilo_common.update_redfish_properties(task)
+ actual_info = task.node.driver_info
+ expected_info = dict(self.info, **redfish_info)
+ self.assertEqual(expected_info, actual_info)
+
def test__get_floppy_image_name(self):
image_name_expected = 'image-' + self.node.uuid
image_name_actual = ilo_common._get_floppy_image_name(self.node)
@@ -1504,3 +1522,37 @@ class IloCommonMethodsTestCase(BaseIloTest):
self.assertRaises(exception.IloOperationError,
ilo_common.setup_uefi_https,
task, iso, True)
+
+ @mock.patch.object(image_service, 'FileImageService', spec_set=True,
+ autospec=True)
+ @mock.patch.object(image_service, 'HttpImageService', spec_set=True,
+ autospec=True)
+ @mock.patch.object(builtins, 'open', autospec=True)
+ def test_download_file_url(self, open_mock, http_mock, file_mock):
+ url = "file:///test1/iLO.crt"
+ target_file = "/a/b/c"
+ fd_mock = mock.MagicMock(spec=io.BytesIO)
+ open_mock.return_value = fd_mock
+ fd_mock.__enter__.return_value = fd_mock
+ ilo_common.download(target_file, url)
+ open_mock.assert_called_once_with(target_file, 'wb')
+ http_mock.assert_not_called()
+ file_mock.return_value.download.assert_called_once_with(
+ "/test1/iLO.crt", fd_mock)
+
+ @mock.patch.object(image_service, 'FileImageService', spec_set=True,
+ autospec=True)
+ @mock.patch.object(image_service, 'HttpImageService', spec_set=True,
+ autospec=True)
+ @mock.patch.object(builtins, 'open', autospec=True)
+ def test_download_http_url(self, open_mock, http_mock, file_mock):
+ url = "http://1.1.1.1/iLO.crt"
+ target_file = "/a/b/c"
+ fd_mock = mock.MagicMock(spec=io.BytesIO)
+ open_mock.return_value = fd_mock
+ fd_mock.__enter__.return_value = fd_mock
+ ilo_common.download(target_file, url)
+ http_mock.return_value.download.assert_called_once_with(
+ "http://1.1.1.1/iLO.crt", fd_mock)
+ file_mock.assert_not_called()
+ open_mock.assert_called_once_with(target_file, 'wb')
diff --git a/ironic/tests/unit/drivers/modules/ilo/test_management.py b/ironic/tests/unit/drivers/modules/ilo/test_management.py
index e4d891c3d..f087c4d58 100644
--- a/ironic/tests/unit/drivers/modules/ilo/test_management.py
+++ b/ironic/tests/unit/drivers/modules/ilo/test_management.py
@@ -14,9 +14,12 @@
"""Test class for Management Interface used by iLO modules."""
+import os
+import shutil
from unittest import mock
import ddt
+from oslo_config import cfg
from oslo_utils import importutils
from oslo_utils import uuidutils
@@ -42,6 +45,8 @@ ilo_error = importutils.try_import('proliantutils.exception')
INFO_DICT = db_utils.get_test_ilo_info()
+CONF = cfg.CONF
+
@ddt.ddt
class IloManagementTestCase(test_common.BaseIloTest):
@@ -424,6 +429,116 @@ class IloManagementTestCase(test_common.BaseIloTest):
step_mock.assert_called_once_with(
task.node, 'update_authentication_failure_logging', '1', False)
+ @mock.patch.object(ilo_management, '_execute_ilo_step',
+ spec_set=True, autospec=True)
+ @mock.patch.object(os, 'makedirs', spec_set=True, autospec=True)
+ def test_create_csr(self, os_mock, step_mock):
+ csr_params_args = {
+ "City": "Bangalore",
+ "CommonName": "1.1.1.1",
+ "Country": "ABC",
+ "OrgName": "DEF",
+ "State": "IJK"
+ }
+ csr_args = {
+ "csr_params": csr_params_args}
+ CONF.ilo.cert_path = "/var/lib/ironic/ilo"
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ task.driver.management.create_csr(task, **csr_args)
+ cert_path = os.path.join(CONF.ilo.cert_path, self.node.uuid)
+ step_mock.assert_called_once_with(task.node, 'create_csr',
+ cert_path, csr_params_args)
+ os_mock.assert_called_once_with(cert_path, 0o755)
+
+ @mock.patch.object(ilo_management, '_execute_ilo_step',
+ spec_set=True, autospec=True)
+ @mock.patch.object(os, 'makedirs', spec_set=True, autospec=True)
+ @mock.patch.object(shutil, 'copy', spec_set=True, autospec=True)
+ def test_add_https_certificate(self, shutil_mock, os_mock,
+ step_mock):
+ CONF.ilo.cert_path = "/var/lib/ironic/ilo"
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ cert_file_args = {'cert_file': '/test1/cert'}
+ task.driver.management.add_https_certificate(
+ task, **cert_file_args)
+ cert_path = os.path.join(CONF.ilo.cert_path, self.node.uuid)
+ cert_path_name = os.path.join(cert_path, self.node.uuid)
+ filename = cert_path_name + ".crt"
+ step_mock.assert_called_once_with(
+ task.node, 'add_https_certificate', filename)
+ os_mock.assert_called_once_with(cert_path, 0o755)
+ shutil_mock.assert_called_once_with('/test1/cert', filename)
+
+ @mock.patch.object(ilo_management, '_execute_ilo_step',
+ spec_set=True, autospec=True)
+ @mock.patch.object(os, 'makedirs', spec_set=True, autospec=True)
+ @mock.patch.object(shutil, 'copy', spec_set=True, autospec=True)
+ @mock.patch.object(ilo_common, 'download', spec_set=True, autospec=True)
+ def test_add_https_certificate_fileurl(self, download_mock, shutil_mock,
+ os_mock, step_mock):
+ CONF.ilo.cert_path = "/var/lib/ironic/ilo"
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ cert_file_args = {'cert_file': 'file:///test1/cert'}
+ task.driver.management.add_https_certificate(
+ task, **cert_file_args)
+ cert_path = os.path.join(CONF.ilo.cert_path, self.node.uuid)
+ cert_path_name = os.path.join(cert_path, self.node.uuid)
+ fname = cert_path_name + ".crt"
+ step_mock.assert_called_once_with(
+ task.node, 'add_https_certificate', fname)
+ os_mock.assert_called_once_with(cert_path, 0o755)
+ shutil_mock.assert_not_called()
+ download_mock.assert_called_once_with(fname, 'file:///test1/cert')
+
+ @mock.patch.object(ilo_management, '_execute_ilo_step',
+ spec_set=True, autospec=True)
+ @mock.patch.object(os, 'makedirs', spec_set=True, autospec=True)
+ @mock.patch.object(shutil, 'copy', spec_set=True, autospec=True)
+ @mock.patch.object(ilo_common, 'download', spec_set=True, autospec=True)
+ def test_add_https_certificate_httpurl(self, download_mock, shutil_mock,
+ os_mock, step_mock):
+ CONF.ilo.cert_path = "/var/lib/ironic/ilo"
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ cert_file_args = {'cert_file': 'http://1.1.1.1/cert'}
+ task.driver.management.add_https_certificate(
+ task, **cert_file_args)
+ cert_path = os.path.join(CONF.ilo.cert_path, self.node.uuid)
+ cert_path_name = os.path.join(cert_path, self.node.uuid)
+ fname = cert_path_name + ".crt"
+ step_mock.assert_called_once_with(
+ task.node, 'add_https_certificate', fname)
+ os_mock.assert_called_once_with(cert_path, 0o755)
+ shutil_mock.assert_not_called()
+ download_mock.assert_called_once_with(fname, 'http://1.1.1.1/cert')
+
+ @mock.patch.object(ilo_management, '_execute_ilo_step',
+ spec_set=True, autospec=True)
+ @mock.patch.object(os, 'makedirs', spec_set=True, autospec=True)
+ @mock.patch.object(shutil, 'copy', spec_set=True, autospec=True)
+ @mock.patch.object(ilo_common, 'download', spec_set=True, autospec=True)
+ def test_add_https_certificate_url_exception(self, download_mock,
+ shutil_mock, os_mock,
+ step_mock):
+ CONF.ilo.cert_path = "/var/lib/ironic/ilo"
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ cert_file_args = {'cert_file': 'swift://1.1.1.1/cert'}
+ self.assertRaises(exception.IloOperationNotSupported,
+ task.driver.management.add_https_certificate,
+ task,
+ **cert_file_args)
+
+ cert_path = os.path.join(CONF.ilo.cert_path, self.node.uuid)
+ step_mock.assert_not_called()
+ os_mock.assert_called_once_with(cert_path, 0o755)
+ shutil_mock.assert_not_called()
+ download_mock.assert_not_called()
+
@mock.patch.object(deploy_utils, 'build_agent_options',
spec_set=True, autospec=True)
@mock.patch.object(ilo_boot.IloVirtualMediaBoot, 'clean_up_ramdisk',
diff --git a/ironic/tests/unit/drivers/modules/ilo/test_vendor.py b/ironic/tests/unit/drivers/modules/ilo/test_vendor.py
index f3114826e..b7bc3cbce 100644
--- a/ironic/tests/unit/drivers/modules/ilo/test_vendor.py
+++ b/ironic/tests/unit/drivers/modules/ilo/test_vendor.py
@@ -1,3 +1,4 @@
+# Copyright 2022 Hewlett Packard Enterprise Development LP
# Copyright 2015 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
@@ -30,6 +31,7 @@ from ironic.tests.unit.drivers.modules.ilo import test_common
class VendorPassthruTestCase(test_common.BaseIloTest):
boot_interface = 'ilo-virtual-media'
+ vendor_interface = 'ilo'
@mock.patch.object(manager_utils, 'node_power_action', spec_set=True,
autospec=True)
@@ -95,3 +97,72 @@ class VendorPassthruTestCase(test_common.BaseIloTest):
task, info)
validate_image_prop_mock.assert_called_once_with(
task.context, 'foo')
+
+ @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True,
+ autospec=True)
+ def test__validate_is_it_a_supported_system(
+ self, get_ilo_object_mock):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ task.node.maintenance = True
+ ilo_mock_object = get_ilo_object_mock.return_value
+ ilo_mock_object.get_product_name.return_value = (
+ 'ProLiant DL380 Gen10')
+ task.driver.vendor._validate_is_it_a_supported_system(task)
+ get_ilo_object_mock.assert_called_once_with(task.node)
+
+ @mock.patch.object(ilo_common, 'get_ilo_object', spec_set=True,
+ autospec=True)
+ def test__validate_is_it_a_supported_system_exception(
+ self, get_ilo_object_mock):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ task.node.maintenance = True
+ ilo_mock_object = get_ilo_object_mock.return_value
+ ilo_mock_object.get_product_name.return_value = (
+ 'ProLiant DL380 Gen8')
+ self.assertRaises(
+ exception.IloOperationNotSupported,
+ task.driver.vendor._validate_is_it_a_supported_system, task)
+
+ @mock.patch.object(ilo_common, 'parse_driver_info',
+ spec_set=True, autospec=True)
+ @mock.patch.object(ilo_common, 'update_redfish_properties',
+ spec_set=True, autospec=True)
+ @mock.patch.object(ilo_vendor.VendorPassthru,
+ '_validate_is_it_a_supported_system',
+ spec_set=True, autospec=True)
+ def test_validate_create_subscription(self, validate_redfish_system_mock,
+ redfish_properties_mock,
+ driver_info_mock):
+ self.node.vendor_interface = 'ilo'
+ self.node.save()
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ d_info = {'ilo_address': '1.1.1.1',
+ 'ilo_username': 'user',
+ 'ilo_password': 'password',
+ 'ilo_verify_ca': False}
+ driver_info_mock.return_value = d_info
+ redfish_properties = {'redfish_address': '1.1.1.1',
+ 'redfish_username': 'user',
+ 'redfish_password': 'password',
+ 'redfish_system_id': '/redfish/v1/Systems/1',
+ 'redfish_verify_ca': False}
+ redfish_properties_mock.return_value = redfish_properties
+ kwargs = {'Destination': 'https://someulr',
+ 'Context': 'MyProtocol'}
+ task.driver.vendor.validate(task, 'create_subscription', **kwargs)
+ driver_info_mock.assert_called_once_with(task.node)
+ redfish_properties_mock.assert_called_once_with(task)
+ validate_redfish_system_mock.assert_called_once_with(
+ task.driver.vendor, task)
+
+ def test_validate_operation_exeption(self):
+ self.node.vendor_interface = 'ilo'
+ self.node.save()
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertRaises(
+ exception.IloOperationNotSupported,
+ task.driver.vendor.validate, task, 'eject_vmedia')
diff --git a/ironic/tests/unit/drivers/modules/test_snmp.py b/ironic/tests/unit/drivers/modules/test_snmp.py
index 6bdd2da5a..00799dc4d 100644
--- a/ironic/tests/unit/drivers/modules/test_snmp.py
+++ b/ironic/tests/unit/drivers/modules/test_snmp.py
@@ -327,6 +327,34 @@ class SNMPValidateParametersTestCase(db_base.DbTestCase):
info = snmp._parse_driver_info(node)
self.assertEqual('teltronix', info['driver'])
+ def test__parse_driver_info_servertech_sentry3(self):
+ # Make sure the servertech_sentry3 driver type is parsed.
+ info = db_utils.get_test_snmp_info(snmp_driver='servertech_sentry3')
+ node = self._get_test_node(info)
+ info = snmp._parse_driver_info(node)
+ self.assertEqual('servertech_sentry3', info['driver'])
+
+ def test__parse_driver_info_servertech_sentry4(self):
+ # Make sure the servertech_sentry4 driver type is parsed.
+ info = db_utils.get_test_snmp_info(snmp_driver='servertech_sentry4')
+ node = self._get_test_node(info)
+ info = snmp._parse_driver_info(node)
+ self.assertEqual('servertech_sentry4', info['driver'])
+
+ def test__parse_driver_info_raritan_pdu2(self):
+ # Make sure the raritan_pdu2 driver type is parsed.
+ info = db_utils.get_test_snmp_info(snmp_driver='raritan_pdu2')
+ node = self._get_test_node(info)
+ info = snmp._parse_driver_info(node)
+ self.assertEqual('raritan_pdu2', info['driver'])
+
+ def test__parse_driver_info_vertivgeist_pdu(self):
+ # Make sure the vertivgeist_pdu driver type is parsed.
+ info = db_utils.get_test_snmp_info(snmp_driver='vertivgeist_pdu')
+ node = self._get_test_node(info)
+ info = snmp._parse_driver_info(node)
+ self.assertEqual('vertivgeist_pdu', info['driver'])
+
def test__parse_driver_info_snmp_v1(self):
# Make sure SNMPv1 is parsed with a community string.
info = db_utils.get_test_snmp_info(snmp_version='1',
@@ -1260,6 +1288,58 @@ class SNMPDeviceDriverTestCase(db_base.DbTestCase):
def test_apc_rackpdu_power_reset(self, mock_get_client):
self._test_simple_device_power_reset('apc_rackpdu', mock_get_client)
+ def test_raritan_pdu2_snmp_objects(self, mock_get_client):
+ # Ensure the correct SNMP object OIDs and values are used by the
+ # Raritan PDU2 driver
+ self._update_driver_info(snmp_driver="raritan_pdu2",
+ snmp_outlet="6")
+ driver = snmp._get_driver(self.node)
+ oid = (1, 3, 6, 1, 4, 1, 13742, 6, 4, 1, 2, 1, 2, 1, 6)
+ action = (2,)
+
+ self.assertEqual(oid, driver._snmp_oid(action))
+ self.assertEqual(1, driver.value_power_on)
+ self.assertEqual(0, driver.value_power_off)
+
+ def test_servertech_sentry3_snmp_objects(self, mock_get_client):
+ # Ensure the correct SNMP object OIDs and values are used by the
+ # ServerTech Sentry3 driver
+ self._update_driver_info(snmp_driver="servertech_sentry3",
+ snmp_outlet="6")
+ driver = snmp._get_driver(self.node)
+ oid = (1, 3, 6, 1, 4, 1, 1718, 3, 2, 3, 1, 5, 1, 1, 6)
+ action = (5,)
+
+ self.assertEqual(oid, driver._snmp_oid(action))
+ self.assertEqual(1, driver.value_power_on)
+ self.assertEqual(2, driver.value_power_off)
+
+ def test_servertech_sentry4_snmp_objects(self, mock_get_client):
+ # Ensure the correct SNMP object OIDs and values are used by the
+ # ServerTech Sentry4 driver
+ self._update_driver_info(snmp_driver="servertech_sentry4",
+ snmp_outlet="6")
+ driver = snmp._get_driver(self.node)
+ oid = (1, 3, 6, 1, 4, 1, 1718, 4, 1, 8, 5, 1, 2, 1, 1, 6)
+ action = (2,)
+
+ self.assertEqual(oid, driver._snmp_oid(action))
+ self.assertEqual(1, driver.value_power_on)
+ self.assertEqual(2, driver.value_power_off)
+
+ def test_vertivgeist_pdu_snmp_objects(self, mock_get_client):
+ # Ensure the correct SNMP object OIDs and values are used by the
+ # Vertiv Geist PDU driver
+ self._update_driver_info(snmp_driver="vertivgeist_pdu",
+ snmp_outlet="6")
+ driver = snmp._get_driver(self.node)
+ oid = (1, 3, 6, 1, 4, 1, 21239, 5, 2, 3, 5, 1, 4, 6)
+ action = (4,)
+
+ self.assertEqual(oid, driver._snmp_oid(action))
+ self.assertEqual(2, driver.value_power_on)
+ self.assertEqual(4, driver.value_power_off)
+
def test_aten_snmp_objects(self, mock_get_client):
# Ensure the correct SNMP object OIDs and values are used by the
# Aten driver
diff --git a/releasenotes/notes/additonal-snmp-drivers-ae1174e6bd6ee3a6.yaml b/releasenotes/notes/additonal-snmp-drivers-ae1174e6bd6ee3a6.yaml
new file mode 100644
index 000000000..f98f2e607
--- /dev/null
+++ b/releasenotes/notes/additonal-snmp-drivers-ae1174e6bd6ee3a6.yaml
@@ -0,0 +1,5 @@
+---
+features:
+ - |
+ Adds ``raritan_pdu2``, ``servertech_sentry3``, ``servertech_sentry4``,
+ and ``vertivgest_pdu`` snmp drivers to support additional PDU models.
diff --git a/releasenotes/notes/correct-source-path-handling-lookups-4ce2023a56372f10.yaml b/releasenotes/notes/correct-source-path-handling-lookups-4ce2023a56372f10.yaml
new file mode 100644
index 000000000..10d270a45
--- /dev/null
+++ b/releasenotes/notes/correct-source-path-handling-lookups-4ce2023a56372f10.yaml
@@ -0,0 +1,16 @@
+---
+fixes:
+ - |
+ Fixes an issue where image information retrieval would fail when a
+ path was supplied when using the ``anaconda`` deploy interface,
+ as `HTTP` ``HEAD`` requests on a URL path have no ``Content-Length``.
+ We now consider if a path is used prior to attempting to collect
+ additional configuration data from what is normally expected to
+ be Glance.
+ - |
+ Fixes an issue where the fallback to a default kickstart template
+ value would result in error indicating
+ "Scheme-less image href is not a UUID".
+ This was becaues the handling code falling back to the default
+ did not explicitly indicate it was a file URL before saving the
+ value.
diff --git a/releasenotes/notes/create_csr_clean_step-a720932f61b42118.yaml b/releasenotes/notes/create_csr_clean_step-a720932f61b42118.yaml
new file mode 100644
index 000000000..1951245c1
--- /dev/null
+++ b/releasenotes/notes/create_csr_clean_step-a720932f61b42118.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Adds new clean steps ``create_csr`` and ``add_https_certificate``
+ to ``ilo`` and ``ilo5`` hardware types which allows users to
+ create Certificate Signing Request(CSR) and adds signed HTTPS
+ certificate to the iLO.
diff --git a/releasenotes/notes/ilo-event-subscription-0dadf136411bd16a.yaml b/releasenotes/notes/ilo-event-subscription-0dadf136411bd16a.yaml
new file mode 100644
index 000000000..fcfc515e4
--- /dev/null
+++ b/releasenotes/notes/ilo-event-subscription-0dadf136411bd16a.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - |
+ Provides vendor passthru methods for ``ilo`` and ``ilo5`` hardware types
+ to create, delete and get subscriptions for BMC events. These methods are
+ supported for ``HPE ProLiant Gen10`` and ``HPE ProLiant Gen10 Plus``
+ servers.
diff --git a/requirements.txt b/requirements.txt
index 24c09f50c..ae8e14f39 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -47,4 +47,4 @@ psutil>=3.2.2 # BSD
futurist>=1.2.0 # Apache-2.0
tooz>=2.7.0 # Apache-2.0
openstacksdk>=0.48.0 # Apache-2.0
-sushy>=3.10.0
+sushy>=4.3.0
diff --git a/zuul.d/ironic-jobs.yaml b/zuul.d/ironic-jobs.yaml
index c9b969d4f..9d7435bd3 100644
--- a/zuul.d/ironic-jobs.yaml
+++ b/zuul.d/ironic-jobs.yaml
@@ -217,6 +217,48 @@
s-proxy: False
- job:
+ name: ironic-standalone-anaconda
+ parent: ironic-standalone-redfish
+ description:
+ Test ironic with the anaconda deployment interface.
+ Test also uses Redfish.
+ required-projects:
+ - opendev.org/openstack/sushy-tools
+ irrelevant-files:
+ - ^.*\.rst$
+ - ^api-ref/.*$
+ - ^doc/.*$
+ - ^install-guide/.*$
+ - ^ironic/locale/.*$
+ - ^ironic/tests/.*$
+ - ^releasenotes/.*$
+ - ^setup.cfg$
+ - ^test-requirements.txt$
+ - ^tools/.*$
+ - ^tox.ini$
+ vars:
+ tempest_test_regex: BaremetalRedfishIPxeAnacondaNoGlance
+ tempest_test_timeout: 4800
+ tempest_concurrency: 2
+ devstack_localrc:
+ IRONIC_ENABLED_DEPLOY_INTERFACES: "anaconda"
+ IRONIC_VM_COUNT: 2
+ IRONIC_VM_VOLUME_COUNT: 1
+ IRONIC_VM_SPECS_RAM: 3192
+ IRONIC_VM_SPECS_CPU: 3
+ IRONIC_ENFORCE_SCOPE: True
+ # We're using a lot of disk space in this job. Some testing nodes have
+ # a small root partition, so use /opt which is mounted from a bigger
+ # ephemeral partition on such nodes
+ LIBVIRT_STORAGE_POOL_PATH: /opt/libvirt/images
+ IRONIC_ANACONDA_IMAGE_REF: http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/
+ IRONIC_ANACONDA_KERNEL_REF: http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/pxeboot/vmlinuz
+ IRONIC_ANACONDA_RAMDISK_REF: http://mirror.stream.centos.org/9-stream/BaseOS/x86_64/os/images/pxeboot/initrd.img
+ IRONIC_ANACONDA_INSECURE_HEARTBEAT: True
+ IRONIC_DEPLOY_CALLBACK_WAIT_TIMEOUT: 3600
+ IRONIC_PXE_BOOT_RETRY_TIMEOUT: 3600
+
+- job:
name: ironic-tempest-bios-redfish-pxe
description: "Deploy ironic node over PXE using BIOS boot mode"
parent: ironic-tempest-uefi-redfish-vmedia
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 8b821f816..586675f87 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -45,6 +45,8 @@
voting: false
- ironic-tempest-ipxe-ipv6:
voting: false
+ - ironic-standalone-anaconda:
+ voting: false
- ironic-inspector-tempest-rbac-scope-enforced:
voting: false
- bifrost-integration-tinyipa-ubuntu-focal: