Adds OCS Power and Management interfaces

Includes support for the Open CloudServer (OCS) chassis manager
power operations. The OCS design has been contributed by
Microsoft to the Open Compute project.

Implements blueprint msft-ocs-power-driver

Change-Id: Ic2c90ab5c8d79c55ae83dd485b3cf9281b600c23
This commit is contained in:
Alessandro Pilotti 2015-04-22 14:14:49 +03:00
parent ca5e89d1ed
commit 6a87c8a6ca
15 changed files with 1142 additions and 0 deletions

View File

@ -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.")

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()}))

View File

@ -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)

View File

@ -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()

View File

@ -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',

View File

View File

@ -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))

View File

@ -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)

View File

@ -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 = (
'<BootResponse xmlns="%s" '
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
'<completionCode>Success</completionCode>'
'<apiVersion>1</apiVersion>'
'<statusDescription>Success</statusDescription>'
'<bladeNumber>1</bladeNumber>'
'<nextBoot>ForcePxe</nextBoot>'
'</BootResponse>') % msftocsclient.WCSNS
FAKE_BLADE_RESPONSE = (
'<BladeResponse xmlns="%s" '
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
'<completionCode>Success</completionCode>'
'<apiVersion>1</apiVersion>'
'<statusDescription/>'
'<bladeNumber>1</bladeNumber>'
'</BladeResponse>') % msftocsclient.WCSNS
FAKE_POWER_STATE_RESPONSE = (
'<PowerStateResponse xmlns="%s" '
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
'<completionCode>Success</completionCode>'
'<apiVersion>1</apiVersion>'
'<statusDescription>Blade Power is On, firmware decompressed'
'</statusDescription>'
'<bladeNumber>1</bladeNumber>'
'<Decompression>0</Decompression>'
'<powerState>ON</powerState>'
'</PowerStateResponse>') % msftocsclient.WCSNS
FAKE_BLADE_STATE_RESPONSE = (
'<BladeStateResponse xmlns="%s" '
'xmlns:i="http://www.w3.org/2001/XMLSchema-instance">'
'<completionCode>Success</completionCode>'
'<apiVersion>1</apiVersion>'
'<statusDescription/>'
'<bladeNumber>1</bladeNumber>'
'<bladeState>ON</bladeState>'
'</BladeStateResponse>') % 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,
'<fake xmlns="%s"></fake>' % msftocsclient.WCSNS)
def test__check_completion_with_bad_completion_code_fail(self):
self.assertRaises(exception.MSFTOCSClientApiException,
self._client._check_completion_code,
'<fake xmlns="%s">'
'<completionCode>Fail</completionCode>'
'</fake>' % 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"})

View File

@ -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)

View File

@ -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