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:
Michael Turek 2017-04-19 13:26:22 -04:00 committed by Dmitry Tantsur
parent 8a6c3c6108
commit b23a176a73
6 changed files with 1043 additions and 0 deletions

View File

@ -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>

View File

@ -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).')),
]

View 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

View 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)

View File

@ -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