Add AMT-PXE-Driver Power&Management&Vendor Interface

Introduce a new driver pxe-amt to extend Ironic's range to desktops.
AMT (Active Management Technology)/vPro is widely used in desktops
to remotely control the power, similar to IPMI in servers.
It will use AMT as power management and PXE as deploy management.
This patch only provides basic operations to support the workflow of
remotely deploying on AMT/vPro system.

This adds power, management and it's own vendor interface.
This also adds a new fake-amt driver.

Implements blueprint amt-pxe-driver
Change-Id: Idd9b63d124f52e24efab8b49dfe1f2e25b8387e6
This commit is contained in:
Lin Tan 2015-02-06 14:31:19 +08:00
parent 59fdc74d9f
commit f72a694784
12 changed files with 1101 additions and 7 deletions

View File

@ -11,7 +11,7 @@ pysnmp
python-scciclient
python-seamicroclient
# The drac driver imports a python module called "pywsman", however,
# The drac and amt driver import a python module called "pywsman", however,
# this does not exist on pypi.
# It is installed by the openwsman-python (on RH) or python-openwsman (on deb)
# package, from https://github.com/Openwsman/openwsman/blob/master/bindings/python/Makefile.am#L29

View File

@ -508,6 +508,19 @@
#protocol=http
#
# Options defined in ironic.drivers.modules.amt.power
#
# Maximum number of times to attempt an AMT operation, before
# failing (integer value)
#max_attempts=3
# Amount of time (in seconds) to wait, before retrying an AMT
# operation (integer value)
#action_wait=10
[api]
#

View File

