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:
parent
ca5e89d1ed
commit
6a87c8a6ca
@ -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.")
|
||||
|
||||
|
@ -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()
|
||||
|
0
ironic/drivers/modules/msftocs/__init__.py
Normal file
0
ironic/drivers/modules/msftocs/__init__.py
Normal file
110
ironic/drivers/modules/msftocs/common.py
Normal file
110
ironic/drivers/modules/msftocs/common.py
Normal 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)
|
118
ironic/drivers/modules/msftocs/management.py
Normal file
118
ironic/drivers/modules/msftocs/management.py
Normal 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()
|
177
ironic/drivers/modules/msftocs/msftocsclient.py
Normal file
177
ironic/drivers/modules/msftocs/msftocsclient.py
Normal 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()}))
|
106
ironic/drivers/modules/msftocs/power.py
Normal file
106
ironic/drivers/modules/msftocs/power.py
Normal 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)
|
@ -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()
|
||||
|
@ -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',
|
||||
|
0
ironic/tests/drivers/msftocs/__init__.py
Normal file
0
ironic/tests/drivers/msftocs/__init__.py
Normal file
110
ironic/tests/drivers/msftocs/test_common.py
Normal file
110
ironic/tests/drivers/msftocs/test_common.py
Normal 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))
|
131
ironic/tests/drivers/msftocs/test_management.py
Normal file
131
ironic/tests/drivers/msftocs/test_management.py
Normal 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)
|
182
ironic/tests/drivers/msftocs/test_msftocsclient.py
Normal file
182
ironic/tests/drivers/msftocs/test_msftocsclient.py
Normal 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"})
|
163
ironic/tests/drivers/msftocs/test_power.py
Normal file
163
ironic/tests/drivers/msftocs/test_power.py
Normal 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)
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user