Merge "Adds OCS Power and Management interfaces"

This commit is contained in:
Jenkins 2015-05-06 19:03:25 +00:00 committed by Gerrit Code Review
commit 5df3bd1dac
15 changed files with 1142 additions and 0 deletions

View File

@ -334,6 +334,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