Merge "Adds OCS Power and Management interfaces"
This commit is contained in:
commit
5df3bd1dac
@ -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.")
|
||||
|
||||
|
@ -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