@ -23,6 +23,8 @@ from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules import agent
from ironic.drivers.modules.amt import management as amt_mgmt
from ironic.drivers.modules.amt import power as amt_power
from ironic.drivers.modules import discoverd
from ironic.drivers.modules.drac import management as drac_mgmt
from ironic.drivers.modules.drac import power as drac_power
@ -214,3 +216,16 @@ class FakeIPMIToolDiscoverdDriver(base.BaseDriver):
self.vendor = ipmitool.VendorPassthru()
self.management = ipmitool.IPMIManagement()
self.inspect = discoverd.DiscoverdInspect()
class FakeAMTDriver(base.BaseDriver):
"""Fake AMT driver."""
def __init__(self):
if not importutils.try_import('pywsman'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import pywsman library"))
self.power = amt_power.AMTPower()
self.deploy = fake.FakeDeploy()
self.management = amt_mgmt.AMTManagement()

View File

@ -0,0 +1,196 @@
#
# 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.
"""
AMT Management Driver
"""
import copy
from oslo_utils import excutils
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LI
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules.amt import common as amt_common
from ironic.drivers.modules.amt import resource_uris
from ironic.openstack.common import log as logging
pywsman = importutils.try_import('pywsman')
LOG = logging.getLogger(__name__)
def _set_boot_device_order(node, boot_device):
"""Set boot device order configuration of AMT Client.
:param node: a node object
:param boot_device: the boot device
:raises: AMTFailure
:raises: AMTConnectFailure
"""
client = amt_common.get_wsman_client(node)
source = pywsman.EndPointReference(resource_uris.CIM_BootSourceSetting,
None)
device = amt_common.BOOT_DEVICES_MAPPING[boot_device]
source.add_selector('InstanceID', device)
method = 'ChangeBootOrder'
options = pywsman.ClientOptions()
options.add_selector('InstanceID', 'Intel(r) AMT: Boot Configuration 0')
options.add_property('Source', source)
try:
client.wsman_invoke(options, resource_uris.CIM_BootConfigSetting,
method)
except (exception.AMTFailure, exception.AMTConnectFailure) as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Failed to set boot device %(boot_device)s for "
"node %(node_id)s with error: %(error)s."),
{'boot_device': boot_device, 'node_id': node.uuid,
'error': e})
else:
LOG.info(_LI("Successfully set boot device %(boot_device)s for "
"node %(node_id)s"),
{'boot_device': boot_device, 'node_id': node.uuid})
def _enable_boot_config(node):
"""Enable boot configuration of AMT Client.
:param node: a node object
:raises: AMTFailure
:raises: AMTConnectFailure
"""
client = amt_common.get_wsman_client(node)
config = pywsman.EndPointReference(resource_uris.CIM_BootConfigSetting,
None)
config.add_selector('InstanceID', 'Intel(r) AMT: Boot Configuration 0')
method = 'SetBootConfigRole'
options = pywsman.ClientOptions()
options.add_selector('Name', 'Intel(r) AMT Boot Service')
options.add_property('Role', '1')
options.add_property('BootConfigSetting', config)
try:
client.wsman_invoke(options, resource_uris.CIM_BootService, method)
except (exception.AMTFailure, exception.AMTConnectFailure) as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Failed to enable boot config for node "
"%(node_id)s with error: %(error)s."),
{'node_id': node.uuid, 'error': e})
else:
LOG.info(_LI("Successfully enabled boot config for node %(node_id)s."),
{'node_id': node.uuid})
class AMTManagement(base.ManagementInterface):
def get_properties(self):
return copy.deepcopy(amt_common.COMMON_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 contains the target node
:raises: MissingParameterValue if any required parameters are missing.
:raises: InvalidParameterValue if any parameters have invalid values.
"""
# FIXME(lintan): validate hangs if unable to reach AMT, so dont
# connect to the node until bug 1314961 is resolved.
amt_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(amt_common.BOOT_DEVICES_MAPPING)
@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.
"""
node = task.node
if device not in amt_common.BOOT_DEVICES_MAPPING:
raise exception.InvalidParameterValue(_("set_boot_device called "
"with invalid device %(device)s for node %(node_id)s.") %
{'device': device, 'node_id': node.uuid})
# AMT/vPro doesnt support set boot_device persistent, so we have to
# save amt_boot_device/amt_boot_persistent in driver_internal_info.
driver_internal_info = node.driver_internal_info
driver_internal_info['amt_boot_device'] = device
driver_internal_info['amt_boot_persistent'] = persistent
node.driver_internal_info = driver_internal_info
node.save()
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.
"""
driver_internal_info = task.node.driver_internal_info
device = driver_internal_info.get('amt_boot_device')
persistent = driver_internal_info.get('amt_boot_persistent')
if not device:
device = amt_common.DEFAULT_BOOT_DEVICE
persistent = True
return {'boot_device': device,
'persistent': persistent}
def ensure_next_boot_device(self, node, boot_device):
"""Set next boot device (one time only) of AMT Client.
:param node: a node object
:param boot_device: the boot device
:raises: AMTFailure
:raises: AMTConnectFailure
"""
driver_internal_info = node.driver_internal_info
if not driver_internal_info.get('amt_boot_persistent'):
driver_internal_info['amt_boot_device'] = (
amt_common.DEFAULT_BOOT_DEVICE)
driver_internal_info['amt_boot_persistent'] = True
node.driver_internal_info = driver_internal_info
node.save()
_set_boot_device_order(node, boot_device)
_enable_boot_config(node)
def get_sensors_data(self, task):
raise NotImplementedError()

View File

@ -0,0 +1,266 @@
#
# 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.
"""
AMT Power Driver
"""
import copy
from oslo_config import cfg
from oslo_utils import excutils
from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common.i18n import _LE
from ironic.common.i18n import _LI
from ironic.common.i18n import _LW
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules.amt import common as amt_common
from ironic.drivers.modules.amt import resource_uris
from ironic.openstack.common import log as logging
from ironic.openstack.common import loopingcall
pywsman = importutils.try_import('pywsman')
opts = [
cfg.IntOpt('max_attempts',
default=3,
help='Maximum number of times to attempt an AMT operation, '
'before failing'),
cfg.IntOpt('action_wait',
default=10,
help='Amount of time (in seconds) to wait, before retrying '
'an AMT operation')
]
CONF = cfg.CONF
CONF.register_opts(opts, group='amt')
LOG = logging.getLogger(__name__)
AMT_POWER_MAP = {
states.POWER_ON: '2',
states.POWER_OFF: '8',
}
def _generate_power_action_input(action):
"""Generate Xmldoc as set_power_state input.
This generates a Xmldoc used as input for set_power_state.
:param action: the power action.
:returns: Xmldoc.
"""
method_input = "RequestPowerStateChange_INPUT"
address = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
anonymous = ('http://schemas.xmlsoap.org/ws/2004/08/addressing/'
'role/anonymous')
wsman = 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd'
namespace = resource_uris.CIM_PowerManagementService
doc = pywsman.XmlDoc(method_input)
root = doc.root()
root.set_ns(namespace)
root.add(namespace, 'PowerState', action)
child = root.add(namespace, 'ManagedElement', None)
child.add(address, 'Address', anonymous)
grand_child = child.add(address, 'ReferenceParameters', None)
grand_child.add(wsman, 'ResourceURI', resource_uris.CIM_ComputerSystem)
g_grand_child = grand_child.add(wsman, 'SelectorSet', None)
g_g_grand_child = g_grand_child.add(wsman, 'Selector', 'ManagedSystem')
g_g_grand_child.attr_add(wsman, 'Name', 'Name')
return doc
def _set_power_state(node, target_state):
"""Set power state of the AMT Client.
:param node: a node object.
:param target_state: desired power state.
:raises: AMTFailure
:raises: AMTConnectFailure
"""
client = amt_common.get_wsman_client(node)
method = 'RequestPowerStateChange'
options = pywsman.ClientOptions()
options.add_selector('Name', 'Intel(r) AMT Power Management Service')
doc = _generate_power_action_input(AMT_POWER_MAP[target_state])
try:
client.wsman_invoke(options, resource_uris.CIM_PowerManagementService,
method, doc)
except (exception.AMTFailure, exception.AMTConnectFailure) as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Failed to set power state %(state)s for "
"node %(node_id)s with error: %(error)s."),
{'state': target_state, 'node_id': node.uuid,
'error': e})
else:
LOG.info(_LI("Power state set to %(state)s for node %(node_id)s"),
{'state': target_state, 'node_id': node.uuid})
def _power_status(node):
"""Get the power status for a node.
:param node: a node object.
:returns: one of ironic.common.states POWER_OFF, POWER_ON or ERROR.
:raises: AMTFailure.
:raises: AMTConnectFailure.
"""
client = amt_common.get_wsman_client(node)
namespace = resource_uris.CIM_AssociatedPowerManagementService
try:
doc = client.wsman_get(namespace)
except (exception.AMTFailure, exception.AMTConnectFailure) as e:
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Failed to get power state for node %(node_id)s "
"with error: %(error)s."),
{'node_id': node.uuid, 'error': e})
item = "PowerState"
power_state = amt_common.xml_find(doc, namespace, item).text
for state in AMT_POWER_MAP:
if power_state == AMT_POWER_MAP[state]:
return state
return states.ERROR
def _set_and_wait(task, target_state):
"""Helper function for DynamicLoopingCall.
This method changes the power state and polls AMT until the desired
power state is reached.
:param task: a TaskManager instance contains the target node.
:param target_state: desired power state.
:returns: one of ironic.common.states.
:raises: PowerStateFailure if cannot set the node to target_state.
:raises: AMTFailure.
:raises: AMTConnectFailure
:raises: InvalidParameterValue
"""
node = task.node
driver = task.driver
if target_state not in (states.POWER_ON, states.POWER_OFF):
raise exception.InvalidParameterValue(_('Unsupported target_state: %s')
% target_state)
elif target_state == states.POWER_ON:
boot_device = node.driver_internal_info.get('amt_boot_device')
if boot_device and boot_device != amt_common.DEFAULT_BOOT_DEVICE:
driver.management.ensure_next_boot_device(node, boot_device)
def _wait(status):
status['power'] = _power_status(node)
if status['power'] == target_state:
raise loopingcall.LoopingCallDone()
if status['iter'] >= CONF.amt.max_attempts:
status['power'] = states.ERROR
LOG.warning(_LW("AMT failed to set power state %(state)s after "
"%(tries)s retries on node %(node_id)s."),
{'state': target_state, 'tries': status['iter'],
'node_id': node.uuid})
raise loopingcall.LoopingCallDone()
try:
_set_power_state(node, target_state)
except Exception:
# Log failures but keep trying
LOG.warning(_LW("AMT set power state %(state)s for node %(node)s "
"- Attempt %(attempt)s times of %(max_attempt)s "
"failed."),
{'state': target_state, 'node': node.uuid,
'attempt': status['iter'] + 1,
'max_attempt': CONF.amt.max_attempts})
status['iter'] += 1
status = {'power': None, 'iter': 0}
timer = loopingcall.FixedIntervalLoopingCall(_wait, status)
timer.start(interval=CONF.amt.action_wait).wait()
if status['power'] != target_state:
raise exception.PowerStateFailure(pstate=target_state)
return status['power']
class AMTPower(base.PowerInterface):
"""AMT Power interface.
This Power interface control the power of node by providing power on/off
and reset functions.
"""
def get_properties(self):
return copy.deepcopy(amt_common.COMMON_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 contains the target node.
:raises: MissingParameterValue if any required parameters are missing.
:raises: InvalidParameterValue if any parameters have invalid values.
"""
# FIXME(lintan): validate hangs if unable to reach AMT, so dont
# connect to the node until bug 1314961 is resolved.
amt_common.parse_driver_info(task.node)
def get_power_state(self, task):
"""Get the power state from the node.
:param task: a TaskManager instance contains the target node.
:raises: AMTFailure.
:raises: AMTConnectFailure.
"""
return _power_status(task.node)
@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: AMTFailure.
:raises: AMTConnectFailure.
:raises: InvalidParameterValue
"""
_set_and_wait(task, 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.
:raises: AMTFailure.
:raises: AMTConnectFailure.
:raises: InvalidParameterValue
"""
_set_and_wait(task, states.POWER_OFF)
_set_and_wait(task, states.POWER_ON)

View File

@ -0,0 +1,30 @@
#
# 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.
"""
AMT Vendor Methods
"""
from ironic.common import boot_devices
from ironic.conductor import task_manager
from ironic.drivers import base
from ironic.drivers.modules import pxe
class AMTPXEVendorPassthru(pxe.VendorPassthru):
@base.passthru(['POST'], method='pass_deploy_info')
@task_manager.require_exclusive_lock
def _continue_deploy(self, task, **kwargs):
task.driver.management.ensure_next_boot_device(task.node,
boot_devices.PXE)
super(AMTPXEVendorPassthru, self)._continue_deploy(task, **kwargs)

View File

@ -22,6 +22,9 @@ from oslo_utils import importutils
from ironic.common import exception
from ironic.common.i18n import _
from ironic.drivers import base
from ironic.drivers.modules.amt import management as amt_management
from ironic.drivers.modules.amt import power as amt_power
from ironic.drivers.modules.amt import vendor as amt_vendor
from ironic.drivers.modules import iboot
from ironic.drivers.modules.ilo import deploy as ilo_deploy
from ironic.drivers.modules.ilo import management as ilo_management
@ -234,3 +237,23 @@ class PXEAndVirtualBoxDriver(base.BaseDriver):
self.deploy = pxe.PXEDeploy()
self.management = virtualbox.VirtualBoxManagement()
self.vendor = pxe.VendorPassthru()
class PXEAndAMTDriver(base.BaseDriver):
"""PXE + AMT driver.
This driver implements the `core` functionality, combining
:class:`ironic.drivers.amt.AMTPower` 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):
if not importutils.try_import('pywsman'):
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import pywsman library"))
self.power = amt_power.AMTPower()
self.deploy = pxe.PXEDeploy()
self.management = amt_management.AMTManagement()
self.vendor = amt_vendor.AMTPXEVendorPassthru()

View File

@ -0,0 +1,223 @@
#
# 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 AMT ManagementInterface
"""
import mock
from oslo_config import cfg
from ironic.common import boot_devices
from ironic.common import exception
from ironic.conductor import task_manager
from ironic.drivers.modules.amt import common as amt_common
from ironic.drivers.modules.amt import management as amt_mgmt
from ironic.drivers.modules.amt import resource_uris
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.drivers.drac import utils as test_utils
from ironic.tests.objects import utils as obj_utils
INFO_DICT = db_utils.get_test_amt_info()
CONF = cfg.CONF
@mock.patch.object(amt_common, 'pywsman')
class AMTManagementInteralMethodsTestCase(db_base.DbTestCase):
def setUp(self):
super(AMTManagementInteralMethodsTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake_amt')
self.node = obj_utils.create_test_node(self.context,
driver='fake_amt',
driver_info=INFO_DICT)
def test__set_boot_device_order(self, mock_client_pywsman):
namespace = resource_uris.CIM_BootConfigSetting
device = boot_devices.PXE
result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}],
namespace)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = mock_xml
amt_mgmt._set_boot_device_order(self.node, device)
mock_pywsman.invoke.assert_called_once_with(mock.ANY,
namespace, 'ChangeBootOrder')
def test__set_boot_device_order_fail(self, mock_client_pywsman):
namespace = resource_uris.CIM_BootConfigSetting
device = boot_devices.PXE
result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}],
namespace)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = mock_xml
self.assertRaises(exception.AMTFailure,
amt_mgmt._set_boot_device_order, self.node, device)
mock_pywsman.invoke.assert_called_once_with(mock.ANY,
namespace, 'ChangeBootOrder')
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = None
self.assertRaises(exception.AMTConnectFailure,
amt_mgmt._set_boot_device_order, self.node, device)
def test__enable_boot_config(self, mock_client_pywsman):
namespace = resource_uris.CIM_BootService
result_xml = test_utils.build_soap_xml([{'ReturnValue': '0'}],
namespace)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = mock_xml
amt_mgmt._enable_boot_config(self.node)
mock_pywsman.invoke.assert_called_once_with(mock.ANY,
namespace, 'SetBootConfigRole')
def test__enable_boot_config_fail(self, mock_client_pywsman):
namespace = resource_uris.CIM_BootService
result_xml = test_utils.build_soap_xml([{'ReturnValue': '2'}],
namespace)
mock_xml = test_utils.mock_wsman_root(result_xml)
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = mock_xml
self.assertRaises(exception.AMTFailure,
amt_mgmt._enable_boot_config, self.node)
mock_pywsman.invoke.assert_called_once_with(mock.ANY,
namespace, 'SetBootConfigRole')
mock_pywsman = mock_client_pywsman.Client.return_value
mock_pywsman.invoke.return_value = None
self.assertRaises(exception.AMTConnectFailure,
amt_mgmt._enable_boot_config, self.node)
class AMTManagementTestCase(db_base.DbTestCase):
def setUp(self):
super(AMTManagementTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake_amt')
self.info = INFO_DICT
self.node = obj_utils.create_test_node(self.context,
driver='fake_amt',
driver_info=self.info)
def test_get_properties(self):
expected = amt_common.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(amt_common, 'parse_driver_info')
def test_validate(self, mock_drvinfo):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.driver.management.validate(task)
mock_drvinfo.assert_called_once_with(task.node)
@mock.patch.object(amt_common, 'parse_driver_info')
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.management.validate,
task)
def test_get_supported_boot_devices(self):
expected = [boot_devices.PXE, boot_devices.DISK, boot_devices.CDROM]
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()))
def test_set_boot_device_one_time(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.management.set_boot_device(task, 'pxe')
self.assertEqual('pxe',
task.node.driver_internal_info["amt_boot_device"])
self.assertFalse(
task.node.driver_internal_info["amt_boot_persistent"])
def test_set_boot_device_persistent(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.management.set_boot_device(task, 'pxe',
persistent=True)
self.assertEqual('pxe',
task.node.driver_internal_info["amt_boot_device"])
self.assertTrue(
task.node.driver_internal_info["amt_boot_persistent"])
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(amt_mgmt, '_enable_boot_config')
@mock.patch.object(amt_mgmt, '_set_boot_device_order')
def test_ensure_next_boot_device_one_time(self, mock_sbdo, mock_ebc):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
device = boot_devices.PXE
task.node.driver_internal_info['amt_boot_device'] = 'pxe'
task.driver.management.ensure_next_boot_device(task.node, device)
self.assertEqual('disk',
task.node.driver_internal_info["amt_boot_device"])
self.assertTrue(
task.node.driver_internal_info["amt_boot_persistent"])
mock_sbdo.assert_called_once_with(task.node, device)
mock_ebc.assert_called_once_with(task.node)
@mock.patch.object(amt_mgmt, '_enable_boot_config')
@mock.patch.object(amt_mgmt, '_set_boot_device_order')
def test_ensure_next_boot_device_persistent(self, mock_sbdo, mock_ebc):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
device = boot_devices.PXE
task.node.driver_internal_info['amt_boot_device'] = 'pxe'
task.node.driver_internal_info['amt_boot_persistent'] = True
task.driver.management.ensure_next_boot_device(task.node, device)
self.assertEqual('pxe',
task.node.driver_internal_info["amt_boot_device"])
self.assertTrue(
task.node.driver_internal_info["amt_boot_persistent"])
mock_sbdo.assert_called_once_with(task.node, device)
mock_ebc.assert_called_once_with(task.node)
def test_get_boot_device(self):
expected = {'boot_device': boot_devices.DISK, 'persistent': True}
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected,
task.driver.management.get_boot_device(task))
def test_get_sensor_data(self):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(NotImplementedError,
task.driver.management.get_sensors_data,
task)

View File

@ -0,0 +1,260 @@
#
# 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 AMT ManagementInterface
"""
import mock
from oslo_config import cfg
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules.amt import common as amt_common
from ironic.drivers.modules.amt import power as amt_power
from ironic.drivers.modules.amt import resource_uris
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.drivers.drac import utils as test_utils
from ironic.tests.objects import utils as obj_utils
INFO_DICT = db_utils.get_test_amt_info()
CONF = cfg.CONF
class AMTPowerInteralMethodsTestCase(db_base.DbTestCase):
def setUp(self):
super(AMTPowerInteralMethodsTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake_amt')
self.info = INFO_DICT
self.node = obj_utils.create_test_node(self.context,
driver='fake_amt',
driver_info=self.info)
CONF.set_override('max_attempts', 2, 'amt')
CONF.set_override('action_wait', 0, 'amt')
@mock.patch.object(amt_common, 'get_wsman_client')
def test__set_power_state(self, mock_client_pywsman):
namespace = resource_uris.CIM_PowerManagementService
mock_client = mock_client_pywsman.return_value
amt_power._set_power_state(self.node, states.POWER_ON)
mock_client.wsman_invoke.assert_called_once_with(mock.ANY,
namespace, 'RequestPowerStateChange', mock.ANY)
@mock.patch.object(amt_common, 'get_wsman_client')
def test__set_power_state_fail(self, mock_client_pywsman):
mock_client = mock_client_pywsman.return_value
mock_client.wsman_invoke.side_effect = exception.AMTFailure('x')
self.assertRaises(exception.AMTFailure,
amt_power._set_power_state,
self.node, states.POWER_ON)
@mock.patch.object(amt_common, 'get_wsman_client')
def test__power_status(self, mock_gwc):
namespace = resource_uris.CIM_AssociatedPowerManagementService
result_xml = test_utils.build_soap_xml([{'PowerState':
'2'}],
namespace)
mock_doc = test_utils.mock_wsman_root(result_xml)
mock_client = mock_gwc.return_value
mock_client.wsman_get.return_value = mock_doc
self.assertEqual(
states.POWER_ON, amt_power._power_status(self.node))
result_xml = test_utils.build_soap_xml([{'PowerState':
'8'}],
namespace)
mock_doc = test_utils.mock_wsman_root(result_xml)
mock_client = mock_gwc.return_value
mock_client.wsman_get.return_value = mock_doc
self.assertEqual(
states.POWER_OFF, amt_power._power_status(self.node))
result_xml = test_utils.build_soap_xml([{'PowerState':
'4'}],
namespace)
mock_doc = test_utils.mock_wsman_root(result_xml)
mock_client = mock_gwc.return_value
mock_client.wsman_get.return_value = mock_doc
self.assertEqual(
states.ERROR, amt_power._power_status(self.node))
@mock.patch.object(amt_common, 'get_wsman_client')
def test__power_status_fail(self, mock_gwc):
mock_client = mock_gwc.return_value
mock_client.wsman_get.side_effect = exception.AMTFailure('x')
self.assertRaises(exception.AMTFailure,
amt_power._power_status,
self.node)
@mock.patch.object(amt_power, '_power_status')
@mock.patch.object(amt_power, '_set_power_state')
def test__set_and_wait_power_on_with_boot_device(self, mock_sps,
mock_ps):
mock_snbd = mock.Mock()
target_state = states.POWER_ON
boot_device = boot_devices.PXE
mock_ps.side_effect = [states.POWER_OFF, states.POWER_ON]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
task.node.driver_internal_info['amt_boot_device'] = boot_device
task.driver.management.ensure_next_boot_device = mock_snbd
mock_snbd.return_value = None
self.assertEqual(states.POWER_ON,
amt_power._set_and_wait(task, target_state))
mock_snbd.assert_called_with(task.node, boot_devices.PXE)
mock_sps.assert_called_once_with(task.node, states.POWER_ON)
mock_ps.assert_called_with(task.node)
@mock.patch.object(amt_power, '_power_status')
@mock.patch.object(amt_power, '_set_power_state')
def test__set_and_wait_power_on_without_boot_device(self, mock_sps,
mock_ps):
target_state = states.POWER_ON
mock_ps.side_effect = [states.POWER_OFF, states.POWER_ON]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(states.POWER_ON,
amt_power._set_and_wait(task, target_state))
mock_sps.assert_called_once_with(task.node, states.POWER_ON)
mock_ps.assert_called_with(task.node)
boot_device = boot_devices.DISK
self.node.driver_internal_info['amt_boot_device'] = boot_device
mock_ps.side_effect = [states.POWER_OFF, states.POWER_ON]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(states.POWER_ON,
amt_power._set_and_wait(task, target_state))
mock_sps.assert_called_with(task.node, states.POWER_ON)
mock_ps.assert_called_with(task.node)
def test__set_and_wait_wrong_target_state(self):
target_state = 'fake-state'
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.InvalidParameterValue,
amt_power._set_and_wait, task, target_state)
@mock.patch.object(amt_power, '_power_status')
@mock.patch.object(amt_power, '_set_power_state')
def test__set_and_wait_exceed_iterations(self, mock_sps,
mock_ps):
target_state = states.POWER_ON
mock_ps.side_effect = [states.POWER_OFF, states.POWER_OFF,
states.POWER_OFF]
mock_sps.return_value = exception.AMTFailure('x')
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertRaises(exception.PowerStateFailure,
amt_power._set_and_wait, task, target_state)
mock_sps.assert_called_with(task.node, states.POWER_ON)
mock_ps.assert_called_with(task.node)
self.assertEqual(3, mock_ps.call_count)
@mock.patch.object(amt_power, '_power_status')
def test__set_and_wait_already_target_state(self, mock_ps):
target_state = states.POWER_ON
mock_ps.side_effect = [states.POWER_ON]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(states.POWER_ON,
amt_power._set_and_wait(task, target_state))
mock_ps.assert_called_with(task.node)
@mock.patch.object(amt_power, '_power_status')
@mock.patch.object(amt_power, '_set_power_state')
def test__set_and_wait_power_off(self, mock_sps, mock_ps):
target_state = states.POWER_OFF
mock_ps.side_effect = [states.POWER_ON, states.POWER_OFF]
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(states.POWER_OFF,
amt_power._set_and_wait(task, target_state))
mock_sps.assert_called_once_with(task.node, states.POWER_OFF)
mock_ps.assert_called_with(task.node)
class AMTPowerTestCase(db_base.DbTestCase):
def setUp(self):
super(AMTPowerTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver='fake_amt')
self.info = INFO_DICT
self.node = obj_utils.create_test_node(self.context,
driver='fake_amt',
driver_info=self.info)
def test_get_properties(self):
expected = amt_common.COMMON_PROPERTIES
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
self.assertEqual(expected, task.driver.get_properties())
@mock.patch.object(amt_common, 'parse_driver_info')
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(amt_common, 'parse_driver_info')
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(amt_power, '_power_status')
def test_get_power_state(self, mock_ps):
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
mock_ps.return_value = states.POWER_ON
self.assertEqual(states.POWER_ON,
task.driver.power.get_power_state(task))
mock_ps.assert_called_once_with(task.node)
@mock.patch.object(amt_power, '_set_and_wait')
def test_set_power_state(self, mock_saw):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
pstate = states.POWER_ON
mock_saw.return_value = states.POWER_ON
task.driver.power.set_power_state(task, pstate)
mock_saw.assert_called_once_with(task, pstate)
@mock.patch.object(amt_power, '_set_and_wait')
def test_set_power_state_fail(self, mock_saw):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
pstate = states.POWER_ON
mock_saw.side_effect = exception.PowerStateFailure('x')
self.assertRaises(exception.PowerStateFailure,
task.driver.power.set_power_state,
task, pstate)
mock_saw.assert_called_once_with(task, pstate)
@mock.patch.object(amt_power, '_set_and_wait')
def test_reboot(self, mock_saw):
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.driver.power.reboot(task)
calls = [mock.call(task, states.POWER_OFF),
mock.call(task, states.POWER_ON)]
mock_saw.has_calls(calls)

View File

@ -0,0 +1,65 @@
#
# 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 AMT Vendor methods."""
import mock
from ironic.common import boot_devices
from ironic.common import states
from ironic.conductor import task_manager
from ironic.drivers.modules import pxe
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_amt_info()
class AMTPXEVendorPassthruTestCase(db_base.DbTestCase):
def setUp(self):
super(AMTPXEVendorPassthruTestCase, self).setUp()
mgr_utils.mock_the_extension_manager(driver="pxe_amt")
self.node = obj_utils.create_test_node(self.context,
driver='pxe_amt', driver_info=INFO_DICT)
def test_vendor_routes(self):
expected = ['heartbeat', 'pass_deploy_info']
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
vendor_routes = task.driver.vendor.vendor_routes
self.assertIsInstance(vendor_routes, dict)
self.assertEqual(sorted(expected), sorted(list(vendor_routes)))
def test_driver_routes(self):
expected = ['lookup']
with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task:
driver_routes = task.driver.vendor.driver_routes
self.assertIsInstance(driver_routes, dict)
self.assertEqual(sorted(expected), sorted(list(driver_routes)))
@mock.patch.object(pxe.VendorPassthru, '_continue_deploy')
def test_vendorpassthru_continue_deploy(self, mock_pxe_vendorpassthru):
mock_ensure = mock.Mock()
kwargs = {'address': '123456'}
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
task.node.provision_state = states.DEPLOYWAIT
task.node.target_provision_state = states.ACTIVE
task.driver.management.ensure_next_boot_device = mock_ensure
task.driver.vendor._continue_deploy(task, **kwargs)
mock_ensure.assert_called_with(task.node, boot_devices.PXE)
mock_pxe_vendorpassthru.assert_called_once_with(task, **kwargs)

View File

@ -97,16 +97,17 @@ if not proliantutils:
# attempt to load the external 'pywsman' library, which is required by
# the optional drivers.modules.drac module
# the optional drivers.modules.drac and drivers.modules.amt module
pywsman = importutils.try_import('pywsman')
if not pywsman:
pywsman = mock.Mock()
sys.modules['pywsman'] = pywsman
# if anything has loaded the drac driver yet, reload it now that the
# external library has been mocked
if 'ironic.drivers.modules.drac' in sys.modules:
reload(sys.modules['ironic.drivers.modules.drac'])
# Now that the external library has been mocked, if anything had already
# loaded any of the drivers, reload them.
if 'ironic.drivers.modules.drac' in sys.modules:
reload(sys.modules['ironic.drivers.modules.drac'])
if 'ironic.drivers.modules.amt' in sys.modules:
reload(sys.modules['ironic.drivers.modules.amt'])
# attempt to load the external 'iboot' library, which is required by

View File

@ -53,6 +53,7 @@ ironic.drivers =
fake_snmp = ironic.drivers.fake:FakeSNMPDriver
fake_irmc = ironic.drivers.fake:FakeIRMCDriver
fake_vbox = ironic.drivers.fake:FakeVirtualBoxDriver
fake_amt = ironic.drivers.fake:FakeAMTDriver
iscsi_ilo = ironic.drivers.ilo:IloVirtualMediaIscsiDriver
pxe_ipmitool = ironic.drivers.pxe:PXEAndIPMIToolDriver
pxe_ipminative = ironic.drivers.pxe:PXEAndIPMINativeDriver
@ -64,6 +65,7 @@ ironic.drivers =
pxe_drac = ironic.drivers.drac:PXEDracDriver
pxe_snmp = ironic.drivers.pxe:PXEAndSNMPDriver
pxe_irmc = ironic.drivers.pxe:PXEAndIRMCDriver
pxe_amt = ironic.drivers.pxe:PXEAndAMTDriver
ironic.database.migration_backend =
sqlalchemy = ironic.db.sqlalchemy.migration