Source code for ironic.drivers.modules.redfish.vendor

# Copyright 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Vendor Interface for Redfish drivers and its supporting methods.
"""

from ironic_lib import metrics_utils
from oslo_log import log
import rfc3986
import sushy

from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules.redfish import boot as redfish_boot
from ironic.drivers.modules.redfish import utils as redfish_utils

LOG = log.getLogger(__name__)
METRICS = metrics_utils.get_metrics_logger(__name__)
SUBSCRIPTION_COMMON_FIELDS = {
    'Id', 'Context', 'Protocol', 'Destination', 'EventTypes'
}


[docs] class RedfishVendorPassthru(base.VendorInterface): """Vendor-specific interfaces for Redfish drivers."""
[docs] def get_properties(self): return {}
[docs] @METRICS.timer('RedfishVendorPassthru.validate') def validate(self, task, method, **kwargs): """Validate vendor-specific actions. Checks if a valid vendor passthru method was passed and validates the parameters for the vendor passthru method. :param task: a TaskManager instance containing the node to act on. :param method: method to be validated. :param kwargs: kwargs containing the vendor passthru method's parameters. :raises: InvalidParameterValue, if any of the parameters have invalid value. """ if method == 'eject_vmedia': self._validate_eject_vmedia(task, kwargs) return if method == 'create_subscription': self._validate_create_subscription(task, kwargs) return if method == 'delete_subscription': self._validate_delete_subscription(task, kwargs) return super(RedfishVendorPassthru, self).validate(task, method, **kwargs)
def _validate_eject_vmedia(self, task, kwargs): """Verify that the boot_device input is valid.""" # If a boot device is provided check that it's valid. # It is OK to eject if already ejected boot_device = kwargs.get('boot_device') if not boot_device: return system = redfish_utils.get_system(task.node) for manager in system.managers: for v_media in manager.virtual_media.get_members(): if boot_device not in v_media.media_types: raise exception.InvalidParameterValue(_( "Boot device %s is not a valid value ") % boot_device)
[docs] @METRICS.timer('RedfishVendorPassthru.eject_vmedia') @base.passthru(['POST'], description=_("Eject a virtual media device. If no device " "is provided then all attached devices will " "be ejected. " "Optional arguments: " "'boot_device' - the boot device to eject, " "either 'cd', 'dvd', 'usb', or 'floppy'")) # @task_manager.require_exclusive_lock def eject_vmedia(self, task, **kwargs): """Eject a virtual media device. Deprecated in favour of the generic API. This should be removed during the 2024.2 cycle. :param task: A TaskManager object. :param kwargs: The arguments sent with vendor passthru. The optional kwargs are:: 'boot_device': the boot device to eject """ # If boot_device not provided all vmedia devices will be ejected boot_device = kwargs.get('boot_device') redfish_boot.eject_vmedia(task, boot_device)
def _validate_create_subscription(self, task, kwargs): """Verify that the args input are valid.""" destination = kwargs.get('Destination') event_types = kwargs.get('EventTypes') # NOTE(iurygregory): Use defaults values from Redfish in case they # are not present in the args. context = kwargs.get('Context', "") protocol = kwargs.get('Protocol', "Redfish") http_headers = kwargs.get('HttpHeaders') if event_types is not None: event_service = redfish_utils.get_event_service(task.node) allowed_values = set( event_service.get_event_types_for_subscription()) if not (isinstance(event_types, list) and set(event_types).issubset(allowed_values)): raise exception.InvalidParameterValue( _("EventTypes %s is not a valid value, allowed values %s") % (str(event_types), str(allowed_values))) # NOTE(iurygregory): check only if they are strings. # BMCs will fail to create a subscription if the context, protocol or # destination are invalid. if not isinstance(context, str): raise exception.InvalidParameterValue( _("Context %s is not a valid string") % context) if not isinstance(protocol, str): raise exception.InvalidParameterValue( _("Protocol %s is not a string") % protocol) # NOTE(iurygregory): if http_headers are None there is no problem, # the validation will fail if the value is not None and not a list. if http_headers is not None and not isinstance(http_headers, list): raise exception.InvalidParameterValue( _("HttpHeaders %s is not a list of headers") % http_headers) try: parsed = rfc3986.uri_reference(destination) validator = rfc3986.validators.Validator().require_presence_of( 'scheme', 'host', ).check_validity_of( 'scheme', 'userinfo', 'host', 'path', 'query', 'fragment', ) try: validator.validate(parsed) except rfc3986.exceptions.RFC3986Exception: # NOTE(iurygregory): raise error because the parsed # destination does not contain scheme or authority. raise TypeError except TypeError: raise exception.InvalidParameterValue( _("Destination %s is not a valid URI") % destination) def _filter_subscription_fields(self, subscription_json): filter_subscription = {k: v for k, v in subscription_json.items() if k in SUBSCRIPTION_COMMON_FIELDS} return filter_subscription
[docs] @METRICS.timer('RedfishVendorPassthru.create_subscription') @base.passthru(['POST'], async_call=False, description=_("Creates a subscription on a node. " "Required argument: a dictionary of " "{'Destination': 'destination_url'}")) def create_subscription(self, task, **kwargs): """Creates a subscription. :param task: A TaskManager object. :param kwargs: The arguments sent with vendor passthru. :raises: RedfishError, if any problem occurs when trying to create a subscription. """ payload = { 'Destination': kwargs.get('Destination'), 'Protocol': kwargs.get('Protocol', "Redfish"), 'Context': kwargs.get('Context', ""), 'EventTypes': kwargs.get('EventTypes', ["Alert"]) } http_headers = kwargs.get('HttpHeaders', []) if http_headers: payload['HttpHeaders'] = http_headers try: event_service = redfish_utils.get_event_service(task.node) subscription = event_service.subscriptions.create(payload) return self._filter_subscription_fields(subscription.json) except sushy.exceptions.SushyError as e: error_msg = (_('Failed to create subscription on node %(node)s. ' 'Subscription payload: %(payload)s. ' 'Error: %(error)s') % {'node': task.node.uuid, 'payload': str(payload), 'error': e}) LOG.error(error_msg) raise exception.RedfishError(error=error_msg)
def _validate_delete_subscription(self, task, kwargs): """Verify that the args input are valid.""" # We can only check if the kwargs contain the id field. if not kwargs.get('id'): raise exception.InvalidParameterValue(_("id can't be None"))
[docs] @METRICS.timer('RedfishVendorPassthru.delete_subscription') @base.passthru(['DELETE'], async_call=False, description=_("Delete a subscription on a node. " "Required argument: a dictionary of " "{'id': 'subscription_bmc_id'}")) def delete_subscription(self, task, **kwargs): """Delete a subscription. :param task: A TaskManager object. :param kwargs: The arguments sent with vendor passthru. :raises: RedfishError, if any problem occurs when trying to delete the subscription. """ try: event_service = redfish_utils.get_event_service(task.node) redfish_subscriptions = event_service.subscriptions bmc_id = kwargs.get('id') # NOTE(iurygregory): some BMCs doesn't report the last / # in the path for the resource, since we will add the ID # we need to make sure the separator is present. separator = "" if redfish_subscriptions.path[-1] == "/" else "/" resource = redfish_subscriptions.path + separator + bmc_id subscription = redfish_subscriptions.get_member(resource) msg = (_('Sucessfuly deleted subscription %(id)s on node ' '%(node)s') % {'id': bmc_id, 'node': task.node.uuid}) subscription.delete() LOG.debug(msg) except sushy.exceptions.SushyError as e: error_msg = (_('Redfish delete_subscription failed for ' 'subscription %(id)s on node %(node)s. ' 'Error: %(error)s') % {'id': bmc_id, 'node': task.node.uuid, 'error': e}) LOG.error(error_msg) raise exception.RedfishError(error=error_msg, code=404)
[docs] @METRICS.timer('RedfishVendorPassthru.get_subscriptions') @base.passthru(['GET'], async_call=False, description=_("Returns all subscriptions on the node.")) def get_all_subscriptions(self, task, **kwargs): """Get all Subscriptions on the node :param task: A TaskManager object. :param kwargs: Not used. :raises: RedfishError, if any problem occurs when retrieving all subscriptions. """ try: event_service = redfish_utils.get_event_service(task.node) subscriptions = event_service.subscriptions.json return subscriptions except sushy.exceptions.SushyError as e: error_msg = (_('Redfish get_subscriptions failed for ' 'node %(node)s. ' 'Error: %(error)s') % {'node': task.node.uuid, 'error': e}) LOG.error(error_msg) raise exception.RedfishError(error=error_msg)
[docs] @METRICS.timer('RedfishVendorPassthru.get_subscription') @base.passthru(['GET'], async_call=False, description=_("Get a subscription on the node. " "Required argument: a dictionary of " "{'id': 'subscription_bmc_id'}")) def get_subscription(self, task, **kwargs): """Get a specific subscription on the node :param task: A TaskManager object. :param kwargs: The arguments sent with vendor passthru. :raises: RedfishError, if any problem occurs when retrieving the subscription. """ try: event_service = redfish_utils.get_event_service(task.node) redfish_subscriptions = event_service.subscriptions bmc_id = kwargs.get('id') # NOTE(iurygregory): some BMCs doesn't report the last / # in the path for the resource, since we will add the ID # we need to make sure the separator is present. separator = "" if redfish_subscriptions.path[-1] == "/" else "/" resource = redfish_subscriptions.path + separator + bmc_id subscription = event_service.subscriptions.get_member(resource) return self._filter_subscription_fields(subscription.json) except sushy.exceptions.SushyError as e: error_msg = (_('Redfish get_subscription failed for ' 'subscription %(id)s on node %(node)s. ' 'Error: %(error)s') % {'id': bmc_id, 'node': task.node.uuid, 'error': e}) LOG.error(error_msg) raise exception.RedfishError(error=error_msg)