diff options
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: |