From da0c7e831e1983620dcc8e3e95fb02f68d582c3c Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Thu, 19 Jul 2018 10:48:46 +0530 Subject: [PATCH] Add Redfish as OOB driver This patch implements Refish as new OOB driver for Drydock. All the existing Drydock Orchestrator actions are implemented. Change-Id: I31d653fb41189a18c34cfafb0f490ca4f4d661b5 --- charts/drydock/values.yaml | 1 + etc/drydock/drydock.conf.sample | 19 + .../drivers/node/maasdriver/models/machine.py | 2 +- .../drivers/oob/redfish_driver/__init__.py | 0 .../oob/redfish_driver/actions/__init__.py | 0 .../drivers/oob/redfish_driver/actions/oob.py | 443 ++++++++++++++++++ .../drivers/oob/redfish_driver/client.py | 177 +++++++ .../drivers/oob/redfish_driver/driver.py | 171 +++++++ python/requirements-direct.txt | 1 + python/requirements-lock.txt | 1 + 10 files changed, 814 insertions(+), 1 deletion(-) create mode 100644 python/drydock_provisioner/drivers/oob/redfish_driver/__init__.py create mode 100644 python/drydock_provisioner/drivers/oob/redfish_driver/actions/__init__.py create mode 100644 python/drydock_provisioner/drivers/oob/redfish_driver/actions/oob.py create mode 100644 python/drydock_provisioner/drivers/oob/redfish_driver/client.py create mode 100644 python/drydock_provisioner/drivers/oob/redfish_driver/driver.py diff --git a/charts/drydock/values.yaml b/charts/drydock/values.yaml index fc0fa9ab..3e7462cc 100644 --- a/charts/drydock/values.yaml +++ b/charts/drydock/values.yaml @@ -325,6 +325,7 @@ conf: ingester: - 'drydock_provisioner.ingester.plugins.yaml.YamlIngester' oob_driver: + - 'drydock_provisioner.drivers.oob.redfish_driver.driver.RedfishDriver' - 'drydock_provisioner.drivers.oob.pyghmi_driver.driver.PyghmiDriver' - 'drydock_provisioner.drivers.oob.manual_driver.driver.ManualDriver' - 'drydock_provisioner.drivers.oob.libvirt_driver.driver.LibvirtDriver' diff --git a/etc/drydock/drydock.conf.sample b/etc/drydock/drydock.conf.sample index b0c17d54..e271d5fa 100644 --- a/etc/drydock/drydock.conf.sample +++ b/etc/drydock/drydock.conf.sample @@ -370,6 +370,25 @@ #poll_interval = 10 +[redfish_driver] + +# +# From drydock_provisioner +# + +# Maximum number of connection retries to Redfish server +#max_retries = 5 + +# Maximum reties to wait for power state change +#power_state_change_max_retries = 18 + +# Polling interval in seconds between retries for power state change +#power_state_change_retry_interval = 10 + +# Use SSL to communicate with Redfish API server (boolean value) +#use_ssl = true + + [timeouts] # diff --git a/python/drydock_provisioner/drivers/node/maasdriver/models/machine.py b/python/drydock_provisioner/drivers/node/maasdriver/models/machine.py index aa1d6b79..ca248d98 100644 --- a/python/drydock_provisioner/drivers/node/maasdriver/models/machine.py +++ b/python/drydock_provisioner/drivers/node/maasdriver/models/machine.py @@ -569,7 +569,7 @@ class Machines(model_base.ResourceCollectionBase): """ maas_node = None - if node_model.oob_type == 'ipmi': + if node_model.oob_type == 'ipmi' or node_model.oob_type == 'redfish': node_oob_network = node_model.oob_parameters['network'] node_oob_ip = node_model.get_network_address(node_oob_network) diff --git a/python/drydock_provisioner/drivers/oob/redfish_driver/__init__.py b/python/drydock_provisioner/drivers/oob/redfish_driver/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/drydock_provisioner/drivers/oob/redfish_driver/actions/__init__.py b/python/drydock_provisioner/drivers/oob/redfish_driver/actions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/drydock_provisioner/drivers/oob/redfish_driver/actions/oob.py b/python/drydock_provisioner/drivers/oob/redfish_driver/actions/oob.py new file mode 100644 index 00000000..acec8226 --- /dev/null +++ b/python/drydock_provisioner/drivers/oob/redfish_driver/actions/oob.py @@ -0,0 +1,443 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. +"""Driver for controlling OOB interface via Redfish. + +Based on Redfish Rest API specification. +""" + +import time + +from oslo_config import cfg + +from drydock_provisioner.orchestrator.actions.orchestrator import BaseAction +from drydock_provisioner.drivers.oob.redfish_driver.client import RedfishException +from drydock_provisioner.drivers.oob.redfish_driver.client import RedfishSession + +import drydock_provisioner.error as errors +import drydock_provisioner.objects.fields as hd_fields + +class RedfishBaseAction(BaseAction): + """Base action for Redfish executed actions.""" + + def get_redfish_session(self, node): + """Initialize a Redfish session to the node. + + :param node: instance of objects.BaremetalNode + :return: An instance of client.RedfishSession initialized to node's Redfish interface + """ + if node.oob_type != 'redfish': + raise errors.DriverError("Node OOB type is not Redfish") + + oob_network = node.oob_parameters['network'] + oob_address = node.get_network_address(oob_network) + if oob_address is None: + raise errors.DriverError( + "Node %s has no OOB Redfish address" % (node.name)) + + oob_account = node.oob_parameters['account'] + oob_credential = node.oob_parameters['credential'] + + self.logger.debug("Starting Redfish session to %s with %s" % + (oob_address, oob_account)) + try: + redfish_obj = RedfishSession(host=oob_address, + account=oob_account, + password=oob_credential, + use_ssl=cfg.CONF.redfish_driver.use_ssl, + connection_retries=cfg.CONF.redfish_driver.max_retries) + except (RedfishException, errors.DriverError) as iex: + self.logger.error( + "Error initializing Redfish session for node %s" % node.name) + self.logger.error("Redfish Exception: %s" % str(iex)) + redfish_obj = None + + return redfish_obj + + def exec_redfish_command(self, node, session, func, *args): + """Call a Redfish command after establishing a session. + + :param node: Instance of objects.BaremetalNode to execute against + :param session: Redfish session + :param func: The redfish Command method to call + :param args: The args to pass the func + """ + try: + self.logger.debug("Calling Redfish command %s on %s" % + (func.__name__, node.name)) + response = func(session, *args) + return response + except RedfishException as iex: + self.logger.error( + "Error executing Redfish command %s for node %s" % (func.__name__, node.name)) + self.logger.error("Redfish Exception: %s" % str(iex)) + + raise errors.DriverError("Redfish command failed.") + + +class ValidateOobServices(RedfishBaseAction): + """Action to validate OOB services are available.""" + + def start(self): + self.task.add_status_msg( + msg="OOB does not require services.", + error=False, + ctx='NA', + ctx_type='NA') + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.success() + self.task.save() + + return + + +class ConfigNodePxe(RedfishBaseAction): + """Action to configure PXE booting via OOB.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + self.task.add_status_msg( + msg="Redfish doesn't configure PXE options.", + error=True, + ctx=n.name, + ctx_type='node') + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.failure() + self.task.save() + return + + +class SetNodeBoot(RedfishBaseAction): + """Action to configure a node to PXE boot.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + self.logger.debug("Setting bootdev to PXE for %s" % n.name) + self.task.add_status_msg( + msg="Setting node to PXE boot.", + error=False, + ctx=n.name, + ctx_type='node') + + bootdev = None + try: + session = self.get_redfish_session(n) + bootdev = self.exec_redfish_command(n, session, RedfishSession.get_bootdev) + if bootdev.get('bootdev', '') != 'Pxe': + self.exec_redfish_command(n, session, RedfishSession.set_bootdev, 'Pxe') + bootdev = self.exec_redfish_command(n, session, RedfishSession.get_bootdev) + session.close_session() + except errors.DriverError: + pass + + if bootdev is not None and (bootdev.get('bootdev', + '') == 'Pxe'): + self.task.add_status_msg( + msg="Set bootdev to PXE.", + error=False, + ctx=n.name, + ctx_type='node') + self.logger.debug("%s reports bootdev of network" % n.name) + self.task.success(focus=n.name) + else: + self.task.add_status_msg( + msg="Unable to set bootdev to PXE.", + error=True, + ctx=n.name, + ctx_type='node') + self.task.failure(focus=n.name) + self.logger.warning( + "Unable to set node %s to PXE boot." % (n.name)) + + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.save() + return + + +class PowerOffNode(RedfishBaseAction): + """Action to power off a node via Redfish.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + self.logger.debug("Sending set_power = off command to %s" % n.name) + self.task.add_status_msg( + msg="Sending set_power = off command.", + error=False, + ctx=n.name, + ctx_type='node') + session = self.get_redfish_session(n) + + # If power is already off, continue with the next node + power_state = self.exec_redfish_command(n, RedfishSession.get_power) + if power_state is not None and (power_state.get( + 'powerstate', '') == 'Off'): + self.task.add_status_msg( + msg="Node reports power off.", + error=False, + ctx=n.name, + ctx_type='node') + self.logger.debug( + "Node %s reports powerstate already off. No action required" % n.name) + self.task.success(focus=n.name) + continue + + self.exec_redfish_command(n, session, RedfishSession.set_power, 'ForceOff') + + attempts = cfg.CONF.redfish_driver.power_state_change_max_retries + + while attempts > 0: + self.logger.debug("Polling powerstate waiting for success.") + power_state = self.exec_redfish_command(n, RedfishSession.get_power) + if power_state is not None and (power_state.get( + 'powerstate', '') == 'Off'): + self.task.add_status_msg( + msg="Node reports power off.", + error=False, + ctx=n.name, + ctx_type='node') + self.logger.debug( + "Node %s reports powerstate of off" % n.name) + self.task.success(focus=n.name) + break + time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval) + attempts = attempts - 1 + + if power_state is not None and (power_state.get('powerstate', '') + != 'Off'): + self.task.add_status_msg( + msg="Node failed to power off.", + error=True, + ctx=n.name, + ctx_type='node') + self.logger.error("Giving up on Redfish command to %s" % n.name) + self.task.failure(focus=n.name) + + session.close_session() + + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.save() + return + + +class PowerOnNode(RedfishBaseAction): + """Action to power on a node via Redfish.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + self.logger.debug("Sending set_power = on command to %s" % n.name) + self.task.add_status_msg( + msg="Sending set_power = on command.", + error=False, + ctx=n.name, + ctx_type='node') + session = self.get_redfish_session(n) + + # If power is already on, continue with the next node + power_state = self.exec_redfish_command(n, RedfishSession.get_power) + if power_state is not None and (power_state.get( + 'powerstate', '') == 'On'): + self.task.add_status_msg( + msg="Node reports power on.", + error=False, + ctx=n.name, + ctx_type='node') + self.logger.debug( + "Node %s reports powerstate already on. No action required" % n.name) + self.task.success(focus=n.name) + continue + + self.exec_redfish_command(n, session, RedfishSession.set_power, 'On') + + attempts = cfg.CONF.redfish_driver.power_state_change_max_retries + + while attempts > 0: + self.logger.debug("Polling powerstate waiting for success.") + power_state = self.exec_redfish_command(n, session, RedfishSession.get_power) + if power_state is not None and (power_state.get( + 'powerstate', '') == 'On'): + self.logger.debug( + "Node %s reports powerstate of on" % n.name) + self.task.add_status_msg( + msg="Node reports power on.", + error=False, + ctx=n.name, + ctx_type='node') + self.task.success(focus=n.name) + break + time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval) + attempts = attempts - 1 + + if power_state is not None and (power_state.get('powerstate', '') + != 'On'): + self.task.add_status_msg( + msg="Node failed to power on.", + error=True, + ctx=n.name, + ctx_type='node') + self.logger.error("Giving up on Redfish command to %s" % n.name) + self.task.failure(focus=n.name) + + session.close_session() + + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.save() + return + + +class PowerCycleNode(RedfishBaseAction): + """Action to hard powercycle a node via Redfish.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + self.logger.debug("Sending set_power = off command to %s" % n.name) + self.task.add_status_msg( + msg="Power cycling node via Redfish.", + error=False, + ctx=n.name, + ctx_type='node') + session = self.get_redfish_session(n) + self.exec_redfish_command(n, session, RedfishSession.set_power, 'ForceOff') + + # Wait for power state of off before booting back up + attempts = cfg.CONF.redfish_driver.power_state_change_max_retries + + while attempts > 0: + power_state = self.exec_redfish_command(n, session, RedfishSession.get_power) + if power_state is not None and power_state.get( + 'powerstate', '') == 'Off': + self.logger.debug("%s reports powerstate of off" % n.name) + break + elif power_state is None: + self.logger.debug( + "No response on Redfish power query to %s" % n.name) + time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval) + attempts = attempts - 1 + + if power_state.get('powerstate', '') != 'Off': + self.task.add_status_msg( + msg="Failed to power down during power cycle.", + error=True, + ctx=n.name, + ctx_type='node') + self.logger.warning( + "Failed powering down node %s during power cycle task" % + n.name) + self.task.failure(focus=n.name) + break + + self.logger.debug("Sending set_power = on command to %s" % n.name) + self.exec_redfish_command(n, session, RedfishSession.set_power, 'On') + + attempts = cfg.CONF.redfish_driver.power_state_change_max_retries + + while attempts > 0: + power_state = self.exec_redfish_command(n, session, RedfishSession.get_power) + if power_state is not None and power_state.get( + 'powerstate', '') == 'On': + self.logger.debug("%s reports powerstate of on" % n.name) + break + elif power_state is None: + self.logger.debug( + "No response on Redfish power query to %s" % n.name) + time.sleep(cfg.CONF.redfish_driver.power_state_change_retry_interval) + attempts = attempts - 1 + + if power_state is not None and (power_state.get('powerstate', + '') == 'On'): + self.task.add_status_msg( + msg="Node power cycle complete.", + error=False, + ctx=n.name, + ctx_type='node') + self.task.success(focus=n.name) + else: + self.task.add_status_msg( + msg="Failed to power up during power cycle.", + error=True, + ctx=n.name, + ctx_type='node') + self.logger.warning( + "Failed powering up node %s during power cycle task" % + n.name) + self.task.failure(focus=n.name) + + session.close_session() + + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.save() + return + + +class InterrogateOob(RedfishBaseAction): + """Action to complete a basic interrogation of the node Redfish interface.""" + + def start(self): + self.task.set_status(hd_fields.TaskStatus.Running) + self.task.save() + + node_list = self.orchestrator.get_target_nodes(self.task) + + for n in node_list: + try: + self.logger.debug( + "Interrogating node %s Redfish interface." % n.name) + session = self.get_redfish_session(n) + powerstate = self.exec_redfish_command(n, session, RedfishSession.get_power) + session.close_session() + if powerstate is None: + raise errors.DriverError() + self.task.add_status_msg( + msg="Redfish interface interrogation yielded powerstate %s" % + powerstate.get('powerstate'), + error=False, + ctx=n.name, + ctx_type='node') + self.task.success(focus=n.name) + except errors.DriverError: + self.logger.debug( + "Interrogating node %s Redfish interface failed." % n.name) + self.task.add_status_msg( + msg="Redfish interface interrogation failed.", + error=True, + ctx=n.name, + ctx_type='node') + self.task.failure(focus=n.name) + + self.task.set_status(hd_fields.TaskStatus.Complete) + self.task.save() + return diff --git a/python/drydock_provisioner/drivers/oob/redfish_driver/client.py b/python/drydock_provisioner/drivers/oob/redfish_driver/client.py new file mode 100644 index 00000000..48225deb --- /dev/null +++ b/python/drydock_provisioner/drivers/oob/redfish_driver/client.py @@ -0,0 +1,177 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. +"""Redfish object to provide commands. + +Uses Redfish client to communicate to node. +""" + +from redfish import AuthMethod, redfish_client +from redfish.rest.v1 import ServerDownOrUnreachableError +from redfish.rest.v1 import InvalidCredentialsError +from redfish.rest.v1 import RetriesExhaustedError + +class RedfishSession(object): + """Redfish Client to provide OOB commands""" + + def __init__(self, host, account, password, use_ssl=True, connection_retries=10): + try: + if use_ssl: + redfish_url = 'https://' + host + else: + redfish_url = 'http://' + host + self.redfish_client = redfish_client(base_url=redfish_url, + username=account, + password=password) + + self.redfish_client.MAX_RETRY = connection_retries + self.redfish_client.login(auth=AuthMethod.SESSION) + except RetriesExhaustedError: + raise RedfishException("Login failed: Retries exhausted") + except InvalidCredentialsError: + raise RedfishException("Login failed: Invalid credentials") + except ServerDownOrUnreachableError: + raise RedfishException("Login failed: Server unreachable") + + def __del__(self): + self.redfish_client.logout() + + def close_session(self): + self.redfish_client.logout() + + def get_system_instance(self): + response = self.redfish_client.get("/redfish/v1/Systems") + + if response.status != 200: + raise RedfishException(response._read) + + # Assumption that only one system is available on Node + if response.dict["Members@odata.count"] != 1: + raise RedfishException("Number of systems are more than one in the node") + instance = response.dict["Members"][0]["@odata.id"] + + return instance + + def get_bootdev(self): + """Get current boot type information from Node. + + :raises: RedfishException on an error + :return: dict -- response will return as dict in format of + {'bootdev': bootdev} + """ + instance = self.get_system_instance() + response = self.redfish_client.get(path=instance) + + if response.status != 200: + raise RedfishException(response._read) + + bootdev = response.dict["Boot"]["BootSourceOverrideTarget"] + return {'bootdev': bootdev} + + def set_bootdev(self, bootdev, **kwargs): + """Set boot type on the Node for next boot. + + :param bootdev: Boot source for the next boot + * None - Boot from the normal boot device. + * Pxe - Boot from network + * Cd - Boot from CD/DVD disc + * Usb - Boot from USB device specified by system BIOS + * Hdd - Boot from Hard drive + * BiosSetup - Boot to bios setup utility + * Utilities - Boot manufacurer utlities program + * UefiTarget - Boot to the UEFI Device specified in the + UefiTargetBootSourceOverride property + * UefiShell - Boot to the UEFI Shell + * UefiHttp - Boot from UEFI HTTP network location + :param **kwargs: To specify extra arguments for a given bootdev + Example to specify UefiTargetBootSourceOverride value + for bootdev UefiTarget + :raises: RedfishException on an error + :return: dict -- response will return as dict in format of + {'bootdev': bootdev} + """ + instance = self.get_system_instance() + + payload = { + "Boot": { + "BootSourceOverrideEnabled": "Once", + "BootSourceOverrideTarget": bootdev, + } + } + + if bootdev == 'UefiTarget': + payload['Boot']['UefiTargetBootSourceOverride'] = kwargs.get( + 'UefiTargetBootSourceOverride', '') + + response = self.redfish_client.patch(path=instance, body=payload) + if response.status != 200: + raise RedfishException(response._read) + + return {'bootdev': bootdev} + + def get_power(self): + """Get current power state information from Node. + + :raises: RedfishException on an error + :return: dict -- response will return as dict in format of + {'powerstate': powerstate} + """ + instance = self.get_system_instance() + + response = self.redfish_client.get(path=instance) + if response.status != 200: + raise RedfishException(response._read) + + powerstate = response.dict["PowerState"] + return {'powerstate': powerstate} + + def set_power(self, powerstate): + """Request power change on the node. + + :param powerstate: set power change + * On - Power On the unit + * ForceOff - Turn off immediately (non graceful) + * PushPowerButton - Simulate pressing physical + power button + * GracefulRestart - Perform a graceful shutdown + and then start + + :raises: RedfishException on an error + :return: dict -- response will return as dict in format of + {'powerstate': powerstate} + """ + instance = self.get_system_instance() + + if powerstate not in ["On", "ForceOff", "PushPowerButton", "GracefulRestart"]: + raise RedfishException("Unsupported powerstate") + + current_state = self.get_power() + if (powerstate == "On" and current_state["powerstate"] == "On") or \ + (powerstate == "ForceOff" and current_state["powerstate"] == "Off"): + return {'powerstate': powerstate} + + payload = { + "ResetType": powerstate + } + + url = instance + "/Actions/ComputerSystem.Reset" + response = self.redfish_client.post(path=url, body=payload) + if response.status in [200, 201, 204]: + return {'powerstate': powerstate} + else: + raise RedfishException(response._read) + + +class RedfishException(Exception): + """Redfish Exception with error in message""" + pass diff --git a/python/drydock_provisioner/drivers/oob/redfish_driver/driver.py b/python/drydock_provisioner/drivers/oob/redfish_driver/driver.py new file mode 100644 index 00000000..fab298da --- /dev/null +++ b/python/drydock_provisioner/drivers/oob/redfish_driver/driver.py @@ -0,0 +1,171 @@ +# Copyright 2018 AT&T Intellectual Property. All other rights reserved. +# +# 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. +"""Driver for controlling OOB interface via Redfish. + +Based on Redfish Rest API specification. +""" + +import uuid +import logging +import concurrent.futures + +from oslo_config import cfg + +import drydock_provisioner.error as errors +import drydock_provisioner.config as config + +import drydock_provisioner.objects.fields as hd_fields + +import drydock_provisioner.drivers.oob.driver as oob_driver +import drydock_provisioner.drivers.driver as generic_driver + +from .actions.oob import ValidateOobServices +from .actions.oob import ConfigNodePxe +from .actions.oob import SetNodeBoot +from .actions.oob import PowerOffNode +from .actions.oob import PowerOnNode +from .actions.oob import PowerCycleNode +from .actions.oob import InterrogateOob + + +class RedfishDriver(oob_driver.OobDriver): + """Driver for executing OOB actions via Redfish library.""" + + redfish_driver_options = [ + cfg.IntOpt( + 'max_retries', + default=10, + min=1, + help='Maximum number of connection retries to Redfish server'), + cfg.IntOpt( + 'power_state_change_max_retries', + default=18, + min=1, + help='Maximum reties to wait for power state change'), + cfg.IntOpt( + 'power_state_change_retry_interval', + default=10, + help='Polling interval in seconds between retries for power state change'), + cfg.BoolOpt( + 'use_ssl', + default=True, + help='Use SSL to communicate with Redfish API server'), + ] + + oob_types_supported = ['redfish'] + + driver_name = "redfish_driver" + driver_key = "redfish_driver" + driver_desc = "Redfish OOB Driver" + + action_class_map = { + hd_fields.OrchestratorAction.ValidateOobServices: ValidateOobServices, + hd_fields.OrchestratorAction.ConfigNodePxe: ConfigNodePxe, + hd_fields.OrchestratorAction.SetNodeBoot: SetNodeBoot, + hd_fields.OrchestratorAction.PowerOffNode: PowerOffNode, + hd_fields.OrchestratorAction.PowerOnNode: PowerOnNode, + hd_fields.OrchestratorAction.PowerCycleNode: PowerCycleNode, + hd_fields.OrchestratorAction.InterrogateOob: InterrogateOob, + } + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + cfg.CONF.register_opts( + RedfishDriver.redfish_driver_options, group=RedfishDriver.driver_key) + + self.logger = logging.getLogger( + config.config_mgr.conf.logging.oobdriver_logger_name) + + def execute_task(self, task_id): + task = self.state_manager.get_task(task_id) + + if task is None: + self.logger.error("Invalid task %s" % (task_id)) + raise errors.DriverError("Invalid task %s" % (task_id)) + + if task.action not in self.supported_actions: + self.logger.error("Driver %s doesn't support task action %s" % + (self.driver_desc, task.action)) + raise errors.DriverError("Driver %s doesn't support task action %s" + % (self.driver_desc, task.action)) + + task.set_status(hd_fields.TaskStatus.Running) + task.save() + + target_nodes = self.orchestrator.get_target_nodes(task) + + with concurrent.futures.ThreadPoolExecutor(max_workers=16) as e: + subtask_futures = dict() + for n in target_nodes: + sub_nf = self.orchestrator.create_nodefilter_from_nodelist([n]) + subtask = self.orchestrator.create_task( + action=task.action, + design_ref=task.design_ref, + node_filter=sub_nf) + task.register_subtask(subtask) + self.logger.debug( + "Starting Redfish subtask %s for action %s on node %s" % + (str(subtask.get_id()), task.action, n.name)) + + action_class = self.action_class_map.get(task.action, None) + if action_class is None: + self.logger.error( + "Could not find action resource for action %s" % + task.action) + self.task.failure() + break + action = action_class(subtask, self.orchestrator, + self.state_manager) + subtask_futures[subtask.get_id().bytes] = e.submit( + action.start) + + timeout = config.config_mgr.conf.timeouts.drydock_timeout + finished, running = concurrent.futures.wait( + subtask_futures.values(), timeout=(timeout * 60)) + + for t, f in subtask_futures.items(): + if not f.done(): + task.add_status_msg( + msg="Subtask %s timed out before completing.", + error=True, + ctx=str(uuid.UUID(bytes=t)), + ctx_type='task') + task.failure() + else: + if f.exception(): + self.logger.error( + "Uncaught exception in subtask %s" % str( + uuid.UUID(bytes=t)), + exc_info=f.exception()) + task.align_result() + task.bubble_results() + task.set_status(hd_fields.TaskStatus.Complete) + task.save() + + return + + +class RedfishActionRunner(generic_driver.DriverActionRunner): + """Threaded runner for a Redfish Action.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.logger = logging.getLogger( + config.config_mgr.conf.logging.oobdriver_logger_name) + + +def list_opts(): + return {RedfishDriver.driver_key: RedfishDriver.redfish_driver_options} diff --git a/python/requirements-direct.txt b/python/requirements-direct.txt index 974097c4..b2370295 100644 --- a/python/requirements-direct.txt +++ b/python/requirements-direct.txt @@ -24,3 +24,4 @@ ulid2==0.1.1 defusedxml===0.5.0 libvirt-python==3.10.0 beaker==1.9.1 +redfish==2.0.1 diff --git a/python/requirements-lock.txt b/python/requirements-lock.txt index 94491aef..969ef0ba 100644 --- a/python/requirements-lock.txt +++ b/python/requirements-lock.txt @@ -61,6 +61,7 @@ python-keystoneclient==3.17.0 python-mimeparse==1.6.0 pytz==2018.5 PyYAML==3.12 +redfish==2.0.1 repoze.lru==0.7 requests==2.19.1 rfc3986==1.1.0