diff --git a/ironic/common/exception.py b/ironic/common/exception.py
index 6b62c380a8..09e3fb6a24 100644
--- a/ironic/common/exception.py
+++ b/ironic/common/exception.py
@@ -331,6 +331,10 @@ class AMTFailure(IronicException):
message = _("AMT call failed: %(cmd)s.")
+class MSFTOCSClientApiException(IronicException):
+ message = _("MSFT OCS call failed.")
+
+
class SSHConnectFailed(IronicException):
message = _("Failed to establish SSH connection to host %(host)s.")
diff --git a/ironic/drivers/fake.py b/ironic/drivers/fake.py
index 0e1dde8dad..e0383ef03c 100644
--- a/ironic/drivers/fake.py
+++ b/ironic/drivers/fake.py
@@ -37,6 +37,8 @@ from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules.irmc import management as irmc_management
from ironic.drivers.modules.irmc import power as irmc_power
+from ironic.drivers.modules.msftocs import management as msftocs_management
+from ironic.drivers.modules.msftocs import power as msftocs_power
from ironic.drivers.modules import pxe
from ironic.drivers.modules import seamicro
from ironic.drivers.modules import snmp
@@ -234,3 +236,12 @@ class FakeAMTDriver(base.BaseDriver):
self.power = amt_power.AMTPower()
self.deploy = fake.FakeDeploy()
self.management = amt_mgmt.AMTManagement()
+
+
+class FakeMSFTOCSDriver(base.BaseDriver):
+ """Fake MSFT OCS driver."""
+
+ def __init__(self):
+ self.power = msftocs_power.MSFTOCSPower()
+ self.deploy = fake.FakeDeploy()
+ self.management = msftocs_management.MSFTOCSManagement()
diff --git a/ironic/drivers/modules/msftocs/__init__.py b/ironic/drivers/modules/msftocs/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ironic/drivers/modules/msftocs/common.py b/ironic/drivers/modules/msftocs/common.py
new file mode 100644
index 0000000000..97d069967b
--- /dev/null
+++ b/ironic/drivers/modules/msftocs/common.py
@@ -0,0 +1,110 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+import copy
+import re
+
+import six
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.drivers.modules.msftocs import msftocsclient
+
+REQUIRED_PROPERTIES = {
+ 'msftocs_base_url': _('Base url of the OCS chassis manager REST API, '
+ 'e.g.: http://10.0.0.1:8000. Required.'),
+ 'msftocs_blade_id': _('Blade id, must be a number between 1 and the '
+ 'maximum number of blades available in the chassis. '
+ 'Required.'),
+ 'msftocs_username': _('Username to access the chassis manager REST API. '
+ 'Required.'),
+ 'msftocs_password': _('Password to access the chassis manager REST API. '
+ 'Required.'),
+}
+
+
+def get_client_info(driver_info):
+ """Returns an instance of the REST API client and the blade id.
+
+ :param driver_info: the node's driver_info dict.
+ """
+ client = msftocsclient.MSFTOCSClientApi(driver_info['msftocs_base_url'],
+ driver_info['msftocs_username'],
+ driver_info['msftocs_password'])
+ return client, driver_info['msftocs_blade_id']
+
+
+def get_properties():
+ """Returns the driver's properties."""
+ return copy.deepcopy(REQUIRED_PROPERTIES)
+
+
+def _is_valid_url(url):
+ """Checks whether a URL is valid.
+
+ :param url: a url string.
+ :returns: True if the url is valid or None, False otherwise.
+ """
+ r = re.compile(
+ r'^https?://'
+ r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)*[A-Z]{2,6}\.?|'
+ r'localhost|'
+ r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'
+ r'(?::\d+)?'
+ r'(?:/?|[/?]\S+)$', re.IGNORECASE)
+
+ return bool(isinstance(url, six.string_types) and r.search(url))
+
+
+def _check_required_properties(driver_info):
+ """Checks if all required properties are present.
+
+ :param driver_info: the node's driver_info dict.
+ :raises: MissingParameterValue if one or more required properties are
+ missing.
+ """
+ missing_properties = set(REQUIRED_PROPERTIES) - set(driver_info)
+ if missing_properties:
+ raise exception.MissingParameterValue(
+ _('The following parameters were missing: %s') %
+ ' '.join(missing_properties))
+
+
+def parse_driver_info(node):
+ """Checks for the required properties and values validity.
+
+ :param node: the target node.
+ :raises: MissingParameterValue if one or more required properties are
+ missing.
+ :raises: InvalidParameterValue if a parameter value is invalid.
+ """
+ driver_info = node.driver_info
+ _check_required_properties(driver_info)
+
+ base_url = driver_info.get('msftocs_base_url')
+ if not _is_valid_url(base_url):
+ raise exception.InvalidParameterValue(
+ _('"%s" is not a valid "msftocs_base_url"') % base_url)
+
+ blade_id = driver_info.get('msftocs_blade_id')
+ try:
+ blade_id = int(blade_id)
+ except ValueError:
+ raise exception.InvalidParameterValue(
+ _('"%s" is not a valid "msftocs_blade_id"') % blade_id)
+ if blade_id < 1:
+ raise exception.InvalidParameterValue(
+ _('"msftocs_blade_id" must be greater than 0. The provided value '
+ 'is: %s') % blade_id)
diff --git a/ironic/drivers/modules/msftocs/management.py b/ironic/drivers/modules/msftocs/management.py
new file mode 100644
index 0000000000..d9a6600d96
--- /dev/null
+++ b/ironic/drivers/modules/msftocs/management.py
@@ -0,0 +1,118 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.conductor import task_manager
+from ironic.drivers import base
+from ironic.drivers.modules.msftocs import common as msftocs_common
+from ironic.drivers.modules.msftocs import msftocsclient
+from ironic.drivers import utils as drivers_utils
+
+BOOT_TYPE_TO_DEVICE_MAP = {
+ msftocsclient.BOOT_TYPE_FORCE_PXE: boot_devices.PXE,
+ msftocsclient.BOOT_TYPE_FORCE_DEFAULT_HDD: boot_devices.DISK,
+ msftocsclient.BOOT_TYPE_FORCE_INTO_BIOS_SETUP: boot_devices.BIOS,
+}
+DEVICE_TO_BOOT_TYPE_MAP = {v: k for k, v in BOOT_TYPE_TO_DEVICE_MAP.items()}
+
+DEFAULT_BOOT_DEVICE = boot_devices.DISK
+
+
+class MSFTOCSManagement(base.ManagementInterface):
+ def get_properties(self):
+ """Returns the driver's properties."""
+ return msftocs_common.get_properties()
+
+ def validate(self, task):
+ """Validate the driver_info in the node.
+
+ Check if the driver_info contains correct required fields.
+
+ :param task: a TaskManager instance containing the target node.
+ :raises: MissingParameterValue if any required parameters are missing.
+ :raises: InvalidParameterValue if any parameters have invalid values.
+ """
+ msftocs_common.parse_driver_info(task.node)
+
+ def get_supported_boot_devices(self):
+ """Get a list of the supported boot devices.
+
+ :returns: A list with the supported boot devices.
+ """
+ return list(BOOT_TYPE_TO_DEVICE_MAP.values())
+
+ def _check_valid_device(self, device, node):
+ """Checks if the desired boot device is valid for this driver.
+
+ :param device: a boot device.
+ :param node: the target node.
+ :raises: InvalidParameterValue if the boot device is not valid.
+ """
+ if device not in DEVICE_TO_BOOT_TYPE_MAP:
+ raise exception.InvalidParameterValue(
+ _("set_boot_device called with invalid device %(device)s for "
+ "node %(node_id)s.") %
+ {'device': device, 'node_id': node.uuid})
+
+ @task_manager.require_exclusive_lock
+ def set_boot_device(self, task, device, persistent=False):
+ """Set the boot device for the task's node.
+
+ Set the boot device to use on next boot of the node.
+
+ :param task: a task from TaskManager.
+ :param device: the boot device.
+ :param persistent: Boolean value. True if the boot device will
+ persist to all future boots, False if not.
+ Default: False.
+ :raises: InvalidParameterValue if an invalid boot device is specified.
+ """
+ self._check_valid_device(device, task.node)
+ client, blade_id = msftocs_common.get_client_info(
+ task.node.driver_info)
+
+ boot_mode = drivers_utils.get_node_capability(task.node, 'boot_mode')
+ uefi = (boot_mode == 'uefi')
+
+ boot_type = DEVICE_TO_BOOT_TYPE_MAP[device]
+ client.set_next_boot(blade_id, boot_type, persistent, uefi)
+
+ def get_boot_device(self, task):
+ """Get the current boot device for the task's node.
+
+ Returns the current boot device of the node.
+
+ :param task: a task from TaskManager.
+ :returns: a dictionary containing:
+ :boot_device: the boot device
+ :persistent: Whether the boot device will persist to all
+ future boots or not, None if it is unknown.
+ """
+ client, blade_id = msftocs_common.get_client_info(
+ task.node.driver_info)
+ device = BOOT_TYPE_TO_DEVICE_MAP.get(
+ client.get_next_boot(blade_id), DEFAULT_BOOT_DEVICE)
+
+ # Note(alexpilotti): Although the ChasssisManager REST API allows to
+ # specify the persistent boot status in SetNextBoot, currently it does
+ # not provide a way to retrieve the value with GetNextBoot.
+ # This is being addressed in the ChassisManager API.
+ return {'boot_device': device,
+ 'persistent': None}
+
+ def get_sensors_data(self, task):
+ raise NotImplementedError()
diff --git a/ironic/drivers/modules/msftocs/msftocsclient.py b/ironic/drivers/modules/msftocs/msftocsclient.py
new file mode 100644
index 0000000000..92df607a2f
--- /dev/null
+++ b/ironic/drivers/modules/msftocs/msftocsclient.py
@@ -0,0 +1,177 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+MSFT OCS ChassisManager v2.0 REST API client
+https://github.com/MSOpenTech/ChassisManager
+"""
+
+import posixpath
+from xml.etree import ElementTree
+
+from oslo_log import log
+import requests
+from requests import auth
+from requests import exceptions as requests_exceptions
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.common.i18n import _LE
+
+LOG = log.getLogger(__name__)
+
+WCSNS = 'http://schemas.datacontract.org/2004/07/Microsoft.GFS.WCS.Contracts'
+COMPLETION_CODE_SUCCESS = "Success"
+
+BOOT_TYPE_UNKNOWN = 0
+BOOT_TYPE_NO_OVERRIDE = 1
+BOOT_TYPE_FORCE_PXE = 2
+BOOT_TYPE_FORCE_DEFAULT_HDD = 3
+BOOT_TYPE_FORCE_INTO_BIOS_SETUP = 4
+BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE = 5
+
+BOOT_TYPE_MAP = {
+ 'Unknown': BOOT_TYPE_UNKNOWN,
+ 'NoOverride': BOOT_TYPE_NO_OVERRIDE,
+ 'ForcePxe': BOOT_TYPE_FORCE_PXE,
+ 'ForceDefaultHdd': BOOT_TYPE_FORCE_DEFAULT_HDD,
+ 'ForceIntoBiosSetup': BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
+ 'ForceFloppyOrRemovable': BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE,
+}
+
+POWER_STATUS_ON = "ON"
+POWER_STATUS_OFF = "OFF"
+
+
+class MSFTOCSClientApi(object):
+ def __init__(self, base_url, username, password):
+ self._base_url = base_url
+ self._username = username
+ self._password = password
+
+ def _exec_cmd(self, rel_url):
+ """Executes a command by calling the chassis manager API."""
+ url = posixpath.join(self._base_url, rel_url)
+ try:
+ response = requests.get(
+ url, auth=auth.HTTPBasicAuth(self._username, self._password))
+ response.raise_for_status()
+ except requests_exceptions.RequestException as ex:
+ LOG.exception(_LE("HTTP call failed: %s"), ex)
+ raise exception.MSFTOCSClientApiException(
+ _("HTTP call failed: %s") % ex.message)
+
+ xml_response = response.text
+ LOG.debug("Call to %(url)s got response: %(xml_response)s",
+ {"url": url, "xml_response": xml_response})
+ return xml_response
+
+ def _check_completion_code(self, xml_response):
+ try:
+ et = ElementTree.fromstring(xml_response)
+ except ElementTree.ParseError as ex:
+ LOG.exception(_LE("XML parsing failed: %s"), ex)
+ raise exception.MSFTOCSClientApiException(
+ _("Invalid XML: %s") % xml_response)
+ item = et.find("./n:completionCode", namespaces={'n': WCSNS})
+ if item is None or item.text != COMPLETION_CODE_SUCCESS:
+ raise exception.MSFTOCSClientApiException(
+ _("Operation failed: %s") % xml_response)
+ return et
+
+ def get_blade_state(self, blade_id):
+ """Returns whether a blade's chipset is receiving power (soft-power).
+
+ :param blade_id: the blade id
+ :returns: one of:
+ POWER_STATUS_ON,
+ POWER_STATUS_OFF
+ :raises: MSFTOCSClientApiException
+ """
+ et = self._check_completion_code(
+ self._exec_cmd("GetBladeState?bladeId=%d" % blade_id))
+ return et.find('./n:bladeState', namespaces={'n': WCSNS}).text
+
+ def set_blade_on(self, blade_id):
+ """Supplies power to a blade chipset (soft-power state).
+
+ :param blade_id: the blade id
+ :raises: MSFTOCSClientApiException
+ """
+ self._check_completion_code(
+ self._exec_cmd("SetBladeOn?bladeId=%d" % blade_id))
+
+ def set_blade_off(self, blade_id):
+ """Shuts down a given blade (soft-power state).
+
+ :param blade_id: the blade id
+ :raises: MSFTOCSClientApiException
+ """
+ self._check_completion_code(
+ self._exec_cmd("SetBladeOff?bladeId=%d" % blade_id))
+
+ def set_blade_power_cycle(self, blade_id, off_time=0):
+ """Performs a soft reboot of a given blade.
+
+ :param blade_id: the blade id
+ :param off_time: seconds to wait between shutdown and boot
+ :raises: MSFTOCSClientApiException
+ """
+ self._check_completion_code(
+ self._exec_cmd("SetBladeActivePowerCycle?bladeId=%(blade_id)d&"
+ "offTime=%(off_time)d" %
+ {"blade_id": blade_id, "off_time": off_time}))
+
+ def get_next_boot(self, blade_id):
+ """Returns the next boot device configured for a given blade.
+
+ :param blade_id: the blade id
+ :returns: one of:
+ BOOT_TYPE_UNKNOWN,
+ BOOT_TYPE_NO_OVERRIDE,
+ BOOT_TYPE_FORCE_PXE, BOOT_TYPE_FORCE_DEFAULT_HDD,
+ BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
+ BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE
+ :raises: MSFTOCSClientApiException
+ """
+ et = self._check_completion_code(
+ self._exec_cmd("GetNextBoot?bladeId=%d" % blade_id))
+ return BOOT_TYPE_MAP[
+ et.find('./n:nextBoot', namespaces={'n': WCSNS}).text]
+
+ def set_next_boot(self, blade_id, boot_type, persistent=True, uefi=True):
+ """Sets the next boot device for a given blade.
+
+ :param blade_id: the blade id
+ :param boot_type: possible values:
+ BOOT_TYPE_UNKNOWN,
+ BOOT_TYPE_NO_OVERRIDE,
+ BOOT_TYPE_FORCE_PXE,
+ BOOT_TYPE_FORCE_DEFAULT_HDD,
+ BOOT_TYPE_FORCE_INTO_BIOS_SETUP,
+ BOOT_TYPE_FORCE_FLOPPY_OR_REMOVABLE
+ :param persistent: whether this setting affects the next boot only or
+ every subsequent boot
+ :param uefi: True if UEFI, False otherwise
+ :raises: MSFTOCSClientApiException
+ """
+ self._check_completion_code(
+ self._exec_cmd(
+ "SetNextBoot?bladeId=%(blade_id)d&bootType=%(boot_type)d&"
+ "uefi=%(uefi)s&persistent=%(persistent)s" %
+ {"blade_id": blade_id,
+ "boot_type": boot_type,
+ "uefi": str(uefi).lower(),
+ "persistent": str(persistent).lower()}))
diff --git a/ironic/drivers/modules/msftocs/power.py b/ironic/drivers/modules/msftocs/power.py
new file mode 100644
index 0000000000..e0c8824644
--- /dev/null
+++ b/ironic/drivers/modules/msftocs/power.py
@@ -0,0 +1,106 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+MSFT OCS Power Driver
+"""
+from oslo_log import log
+
+from ironic.common import exception
+from ironic.common.i18n import _
+from ironic.common.i18n import _LE
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers import base
+from ironic.drivers.modules.msftocs import common as msftocs_common
+from ironic.drivers.modules.msftocs import msftocsclient
+
+LOG = log.getLogger(__name__)
+
+POWER_STATES_MAP = {
+ msftocsclient.POWER_STATUS_ON: states.POWER_ON,
+ msftocsclient.POWER_STATUS_OFF: states.POWER_OFF,
+}
+
+
+class MSFTOCSPower(base.PowerInterface):
+ def get_properties(self):
+ """Returns the driver's properties."""
+ return msftocs_common.get_properties()
+
+ def validate(self, task):
+ """Validate the driver_info in the node.
+
+ Check if the driver_info contains correct required fields.
+
+ :param task: a TaskManager instance containing the target node.
+ :raises: MissingParameterValue if any required parameters are missing.
+ :raises: InvalidParameterValue if any parameters have invalid values.
+ """
+ msftocs_common.parse_driver_info(task.node)
+
+ def get_power_state(self, task):
+ """Get the power state from the node.
+
+ :param task: a TaskManager instance containing the target node.
+ :raises: MSFTOCSClientApiException.
+ """
+ client, blade_id = msftocs_common.get_client_info(
+ task.node.driver_info)
+ return POWER_STATES_MAP[client.get_blade_state(blade_id)]
+
+ @task_manager.require_exclusive_lock
+ def set_power_state(self, task, pstate):
+ """Set the power state of the node.
+
+ Turn the node power on or off.
+
+ :param task: a TaskManager instance contains the target node.
+ :param pstate : The desired power state of the node.
+ :raises: PowerStateFailure if the power cannot set to pstate.
+ :raises: InvalidParameterValue
+ """
+ client, blade_id = msftocs_common.get_client_info(
+ task.node.driver_info)
+
+ try:
+ if pstate == states.POWER_ON:
+ client.set_blade_on(blade_id)
+ elif pstate == states.POWER_OFF:
+ client.set_blade_off(blade_id)
+ else:
+ raise exception.InvalidParameterValue(
+ _('Unsupported target_state: %s') % pstate)
+ except exception.MSFTOCSClientApiException as ex:
+ LOG.exception(_LE("Changing the power state to %(pstate)s failed. "
+ "Error: %(err_msg)s"),
+ {"pstate": pstate, "err_msg": ex})
+ raise exception.PowerStateFailure(pstate=pstate)
+
+ @task_manager.require_exclusive_lock
+ def reboot(self, task):
+ """Cycle the power of the node
+
+ :param task: a TaskManager instance contains the target node.
+ :raises: PowerStateFailure if failed to reboot.
+ """
+ client, blade_id = msftocs_common.get_client_info(
+ task.node.driver_info)
+ try:
+ client.set_blade_power_cycle(blade_id)
+ except exception.MSFTOCSClientApiException as ex:
+ LOG.exception(_LE("Reboot failed. Error: %(err_msg)s"),
+ {"err_msg": ex})
+ raise exception.PowerStateFailure(pstate=states.REBOOT)
diff --git a/ironic/drivers/pxe.py b/ironic/drivers/pxe.py
index 33f6e764b6..abd7951a2a 100644
--- a/ironic/drivers/pxe.py
+++ b/ironic/drivers/pxe.py
@@ -35,6 +35,8 @@ from ironic.drivers.modules import ipminative
from ironic.drivers.modules import ipmitool
from ironic.drivers.modules.irmc import management as irmc_management
from ironic.drivers.modules.irmc import power as irmc_power
+from ironic.drivers.modules.msftocs import management as msftocs_management
+from ironic.drivers.modules.msftocs import power as msftocs_power
from ironic.drivers.modules import pxe
from ironic.drivers.modules import seamicro
from ironic.drivers.modules import snmp
@@ -266,3 +268,20 @@ class PXEAndAMTDriver(base.BaseDriver):
self.deploy = pxe.PXEDeploy()
self.management = amt_management.AMTManagement()
self.vendor = amt_vendor.AMTPXEVendorPassthru()
+
+
+class PXEAndMSFTOCSDriver(base.BaseDriver):
+ """PXE + MSFT OCS driver.
+
+ This driver implements the `core` functionality, combining
+ :class:`ironic.drivers.modules.msftocs.power.MSFTOCSPower` for power on/off
+ and reboot with :class:`ironic.driver.pxe.PXE` for image deployment.
+ Implementations are in those respective classes; this class is merely the
+ glue between them.
+ """
+
+ def __init__(self):
+ self.power = msftocs_power.MSFTOCSPower()
+ self.deploy = pxe.PXEDeploy()
+ self.management = msftocs_management.MSFTOCSManagement()
+ self.vendor = pxe.VendorPassthru()
diff --git a/ironic/tests/db/utils.py b/ironic/tests/db/utils.py
index dbb8e8d954..0f1a58f1c9 100644
--- a/ironic/tests/db/utils.py
+++ b/ironic/tests/db/utils.py
@@ -129,6 +129,15 @@ def get_test_amt_info():
}
+def get_test_msftocs_info():
+ return {
+ "msftocs_base_url": "http://fakehost:8000",
+ "msftocs_username": "admin",
+ "msftocs_password": "fake",
+ "msftocs_blade_id": 1,
+ }
+
+
def get_test_agent_instance_info():
return {
'image_source': 'fake-image',
diff --git a/ironic/tests/drivers/msftocs/__init__.py b/ironic/tests/drivers/msftocs/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ironic/tests/drivers/msftocs/test_common.py b/ironic/tests/drivers/msftocs/test_common.py
new file mode 100644
index 0000000000..75fe71fe28
--- /dev/null
+++ b/ironic/tests/drivers/msftocs/test_common.py
@@ -0,0 +1,110 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+Test class for MSFT OCS common functions
+"""
+
+import mock
+
+from ironic.common import exception
+from ironic.conductor import task_manager
+from ironic.drivers.modules.msftocs import common as msftocs_common
+from ironic.tests.conductor import utils as mgr_utils
+from ironic.tests.db import base as db_base
+from ironic.tests.db import utils as db_utils
+from ironic.tests.objects import utils as obj_utils
+
+INFO_DICT = db_utils.get_test_msftocs_info()
+
+
+class MSFTOCSCommonTestCase(db_base.DbTestCase):
+ def setUp(self):
+ super(MSFTOCSCommonTestCase, self).setUp()
+ mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
+ self.info = INFO_DICT
+ self.node = obj_utils.create_test_node(self.context,
+ driver='fake_msftocs',
+ driver_info=self.info)
+
+ def test_get_client_info(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ driver_info = task.node.driver_info
+ (client, blade_id) = msftocs_common.get_client_info(driver_info)
+
+ self.assertEqual(driver_info['msftocs_base_url'], client._base_url)
+ self.assertEqual(driver_info['msftocs_username'], client._username)
+ self.assertEqual(driver_info['msftocs_password'], client._password)
+ self.assertEqual(driver_info['msftocs_blade_id'], blade_id)
+
+ @mock.patch.object(msftocs_common, '_is_valid_url', autospec=True)
+ def test_parse_driver_info(self, mock_is_valid_url):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ msftocs_common.parse_driver_info(task.node)
+ mock_is_valid_url.assert_called_once_with(
+ task.node.driver_info['msftocs_base_url'])
+
+ def test_parse_driver_info_fail_missing_param(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ del task.node.driver_info['msftocs_base_url']
+ self.assertRaises(exception.MissingParameterValue,
+ msftocs_common.parse_driver_info,
+ task.node)
+
+ def test_parse_driver_info_fail_bad_url(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ task.node.driver_info['msftocs_base_url'] = "bad-url"
+ self.assertRaises(exception.InvalidParameterValue,
+ msftocs_common.parse_driver_info,
+ task.node)
+
+ def test_parse_driver_info_fail_bad_blade_id_type(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ task.node.driver_info['msftocs_blade_id'] = "bad-blade-id"
+ self.assertRaises(exception.InvalidParameterValue,
+ msftocs_common.parse_driver_info,
+ task.node)
+
+ def test_parse_driver_info_fail_bad_blade_id_value(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ task.node.driver_info['msftocs_blade_id'] = 0
+ self.assertRaises(exception.InvalidParameterValue,
+ msftocs_common.parse_driver_info,
+ task.node)
+
+ def test__is_valid_url(self):
+ self.assertIs(True, msftocs_common._is_valid_url("http://fake.com"))
+ self.assertIs(
+ True, msftocs_common._is_valid_url("http://www.fake.com"))
+ self.assertIs(True, msftocs_common._is_valid_url("http://FAKE.com"))
+ self.assertIs(True, msftocs_common._is_valid_url("http://fake"))
+ self.assertIs(
+ True, msftocs_common._is_valid_url("http://fake.com/blah"))
+ self.assertIs(True, msftocs_common._is_valid_url("http://localhost"))
+ self.assertIs(True, msftocs_common._is_valid_url("https://fake.com"))
+ self.assertIs(True, msftocs_common._is_valid_url("http://10.0.0.1"))
+ self.assertIs(False, msftocs_common._is_valid_url("bad-url"))
+ self.assertIs(False, msftocs_common._is_valid_url("http://.bad-url"))
+ self.assertIs(False, msftocs_common._is_valid_url("http://bad-url$"))
+ self.assertIs(False, msftocs_common._is_valid_url("http://$bad-url"))
+ self.assertIs(False, msftocs_common._is_valid_url("http://bad$url"))
+ self.assertIs(False, msftocs_common._is_valid_url(None))
+ self.assertIs(False, msftocs_common._is_valid_url(0))
diff --git a/ironic/tests/drivers/msftocs/test_management.py b/ironic/tests/drivers/msftocs/test_management.py
new file mode 100644
index 0000000000..098d25573f
--- /dev/null
+++ b/ironic/tests/drivers/msftocs/test_management.py
@@ -0,0 +1,131 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+Test class for MSFT OCS ManagementInterface
+"""
+
+import mock
+
+from ironic.common import boot_devices
+from ironic.common import exception
+from ironic.conductor import task_manager
+from ironic.drivers.modules.msftocs import common as msftocs_common
+from ironic.drivers.modules.msftocs import msftocsclient
+from ironic.drivers import utils as drivers_utils
+from ironic.tests.conductor import utils as mgr_utils
+from ironic.tests.db import base as db_base
+from ironic.tests.db import utils as db_utils
+from ironic.tests.objects import utils as obj_utils
+
+INFO_DICT = db_utils.get_test_msftocs_info()
+
+
+class MSFTOCSManagementTestCase(db_base.DbTestCase):
+ def setUp(self):
+ super(MSFTOCSManagementTestCase, self).setUp()
+ mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
+ self.info = INFO_DICT
+ self.node = obj_utils.create_test_node(self.context,
+ driver='fake_msftocs',
+ driver_info=self.info)
+
+ def test_get_properties(self):
+ expected = msftocs_common.REQUIRED_PROPERTIES
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ self.assertEqual(expected, task.driver.get_properties())
+
+ @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
+ def test_validate(self, mock_drvinfo):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ task.driver.power.validate(task)
+ mock_drvinfo.assert_called_once_with(task.node)
+
+ @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
+ def test_validate_fail(self, mock_drvinfo):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ mock_drvinfo.side_effect = exception.InvalidParameterValue('x')
+ self.assertRaises(exception.InvalidParameterValue,
+ task.driver.power.validate,
+ task)
+
+ def test_get_supported_boot_devices(self):
+ expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.BIOS]
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ self.assertEqual(
+ sorted(expected),
+ sorted(task.driver.management.get_supported_boot_devices()))
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def _test_set_boot_device_one_time(self, persistent, uefi,
+ mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ if uefi:
+ drivers_utils.add_node_capability(task, 'boot_mode', 'uefi')
+
+ task.driver.management.set_boot_device(
+ task, boot_devices.PXE, persistent)
+
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_next_boot.assert_called_once_with(
+ blade_id, msftocsclient.BOOT_TYPE_FORCE_PXE, persistent, uefi)
+
+ def test_set_boot_device_one_time(self):
+ self._test_set_boot_device_one_time(False, False)
+
+ def test_set_boot_device_persistent(self):
+ self._test_set_boot_device_one_time(True, False)
+
+ def test_set_boot_device_uefi(self):
+ self._test_set_boot_device_one_time(True, True)
+
+ def test_set_boot_device_fail(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ self.assertRaises(exception.InvalidParameterValue,
+ task.driver.management.set_boot_device,
+ task, 'fake-device')
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_get_boot_device(self, mock_gci):
+ expected = {'boot_device': boot_devices.DISK, 'persistent': None}
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+ force_hdd = msftocsclient.BOOT_TYPE_FORCE_DEFAULT_HDD
+ mock_c.get_next_boot.return_value = force_hdd
+
+ self.assertEqual(expected,
+ task.driver.management.get_boot_device(task))
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.get_next_boot.assert_called_once_with(blade_id)
+
+ def test_get_sensor_data(self):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ self.assertRaises(NotImplementedError,
+ task.driver.management.get_sensors_data,
+ task)
diff --git a/ironic/tests/drivers/msftocs/test_msftocsclient.py b/ironic/tests/drivers/msftocs/test_msftocsclient.py
new file mode 100644
index 0000000000..66097d05e1
--- /dev/null
+++ b/ironic/tests/drivers/msftocs/test_msftocsclient.py
@@ -0,0 +1,182 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+Test class for MSFT OCS REST API client
+"""
+
+import mock
+import requests
+from requests import exceptions as requests_exceptions
+
+from ironic.common import exception
+from ironic.drivers.modules.msftocs import msftocsclient
+from ironic.tests import base
+
+
+FAKE_BOOT_RESPONSE = (
+ ''
+ 'Success'
+ '1'
+ 'Success'
+ '1'
+ 'ForcePxe'
+ '') % msftocsclient.WCSNS
+
+FAKE_BLADE_RESPONSE = (
+ ''
+ 'Success'
+ '1'
+ ''
+ '1'
+ '') % msftocsclient.WCSNS
+
+FAKE_POWER_STATE_RESPONSE = (
+ ''
+ 'Success'
+ '1'
+ 'Blade Power is On, firmware decompressed'
+ ''
+ '1'
+ '0'
+ 'ON'
+ '') % msftocsclient.WCSNS
+
+FAKE_BLADE_STATE_RESPONSE = (
+ ''
+ 'Success'
+ '1'
+ ''
+ '1'
+ 'ON'
+ '') % msftocsclient.WCSNS
+
+
+class MSFTOCSClientApiTestCase(base.TestCase):
+ def setUp(self):
+ super(MSFTOCSClientApiTestCase, self).setUp()
+ self._fake_base_url = "http://fakehost:8000"
+ self._fake_username = "admin"
+ self._fake_password = 'fake'
+ self._fake_blade_id = 1
+ self._client = msftocsclient.MSFTOCSClientApi(
+ self._fake_base_url, self._fake_username, self._fake_password)
+
+ @mock.patch.object(requests, 'get', autospec=True)
+ def test__exec_cmd(self, mock_get):
+ fake_response_text = 'fake_response_text'
+ fake_rel_url = 'fake_rel_url'
+ mock_get.return_value.text = 'fake_response_text'
+
+ self.assertEqual(fake_response_text,
+ self._client._exec_cmd(fake_rel_url))
+ mock_get.assert_called_once_with(
+ self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY)
+
+ @mock.patch.object(requests, 'get', autospec=True)
+ def test__exec_cmd_http_get_fail(self, mock_get):
+ fake_rel_url = 'fake_rel_url'
+ mock_get.side_effect = requests_exceptions.ConnectionError('x')
+
+ self.assertRaises(exception.MSFTOCSClientApiException,
+ self._client._exec_cmd,
+ fake_rel_url)
+ mock_get.assert_called_once_with(
+ self._fake_base_url + "/" + fake_rel_url, auth=mock.ANY)
+
+ def test__check_completion_code(self):
+ et = self._client._check_completion_code(FAKE_BOOT_RESPONSE)
+ self.assertEqual('{%s}BootResponse' % msftocsclient.WCSNS, et.tag)
+
+ def test__check_completion_code_fail(self):
+ self.assertRaises(exception.MSFTOCSClientApiException,
+ self._client._check_completion_code,
+ '' % msftocsclient.WCSNS)
+
+ def test__check_completion_with_bad_completion_code_fail(self):
+ self.assertRaises(exception.MSFTOCSClientApiException,
+ self._client._check_completion_code,
+ ''
+ 'Fail'
+ '' % msftocsclient.WCSNS)
+
+ def test__check_completion_code_xml_parsing_fail(self):
+ self.assertRaises(exception.MSFTOCSClientApiException,
+ self._client._check_completion_code,
+ 'bad_xml')
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_get_blade_state(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BLADE_STATE_RESPONSE
+ self.assertEqual(
+ msftocsclient.POWER_STATUS_ON,
+ self._client.get_blade_state(self._fake_blade_id))
+ mock_exec_cmd.assert_called_once_with(
+ self._client, "GetBladeState?bladeId=%d" % self._fake_blade_id)
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_set_blade_on(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
+ self._client.set_blade_on(self._fake_blade_id)
+ mock_exec_cmd.assert_called_once_with(
+ self._client, "SetBladeOn?bladeId=%d" % self._fake_blade_id)
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_set_blade_off(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
+ self._client.set_blade_off(self._fake_blade_id)
+ mock_exec_cmd.assert_called_once_with(
+ self._client, "SetBladeOff?bladeId=%d" % self._fake_blade_id)
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_set_blade_power_cycle(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BLADE_RESPONSE
+ self._client.set_blade_power_cycle(self._fake_blade_id)
+ mock_exec_cmd.assert_called_once_with(
+ self._client,
+ "SetBladeActivePowerCycle?bladeId=%d&offTime=0" %
+ self._fake_blade_id)
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_get_next_boot(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE
+ self.assertEqual(
+ msftocsclient.BOOT_TYPE_FORCE_PXE,
+ self._client.get_next_boot(self._fake_blade_id))
+ mock_exec_cmd.assert_called_once_with(
+ self._client, "GetNextBoot?bladeId=%d" % self._fake_blade_id)
+
+ @mock.patch.object(
+ msftocsclient.MSFTOCSClientApi, '_exec_cmd', autospec=True)
+ def test_set_next_boot(self, mock_exec_cmd):
+ mock_exec_cmd.return_value = FAKE_BOOT_RESPONSE
+ self._client.set_next_boot(self._fake_blade_id,
+ msftocsclient.BOOT_TYPE_FORCE_PXE)
+ mock_exec_cmd.assert_called_once_with(
+ self._client,
+ "SetNextBoot?bladeId=%(blade_id)d&bootType=%(boot_type)d&"
+ "uefi=%(uefi)s&persistent=%(persistent)s" %
+ {"blade_id": self._fake_blade_id,
+ "boot_type": msftocsclient.BOOT_TYPE_FORCE_PXE,
+ "uefi": "true", "persistent": "true"})
diff --git a/ironic/tests/drivers/msftocs/test_power.py b/ironic/tests/drivers/msftocs/test_power.py
new file mode 100644
index 0000000000..2c7f7788e0
--- /dev/null
+++ b/ironic/tests/drivers/msftocs/test_power.py
@@ -0,0 +1,163 @@
+# Copyright 2015 Cloudbase Solutions Srl
+# All 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.
+
+"""
+Test class for MSFT OCS PowerInterface
+"""
+
+import mock
+
+from ironic.common import exception
+from ironic.common import states
+from ironic.conductor import task_manager
+from ironic.drivers.modules.msftocs import common as msftocs_common
+from ironic.drivers.modules.msftocs import msftocsclient
+from ironic.tests.conductor import utils as mgr_utils
+from ironic.tests.db import base as db_base
+from ironic.tests.db import utils as db_utils
+from ironic.tests.objects import utils as obj_utils
+
+INFO_DICT = db_utils.get_test_msftocs_info()
+
+
+class MSFTOCSPowerTestCase(db_base.DbTestCase):
+ def setUp(self):
+ super(MSFTOCSPowerTestCase, self).setUp()
+ mgr_utils.mock_the_extension_manager(driver='fake_msftocs')
+ self.info = INFO_DICT
+ self.node = obj_utils.create_test_node(self.context,
+ driver='fake_msftocs',
+ driver_info=self.info)
+
+ def test_get_properties(self):
+ expected = msftocs_common.REQUIRED_PROPERTIES
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ self.assertEqual(expected, task.driver.get_properties())
+
+ @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
+ def test_validate(self, mock_drvinfo):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ task.driver.power.validate(task)
+ mock_drvinfo.assert_called_once_with(task.node)
+
+ @mock.patch.object(msftocs_common, 'parse_driver_info', autospec=True)
+ def test_validate_fail(self, mock_drvinfo):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ mock_drvinfo.side_effect = exception.InvalidParameterValue('x')
+ self.assertRaises(exception.InvalidParameterValue,
+ task.driver.power.validate,
+ task)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_get_power_state(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+ mock_c.get_blade_state.return_value = msftocsclient.POWER_STATUS_ON
+
+ self.assertEqual(states.POWER_ON,
+ task.driver.power.get_power_state(task))
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.get_blade_state.assert_called_once_with(blade_id)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_set_power_state_on(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ task.driver.power.set_power_state(task, states.POWER_ON)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_blade_on.assert_called_once_with(blade_id)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_set_power_state_off(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ task.driver.power.set_power_state(task, states.POWER_OFF)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_blade_off.assert_called_once_with(blade_id)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_set_power_state_blade_on_fail(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ ex = exception.MSFTOCSClientApiException('x')
+ mock_c.set_blade_on.side_effect = ex
+
+ pstate = states.POWER_ON
+ self.assertRaises(exception.PowerStateFailure,
+ task.driver.power.set_power_state,
+ task, pstate)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_blade_on.assert_called_once_with(blade_id)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_set_power_state_invalid_parameter_fail(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ pstate = states.ERROR
+ self.assertRaises(exception.InvalidParameterValue,
+ task.driver.power.set_power_state,
+ task, pstate)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_reboot(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ task.driver.power.reboot(task)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_blade_power_cycle.assert_called_once_with(blade_id)
+
+ @mock.patch.object(msftocs_common, 'get_client_info', autospec=True)
+ def test_reboot_fail(self, mock_gci):
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=False) as task:
+ mock_c = mock.MagicMock(spec=msftocsclient.MSFTOCSClientApi)
+ blade_id = task.node.driver_info['msftocs_blade_id']
+ mock_gci.return_value = (mock_c, blade_id)
+
+ ex = exception.MSFTOCSClientApiException('x')
+ mock_c.set_blade_power_cycle.side_effect = ex
+
+ self.assertRaises(exception.PowerStateFailure,
+ task.driver.power.reboot,
+ task)
+ mock_gci.assert_called_once_with(task.node.driver_info)
+ mock_c.set_blade_power_cycle.assert_called_once_with(blade_id)
diff --git a/setup.cfg b/setup.cfg
index 325cf1001c..9b5b7eec7e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -53,6 +53,7 @@ ironic.drivers =
fake_irmc = ironic.drivers.fake:FakeIRMCDriver
fake_vbox = ironic.drivers.fake:FakeVirtualBoxDriver
fake_amt = ironic.drivers.fake:FakeAMTDriver
+ fake_msftocs = ironic.drivers.fake:FakeMSFTOCSDriver
iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
@@ -65,6 +66,7 @@ ironic.drivers =
pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver
pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver
pxe_amt = ironic.drivers.pxe:PXEAndAMTDriver
+ pxe_msftocs = ironic.drivers.pxe:PXEAndMSFTOCSDriver
ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration