Add Cinder storage driver
A cinder interface driver that can be called in order to signal cinder to attach and detach volumes. Authored-By: Julia Kreger <juliaashleykreger@gmail.com> Co-Authored-By: Joanna Taryma <joanna.taryma@intel.com> Co-Authored-By: Mike Turek <mjturek@linux.vnet.ibm.com> Change-Id: I3d6bb126ef34b30f316ed5a10fd13a3106d1400b Partial-Bug: #1559691
This commit is contained in:
parent
8a6c3c6108
commit
b23a176a73
@ -891,6 +891,16 @@
|
||||
# From ironic
|
||||
#
|
||||
|
||||
# Number of retries in the case of a failed action (only
|
||||
# specific actions are retried). This option is part of boot-
|
||||
# from-volume work, which is not currently exposed to users.
|
||||
# (integer value)
|
||||
#action_retries = 3
|
||||
|
||||
# Retry interval in seconds in the case of a failed action
|
||||
# (only specific actions are retried). (integer value)
|
||||
#action_retry_interval = 5
|
||||
|
||||
# Authentication URL (string value)
|
||||
#auth_url = <None>
|
||||
|
||||
|
@ -28,6 +28,16 @@ opts = [
|
||||
help=_('Client retries in the case of a failed request '
|
||||
'connection. This option is part of boot-from-volume '
|
||||
'work, which is not currently exposed to users.')),
|
||||
cfg.IntOpt('action_retries',
|
||||
default=3,
|
||||
help=_('Number of retries in the case of a failed '
|
||||
'action (currently only used when deatching'
|
||||
'volumes). This option is part of boot-from-volume '
|
||||
'work, which is not currently exposed to users.')),
|
||||
cfg.IntOpt('action_retry_interval',
|
||||
default=5,
|
||||
help=_('Retry interval in seconds in the case of a failed '
|
||||
'action (only specific actions are retried).')),
|
||||
]
|
||||
|
||||
|
||||
|
430
ironic/drivers/modules/storage/cinder.py
Normal file
430
ironic/drivers/modules/storage/cinder.py
Normal file
@ -0,0 +1,430 @@
|
||||
# Copyright 2016 Hewlett Packard Enterprise Development Company LP.
|
||||
# Copyright 2016 IBM Corp
|
||||
# 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 retrying
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import strutils
|
||||
|
||||
from ironic.common import cinder
|
||||
from ironic.common import exception
|
||||
from ironic.common.i18n import _
|
||||
from ironic.common import states
|
||||
from ironic.drivers import base
|
||||
from ironic.drivers import utils
|
||||
from ironic import objects
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
# NOTE(TheJulia): Sets containing known valid types that align with
|
||||
# _generate_connector() and the volume connection information spec.
|
||||
VALID_ISCSI_TYPES = ('iqn',)
|
||||
# TODO(TheJulia): FCoE?
|
||||
VALID_FC_TYPES = ('wwpn', 'wwnn')
|
||||
|
||||
|
||||
class CinderStorage(base.StorageInterface):
|
||||
"""A storage_interface driver supporting Cinder."""
|
||||
|
||||
def get_properties(self):
|
||||
"""Return the properties of the interface.
|
||||
|
||||
:returns: dictionary of <property name>:<property description> entries.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def _fail_validation(self, task, reason,
|
||||
exception=exception.InvalidParameterValue):
|
||||
msg = (_("Failed to validate cinder storage interface for node "
|
||||
"%(node)s. %(reason)s") %
|
||||
{'node': task.node.uuid, 'reason': reason})
|
||||
LOG.error(msg)
|
||||
raise exception(msg)
|
||||
|
||||
def _validate_connectors(self, task):
|
||||
"""Validate connector information helper.
|
||||
|
||||
Enumerates through all connector objects, and identifies if
|
||||
iSCSI or Fibre Channel connectors are present.
|
||||
|
||||
:param task: The task object.
|
||||
:raises InvalidParameterValue: If iSCSI is identified and
|
||||
iPXE is disabled.
|
||||
:raises StorageError: If the number of wwpns is not equal to
|
||||
the number of wwnns
|
||||
:returns: Dictionary containing iscsi_found and fc_found
|
||||
keys with boolean values representing if the
|
||||
helper found that connector type configured
|
||||
for the node.
|
||||
"""
|
||||
|
||||
node = task.node
|
||||
iscsi_uuids_found = []
|
||||
wwpn_found = 0
|
||||
wwnn_found = 0
|
||||
ipxe_enabled = CONF.pxe.ipxe_enabled
|
||||
|
||||
for connector in task.volume_connectors:
|
||||
if (connector.type in VALID_ISCSI_TYPES and
|
||||
connector.connector_id is not None):
|
||||
iscsi_uuids_found.append(connector.uuid)
|
||||
if not ipxe_enabled:
|
||||
msg = _("The [pxe]/ipxe_enabled option must "
|
||||
"be set to True to support network "
|
||||
"booting to an iSCSI volume.")
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
if (connector.type in VALID_FC_TYPES and
|
||||
connector.connector_id is not None):
|
||||
# NOTE(TheJulia): Unlike iSCSI with cinder, we have no need
|
||||
# to warn about multiple IQN entries, since we are able to
|
||||
# submit multiple fibre channel WWPN entries.
|
||||
if connector.type == 'wwpn':
|
||||
wwpn_found += 1
|
||||
if connector.type == 'wwnn':
|
||||
wwnn_found += 1
|
||||
if len(iscsi_uuids_found) > 1:
|
||||
LOG.warning("Multiple possible iSCSI connectors, "
|
||||
"%(iscsi_uuids_found) found, for node %(node)s. "
|
||||
"Only the first iSCSI connector, %(iscsi_uuid)s, "
|
||||
"will be utilized.",
|
||||
{'node': node.uuid,
|
||||
'iscsi_uuids_found': iscsi_uuids_found,
|
||||
'iscsi_uuid': iscsi_uuids_found[0]})
|
||||
if wwpn_found != wwnn_found:
|
||||
msg = _("Cinder requires both wwnn and wwpn entries for FCoE "
|
||||
"connections. There must be a wwpn entry for every wwnn "
|
||||
"entry. There are %(wwpn)d wwpn entries and %(wwnn)s wwnn "
|
||||
"entries.") % {'wwpn': wwpn_found, 'wwnn': wwnn_found}
|
||||
self._fail_validation(task, msg, exception.StorageError)
|
||||
return {'fc_found': wwpn_found >= 1,
|
||||
'iscsi_found': len(iscsi_uuids_found) >= 1}
|
||||
|
||||
def _validate_targets(self, task, found_types, iscsi_boot, fc_boot):
|
||||
"""Validate target information helper.
|
||||
|
||||
Enumerates through all target objects and identifies if
|
||||
iSCSI or Fibre Channel targets are present, and matches the
|
||||
connector capability of the node.
|
||||
|
||||
:param task: The task object.
|
||||
:param found_types: Dictionary containing boolean values returned
|
||||
from the _validate_connectors helper method.
|
||||
:param iscsi_boot: Boolean value indicating if iSCSI boot operations
|
||||
are available.
|
||||
:param fc_boot: Boolean value indicating if Fibre Channel boot
|
||||
operations are available.
|
||||
:raises: InvalidParameterValue
|
||||
"""
|
||||
|
||||
for volume in task.volume_targets:
|
||||
if volume.volume_id is None:
|
||||
msg = (_("volume_id missing from target %(id)s.") %
|
||||
{'id': volume.uuid})
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
# NOTE(TheJulia): We should likely consider incorporation
|
||||
# of the volume boot_index field, however it may not be
|
||||
# relevant to the checks we perform here as in the end a
|
||||
# FC volume "attached" to a node is a valid configuration
|
||||
# as well.
|
||||
# TODO(TheJulia): When we create support in nova to record
|
||||
# that a volume attachment is going to take place, we will
|
||||
# likely need to match the driver_volume_type field to
|
||||
# our generic volume_type field. NB The LVM driver appears
|
||||
# to not use that convention in cinder, as it is freeform.
|
||||
if volume.volume_type == 'fibre_channel':
|
||||
if not fc_boot and volume.boot_index == 0:
|
||||
msg = (_("Volume target %(id)s is configured for "
|
||||
"'fibre_channel', however the capability "
|
||||
"'fibre_channel_boot' is not set on node.") %
|
||||
{'id': volume.uuid})
|
||||
self._fail_validation(task, msg)
|
||||
if not found_types['fc_found']:
|
||||
msg = (_("Volume target %(id)s is configured for "
|
||||
"'fibre_channel', however no Fibre Channel "
|
||||
"WWPNs are configured for the node volume "
|
||||
"connectors.") %
|
||||
{'id': volume.uuid})
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
elif volume.volume_type == 'iscsi':
|
||||
if not iscsi_boot and volume.boot_index == 0:
|
||||
msg = (_("Volume target %(id)s is configured for "
|
||||
"'iscsi', however the capability 'iscsi_boot' "
|
||||
"is not set for the node.") %
|
||||
{'id': volume.uuid})
|
||||
self._fail_validation(task, msg)
|
||||
if not found_types['iscsi_found']:
|
||||
msg = (_("Volume target %(id)s is configured for "
|
||||
"'iscsi', however no iSCSI connectors are "
|
||||
"configured for the node.") %
|
||||
{'id': volume.uuid})
|
||||
self._fail_validation(task, msg)
|
||||
else:
|
||||
# NOTE(TheJulia); The note below needs to be updated
|
||||
# whenever support for additional volume types are added.
|
||||
msg = (_("Volume target %(id)s is of an unknown type "
|
||||
"'%(type)s'. Supported types: 'iscsi' or "
|
||||
"'fibre_channel'") %
|
||||
{'id': volume.uuid, 'type': volume.volume_type})
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
def validate(self, task):
|
||||
"""Validate storage_interface configuration for Cinder usage.
|
||||
|
||||
In order to provide fail fast functionality prior to nodes being
|
||||
requested to enter the active state, this method performs basic
|
||||
checks of the volume connectors, volume targets, and operator
|
||||
defined capabilities. These checks are to help ensure that we
|
||||
should have a compatible configuration prior to activating the
|
||||
node.
|
||||
|
||||
:param task: The task object.
|
||||
:raises: InvalidParameterValue If a misconfiguration or mismatch
|
||||
exists that would prevent storage the cinder storage
|
||||
driver from initializing attachments.
|
||||
"""
|
||||
|
||||
found_types = self._validate_connectors(task)
|
||||
node = task.node
|
||||
iscsi_boot = strutils.bool_from_string(
|
||||
utils.get_node_capability(node, 'iscsi_boot'))
|
||||
fc_boot = strutils.bool_from_string(
|
||||
utils.get_node_capability(node, 'fibre_channel_boot'))
|
||||
|
||||
# Validate capability configuration against configured volumes
|
||||
# such that we raise errors for missing connectors if the
|
||||
# boot capability is defined.
|
||||
if iscsi_boot and not found_types['iscsi_found']:
|
||||
valid_types = ', '.join(VALID_ISCSI_TYPES)
|
||||
msg = (_("In order to enable the 'iscsi_boot' capability for "
|
||||
"the node, an associated volume_connector type "
|
||||
"must be valid for iSCSI (%(options)s).") %
|
||||
{'options': valid_types})
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
if fc_boot and not found_types['fc_found']:
|
||||
valid_types = ', '.join(VALID_FC_TYPES)
|
||||
msg = (_("In order to enable the 'fibre_channel_boot' capability "
|
||||
"for the node, an associated volume_connector type must "
|
||||
"be valid for Fibre Channel (%(options)s).") %
|
||||
{'options': valid_types})
|
||||
self._fail_validation(task, msg)
|
||||
|
||||
self._validate_targets(task, found_types, iscsi_boot, fc_boot)
|
||||
|
||||
def _abort_attach_volume(self, task, connector):
|
||||
"""Detach volumes as a result of failed attachment.
|
||||
|
||||
If detachment fails, it will be retried with allow_errors=True.
|
||||
|
||||
:param task: The task object.
|
||||
:param connector: The dictionary representing a node's connectivity
|
||||
as define by _generate_connector.
|
||||
"""
|
||||
targets = [target.volume_id for target in task.volume_targets]
|
||||
try:
|
||||
cinder.detach_volumes(task, targets, connector)
|
||||
except exception.StorageError as e:
|
||||
LOG.warning("Error detaching volume for node %(node)s on "
|
||||
"aborting volume attach: %(err)s. Retrying with "
|
||||
"errors allowed.", {'node': task.node.uuid, 'err': e})
|
||||
cinder.detach_volumes(task, targets, connector, allow_errors=True)
|
||||
|
||||
def attach_volumes(self, task):
|
||||
"""Informs the storage subsystem to attach all volumes for the node.
|
||||
|
||||
:param task: The task object.
|
||||
:raises: StorageError If an underlying exception or failure
|
||||
is detected.
|
||||
"""
|
||||
node = task.node
|
||||
targets = [target.volume_id for target in task.volume_targets]
|
||||
|
||||
# If there are no targets, then we have nothing to do.
|
||||
if not targets:
|
||||
return
|
||||
|
||||
connector = self._generate_connector(task)
|
||||
try:
|
||||
connected = cinder.attach_volumes(task, targets, connector)
|
||||
except exception.StorageError as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error("Error attaching volumes for node %(node)s: "
|
||||
"%(err)s", {'node': node.uuid, 'err': e})
|
||||
self._abort_attach_volume(task, connector)
|
||||
|
||||
if len(targets) != len(connected):
|
||||
LOG.error("The number of volumes defined for node %(node)s does "
|
||||
"not match the number of attached volumes. Attempting "
|
||||
"detach and abort operation.", {'node': node.uuid})
|
||||
self._abort_attach_volume(task, connector)
|
||||
raise exception.StorageError(("Mismatch between the number of "
|
||||
"configured volume targets for "
|
||||
"node %(uuid)s and the number of "
|
||||
"completed attachments.") %
|
||||
{'uuid': node.uuid})
|
||||
|
||||
for volume in connected:
|
||||
volume_uuid = volume['data']['ironic_volume_uuid']
|
||||
targets = objects.VolumeTarget.list_by_volume_id(task.context,
|
||||
volume_uuid)
|
||||
|
||||
for target in targets:
|
||||
# Volumes that were already attached are
|
||||
# skipped. Updating target volume properties
|
||||
# for these volumes is nova's responsibility.
|
||||
if not volume.get('already_attached'):
|
||||
target.properties = volume['data']
|
||||
target.save()
|
||||
|
||||
def detach_volumes(self, task):
|
||||
"""Informs the storage subsystem to detach all volumes for the node.
|
||||
|
||||
This action is retried in case of failure.
|
||||
|
||||
:param task: The task object.
|
||||
:raises: StorageError If an underlying exception or failure
|
||||
is detected.
|
||||
"""
|
||||
# TODO(TheJulia): Ideally we should query the cinder API and reconcile
|
||||
# or add any missing volumes and initiate detachments.
|
||||
node = task.node
|
||||
targets = [target.volume_id for target in task.volume_targets]
|
||||
|
||||
# If there are no targets, then we have nothing to do.
|
||||
if not targets:
|
||||
return
|
||||
|
||||
connector = self._generate_connector(task)
|
||||
|
||||
@retrying.retry(
|
||||
retry_on_exception=lambda e: isinstance(e, exception.StorageError),
|
||||
stop_max_attempt_number=CONF.cinder.action_retries + 1,
|
||||
wait_fixed=CONF.cinder.action_retry_interval * 1000)
|
||||
def detach_volumes():
|
||||
try:
|
||||
# NOTE(TheJulia): If the node is in ACTIVE state, we can
|
||||
# tolerate failures detaching as the node is likely being
|
||||
# powered down to cause a detachment event.
|
||||
allow_errors = (task.node.provision_state == states.ACTIVE)
|
||||
cinder.detach_volumes(task, targets, connector,
|
||||
allow_errors=allow_errors)
|
||||
except exception.StorageError as e:
|
||||
# NOTE(TheJulia): In the event that the node is not in
|
||||
# ACTIVE state, we need to fail hard as we need to ensure
|
||||
# all attachments are removed.
|
||||
msg = ("Error detaching volumes for "
|
||||
"node %(node)s: %(err)s.") % {'node': node.uuid,
|
||||
'err': e}
|
||||
if outer_args['attempt'] < CONF.cinder.action_retries:
|
||||
outer_args['attempt'] += 1
|
||||
msg += " Re-attempting detachment."
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
LOG.error(msg)
|
||||
raise
|
||||
|
||||
# NOTE(mjturek): This dict is used by detach_volumes to determine
|
||||
# if this is the last attempt. This is a dict rather than an int
|
||||
# so that it is mutable by the inner function. In python3 this is
|
||||
# possible with the 'nonlocal' keyword which is unfortunately not
|
||||
# available in python2.
|
||||
outer_args = {'attempt': 0}
|
||||
detach_volumes()
|
||||
|
||||
def should_write_image(self, task):
|
||||
"""Determines if deploy should perform the image write-out.
|
||||
|
||||
NOTE: This method will be written as part of the changes to the
|
||||
boot logic, and for now should always return false to indicate
|
||||
that the deployment image write-out process should be skipped.
|
||||
|
||||
:param task: The task object.
|
||||
:returns: True if the deployment write-out process should be
|
||||
executed.
|
||||
"""
|
||||
return False
|
||||
|
||||
def _generate_connector(self, task):
|
||||
"""Generate cinder connector value based upon the node.
|
||||
|
||||
Generates cinder compatible connector information for the purpose of
|
||||
attaching volumes. Translation: We need to tell the storage where and
|
||||
possibly how we can connect.
|
||||
|
||||
Supports passing iSCSI information in the form of IP and IQN records,
|
||||
as well as Fibre Channel information in the form of WWPN addresses.
|
||||
Fibre Channel WWNN addresses are also sent, however at present in-tree
|
||||
Cinder drivers do not utilize WWNN addresses.
|
||||
|
||||
If multiple connectors exist, the request will be filed with
|
||||
MultiPath IO being enabled.
|
||||
|
||||
A warning is logged if an unsupported volume type is encountered.
|
||||
|
||||
:params task: The task object.
|
||||
|
||||
:returns: A dictionary data structure similar to:
|
||||
{'ip': ip,
|
||||
'initiator': iqn,
|
||||
'multipath: True,
|
||||
'wwpns': ['WWN1', 'WWN2']}
|
||||
:raises: StorageError upon no valid connector record being identified.
|
||||
"""
|
||||
data = {}
|
||||
valid = False
|
||||
for connector in task.volume_connectors:
|
||||
if 'iqn' in connector.type and 'initiator' not in data:
|
||||
data['initiator'] = connector.connector_id
|
||||
valid = True
|
||||
elif 'ip' in connector.type and 'ip' not in data:
|
||||
data['ip'] = connector.connector_id
|
||||
# TODO(TheJulia): Translate to, or generate an IQN.
|
||||
elif 'wwpn' in connector.type:
|
||||
data.setdefault('wwpns', []).append(connector.connector_id)
|
||||
valid = True
|
||||
elif 'wwnn' in connector.type:
|
||||
data.setdefault('wwnns', []).append(connector.connector_id)
|
||||
valid = True
|
||||
else:
|
||||
# TODO(jtaryma): Add handling of type 'mac' with MAC to IP
|
||||
# translation.
|
||||
LOG.warning('Node %(node)s has a volume_connector (%(uuid)s) '
|
||||
'defined with an unsupported type: %(type)s.',
|
||||
{'node': task.node.uuid,
|
||||
'uuid': connector.uuid,
|
||||
'type': connector.type})
|
||||
if not valid:
|
||||
valid_types = ', '.join(VALID_FC_TYPES + VALID_ISCSI_TYPES)
|
||||
msg = (_('Insufficient or incompatible volume connection '
|
||||
'records for node %(uuid)s. Valid connector '
|
||||
'types: %(types)s') %
|
||||
{'uuid': task.node.uuid, 'types': valid_types})
|
||||
LOG.error(msg)
|
||||
raise exception.StorageError(msg)
|
||||
|
||||
# NOTE(TheJulia): Hostname appears to only be used for logging
|
||||
# in cinder drivers, however that may not always be true, and
|
||||
# may need to change over time.
|
||||
data['host'] = task.node.uuid
|
||||
if len(task.volume_connectors) > 1 and len(data) > 1:
|
||||
data['multipath'] = True
|
||||
|
||||
return data
|
592
ironic/tests/unit/drivers/modules/storage/test_cinder.py
Normal file
592
ironic/tests/unit/drivers/modules/storage/test_cinder.py
Normal file
@ -0,0 +1,592 @@
|
||||
# Copyright 2016 Hewlett Packard Enterprise Development Company LP.
|
||||
# Copyright 2016 IBM Corp
|
||||
# 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 mock
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from ironic.common import cinder as cinder_common
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic.conductor import task_manager
|
||||
from ironic.drivers.modules.storage import cinder
|
||||
from ironic.drivers import utils as driver_utils
|
||||
from ironic import objects
|
||||
from ironic.tests.unit.conductor import mgr_utils
|
||||
from ironic.tests.unit.db import base as db_base
|
||||
from ironic.tests.unit.objects import utils as object_utils
|
||||
|
||||
|
||||
class CinderInterfaceTestCase(db_base.DbTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CinderInterfaceTestCase, self).setUp()
|
||||
self.config(ipxe_enabled=True,
|
||||
group='pxe')
|
||||
self.config(action_retries=3,
|
||||
action_retry_interval=0,
|
||||
group='cinder')
|
||||
self.config(enabled_drivers=['fake'])
|
||||
self.config(enabled_storage_interfaces=['noop', 'cinder'])
|
||||
mgr_utils.mock_the_extension_manager()
|
||||
self.interface = cinder.CinderStorage()
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test__fail_validation(self, mock_log):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
"""Ensure the validate helper logs and raises exceptions."""
|
||||
fake_error = 'a error!'
|
||||
expected = ("Failed to validate cinder storage interface for node "
|
||||
"%s. a error!" % self.node.uuid)
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface._fail_validation,
|
||||
task,
|
||||
fake_error)
|
||||
mock_log.error.assert_called_with(expected)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test__generate_connector_raises_with_insufficent_data(self, mock_log):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface._generate_connector,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
def test__generate_connector_iscsi(self):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
expected = {
|
||||
'initiator': 'iqn.address',
|
||||
'ip': 'ip.address',
|
||||
'host': self.node.uuid,
|
||||
'multipath': True}
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='ip',
|
||||
connector_id='ip.address', uuid=uuidutils.generate_uuid())
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
return_value = self.interface._generate_connector(task)
|
||||
self.assertDictEqual(expected, return_value)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test__generate_connector_iscsi_and_unknown(self, mock_log):
|
||||
"""Validate we return and log with valid and invalid connectors."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
expected = {
|
||||
'initiator': 'iqn.address',
|
||||
'host': self.node.uuid,
|
||||
'multipath': True}
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='foo',
|
||||
connector_id='bar', uuid=uuidutils.generate_uuid())
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
return_value = self.interface._generate_connector(task)
|
||||
self.assertDictEqual(expected, return_value)
|
||||
self.assertEqual(1, mock_log.warning.call_count)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test__generate_connector_unknown_raises_excption(self, mock_log):
|
||||
"""Validate an exception is raised with only an invalid connector."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='foo',
|
||||
connector_id='bar')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(
|
||||
exception.StorageError,
|
||||
self.interface._generate_connector,
|
||||
task)
|
||||
self.assertEqual(1, mock_log.warning.call_count)
|
||||
self.assertEqual(1, mock_log.error.call_count)
|
||||
|
||||
def test__generate_connector_single_path(self):
|
||||
"""Validate an exception is raised with only an invalid connector."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
expected = {
|
||||
'initiator': 'iqn.address',
|
||||
'host': self.node.uuid}
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
return_value = self.interface._generate_connector(task)
|
||||
self.assertDictEqual(expected, return_value)
|
||||
|
||||
def test__generate_connector_multiple_fc_wwns(self):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
"""Validate handling of WWPNs and WWNNs."""
|
||||
expected = {
|
||||
'wwpns': ['wwpn1', 'wwpn2'],
|
||||
'wwnns': ['wwnn3', 'wwnn4'],
|
||||
'host': self.node.uuid,
|
||||
'multipath': True}
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context,
|
||||
node_id=self.node.id,
|
||||
type='wwpn',
|
||||
connector_id='wwpn1',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context,
|
||||
node_id=self.node.id,
|
||||
type='wwpn',
|
||||
connector_id='wwpn2',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context,
|
||||
node_id=self.node.id,
|
||||
type='wwnn',
|
||||
connector_id='wwnn3',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context,
|
||||
node_id=self.node.id,
|
||||
type='wwnn',
|
||||
connector_id='wwnn4',
|
||||
uuid=uuidutils.generate_uuid())
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
return_value = self.interface._generate_connector(task)
|
||||
self.assertDictEqual(expected, return_value)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_no_settings(self, mock_log, mock_fail):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_fail.called)
|
||||
self.assertFalse(mock_log.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_failure_if_iscsi_boot_no_connectors(self, mock_log):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
|
||||
valid_types = ', '.join(cinder.VALID_ISCSI_TYPES)
|
||||
expected_msg = ("Failed to validate cinder storage interface for node "
|
||||
"%(id)s. In order to enable the 'iscsi_boot' "
|
||||
"capability for the node, an associated "
|
||||
"volume_connector type must be valid for "
|
||||
"iSCSI (%(options)s)." %
|
||||
{'id': self.node.uuid, 'options': valid_types})
|
||||
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
mock_log.error.assert_called_once_with(expected_msg)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_failure_if_fc_boot_no_connectors(self, mock_log):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
valid_types = ', '.join(cinder.VALID_FC_TYPES)
|
||||
expected_msg = ("Failed to validate cinder storage interface for node "
|
||||
"%(id)s. In order to enable the 'fibre_channel_boot' "
|
||||
"capability for the node, an associated "
|
||||
"volume_connector type must be valid for "
|
||||
"Fibre Channel (%(options)s)." %
|
||||
{'id': self.node.uuid, 'options': valid_types})
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task,
|
||||
'fibre_channel_boot',
|
||||
'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
mock_log.error.assert_called_once_with(expected_msg)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_iscsi_connector(self, mock_log, mock_fail):
|
||||
"""Perform validate with only an iSCSI connector in place."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertFalse(mock_fail.called)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_fc_connectors(self, mock_log, mock_fail):
|
||||
"""Perform validate with only FC connectors in place"""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwpn',
|
||||
connector_id='wwpn.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwnn',
|
||||
connector_id='wwnn.address', uuid=uuidutils.generate_uuid())
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertFalse(mock_fail.called)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_connectors_and_boot(self, mock_log, mock_fail):
|
||||
"""Perform validate with volume connectors and boot capabilities."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwpn',
|
||||
connector_id='wwpn.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwnn',
|
||||
connector_id='wwnn.address', uuid=uuidutils.generate_uuid())
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task,
|
||||
'fibre_channel_boot',
|
||||
'True')
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertFalse(mock_fail.called)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_iscsi_targets(self, mock_log, mock_fail):
|
||||
"""Validate success with full iscsi scenario."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertFalse(mock_fail.called)
|
||||
|
||||
@mock.patch.object(cinder.CinderStorage, '_fail_validation', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_success_fc_targets(self, mock_log, mock_fail):
|
||||
"""Validate success with full fc scenario."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwpn',
|
||||
connector_id='fc.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwnn',
|
||||
connector_id='fc.address', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='fibre_channel',
|
||||
boot_index=0, volume_id='1234')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task,
|
||||
'fibre_channel_boot',
|
||||
'True')
|
||||
self.interface.validate(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertFalse(mock_fail.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_fails_with_ipxe_not_enabled(self, mock_log):
|
||||
"""Ensure a validation failure is raised when iPXE not enabled."""
|
||||
self.config(ipxe_enabled=False, group='pxe')
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='foo.address')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='2345')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_fails_when_fc_connectors_unequal(self, mock_log):
|
||||
"""Validate should fail with only wwnn FC connector in place"""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='wwnn',
|
||||
connector_id='wwnn.address')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.validate,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_fail_on_unknown_volume_types(self, mock_log):
|
||||
"""Ensure exception is raised when connector/target do not match."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='foo.address')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='wetcat',
|
||||
boot_index=0, volume_id='1234')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_fails_iscsi_conn_fc_target(self, mock_log):
|
||||
"""Validate failure of iSCSI connectors with FC target."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='foo.address')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='fibre_channel',
|
||||
boot_index=0, volume_id='1234')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task, 'iscsi_boot', 'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_validate_fails_fc_conn_iscsi_target(self, mock_log):
|
||||
"""Validate failure of FC connectors with iSCSI target."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='fibre_channel',
|
||||
connector_id='foo.address')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
driver_utils.add_node_capability(task,
|
||||
'fibre_channel_boot',
|
||||
'True')
|
||||
self.assertRaises(exception.InvalidParameterValue,
|
||||
self.interface.validate,
|
||||
task)
|
||||
self.assertTrue(mock_log.error.called)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder_common, 'attach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG')
|
||||
def test_attach_detach_volumes_no_volumes(self, mock_log,
|
||||
mock_attach, mock_detach):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.interface.attach_volumes(task)
|
||||
self.interface.detach_volumes(task)
|
||||
|
||||
self.assertFalse(mock_attach.called)
|
||||
self.assertFalse(mock_detach.called)
|
||||
self.assertFalse(mock_log.called)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder_common, 'attach_volumes', autospec=True)
|
||||
def test_attach_detach_volumes_fails_without_connectors(self,
|
||||
mock_attach,
|
||||
mock_detach):
|
||||
"""Without connectors, attach and detach should fail."""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.attach_volumes, task)
|
||||
self.assertFalse(mock_attach.called)
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.detach_volumes, task)
|
||||
self.assertFalse(mock_detach.called)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder_common, 'attach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
@mock.patch.object(objects.volume_target.VolumeTarget, 'list_by_volume_id')
|
||||
def test_attach_detach_called_with_target_and_connector(self,
|
||||
mock_target_list,
|
||||
mock_log,
|
||||
mock_attach,
|
||||
mock_detach):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
target_uuid = uuidutils.generate_uuid()
|
||||
test_volume_target = object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234', uuid=target_uuid)
|
||||
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
expected_target_properties = {
|
||||
'volume_id': '1234',
|
||||
'ironic_volume_uuid': target_uuid,
|
||||
'new_property': 'foo'}
|
||||
mock_attach.return_value = [{
|
||||
'driver_volume_type': 'iscsi',
|
||||
'data': expected_target_properties}]
|
||||
mock_target_list.return_value = [test_volume_target]
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.interface.attach_volumes(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertTrue(mock_attach.called)
|
||||
task.volume_targets[0].refresh()
|
||||
self.assertEqual(expected_target_properties,
|
||||
task.volume_targets[0]['properties'])
|
||||
self.interface.detach_volumes(task)
|
||||
self.assertFalse(mock_log.called)
|
||||
self.assertTrue(mock_detach.called)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder_common, 'attach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_attach_volumes_failure(self, mock_log, mock_attach, mock_detach):
|
||||
"""Verify detach is called upon attachment failing."""
|
||||
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=1, volume_id='5678', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
|
||||
mock_attach.side_effect = exception.StorageError('foo')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.attach_volumes, task)
|
||||
self.assertTrue(mock_attach.called)
|
||||
self.assertTrue(mock_detach.called)
|
||||
# Replacing the mock to not return an error, should still raise an
|
||||
# exception.
|
||||
mock_attach.reset_mock()
|
||||
mock_detach.reset_mock()
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder_common, 'attach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_attach_volumes_failure_no_attach_error(self, mock_log,
|
||||
mock_attach, mock_detach):
|
||||
"""Verify that detach is called on volume/connector mismatch.
|
||||
|
||||
Volume attachment fails if the number of attachments completed
|
||||
does not match the number of configured targets.
|
||||
"""
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=1, volume_id='5678', uuid=uuidutils.generate_uuid())
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
|
||||
mock_attach.return_value = {'mock_return'}
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.attach_volumes, task)
|
||||
self.assertTrue(mock_attach.called)
|
||||
self.assertTrue(mock_detach.called)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG', autospec=True)
|
||||
def test_detach_volumes_failure(self, mock_log, mock_detach):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
# The first attempt should succeed.
|
||||
# The second attempt should throw StorageError
|
||||
# Third attempt, should log errors but not raise an exception.
|
||||
mock_detach.side_effect = [None,
|
||||
exception.StorageError('bar'),
|
||||
None]
|
||||
# This should generate 1 mock_detach call and succeed
|
||||
self.interface.detach_volumes(task)
|
||||
|
||||
task.node.provision_state = states.DELETED
|
||||
# This should generate the other 2 moc_detach calls and warn
|
||||
self.interface.detach_volumes(task)
|
||||
self.assertEqual(3, mock_detach.call_count)
|
||||
self.assertEqual(1, mock_log.warning.call_count)
|
||||
|
||||
@mock.patch.object(cinder_common, 'detach_volumes', autospec=True)
|
||||
@mock.patch.object(cinder, 'LOG')
|
||||
def test_detach_volumes_failure_raises_exception(self,
|
||||
mock_log,
|
||||
mock_detach):
|
||||
self.node = object_utils.create_test_node(self.context,
|
||||
storage_interface='cinder')
|
||||
object_utils.create_test_volume_target(
|
||||
self.context, node_id=self.node.id, volume_type='iscsi',
|
||||
boot_index=0, volume_id='1234')
|
||||
object_utils.create_test_volume_connector(
|
||||
self.context, node_id=self.node.id, type='iqn',
|
||||
connector_id='iqn.address')
|
||||
|
||||
with task_manager.acquire(self.context, self.node.id) as task:
|
||||
mock_detach.side_effect = exception.StorageError('bar')
|
||||
self.assertRaises(exception.StorageError,
|
||||
self.interface.detach_volumes,
|
||||
task)
|
||||
# Check that we warn every retry except the last one.
|
||||
self.assertEqual(3, mock_log.warning.call_count)
|
||||
self.assertEqual(1, mock_log.error.call_count)
|
||||
# CONF.cinder.action_retries + 1, number of retries is set to 3.
|
||||
self.assertEqual(4, mock_detach.call_count)
|
@ -138,6 +138,7 @@ ironic.hardware.interfaces.rescue =
|
||||
ironic.hardware.interfaces.storage =
|
||||
fake = ironic.drivers.modules.fake:FakeStorage
|
||||
noop = ironic.drivers.modules.storage.noop:NoopStorage
|
||||
cinder = ironic.drivers.modules.storage.cinder:CinderStorage
|
||||
|
||||
ironic.hardware.interfaces.vendor =
|
||||
fake = ironic.drivers.modules.fake:FakeVendorB
|
||||
|
Loading…
Reference in New Issue
Block a user