Exemple #1
0
    def test_attach_volumes_failure(self, mock_log, mock_attach, mock_detach):
        """Verify detach is called upon attachment failing."""
        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()
Exemple #2
0
    def test_detach_volumes_failure(self, mock_log, mock_detach):
        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)
Exemple #3
0
def populate_storage_driver_internal_info(task):
    """Set node driver_internal_info for boot from volume parameters.

    :param task: a TaskManager object containing the node.
    :raises: StorageError when a node has an iSCSI or FibreChannel boot volume
             defined but is not capable to support it.
    """
    node = task.node
    boot_volume = get_remote_boot_volume(task)
    if not boot_volume:
        return
    vol_type = str(boot_volume.volume_type).lower()
    node_caps = driver_utils.capabilities_to_dict(
        node.properties.get('capabilities'))
    if vol_type == 'iscsi' and 'iscsi_boot' not in node_caps:
        # TODO(TheJulia): In order to support the FCoE and HBA boot cases,
        # some additional logic will be needed here to ensure we align.
        # The deployment, in theory, should never reach this point
        # if the interfaces all validated, but we shouldn't use that
        # as the only guard against bad configurations.
        raise exception.StorageError(
            _('Node %(node)s has an iSCSI boot '
              'volume defined and no iSCSI boot '
              'support available.') % {'node': node.uuid})
    if vol_type == 'fibre_channel' and 'fibre_channel_boot' not in node_caps:
        raise exception.StorageError(
            _('Node %(node)s has a Fibre Channel '
              'boot volume defined and no Fibre '
              'Channel boot support available.') % {'node': node.uuid})
    boot_capability = ("%s_volume_boot" % vol_type)
    deploy_capability = ("%s_volume_deploy" % vol_type)
    vol_uuid = boot_volume['uuid']
    driver_internal_info = node.driver_internal_info
    if check_interface_capability(task.driver.boot, boot_capability):
        driver_internal_info['boot_from_volume'] = vol_uuid
    # NOTE(TheJulia): This would be a convenient place to check
    # if we need to know about deploying the volume.
    if (check_interface_capability(task.driver.deploy, deploy_capability)
            and task.driver.storage.should_write_image(task)):
        driver_internal_info['boot_from_volume_deploy'] = vol_uuid
        # NOTE(TheJulia): This is also a useful place to include a
        # root device hint since we should/might/be able to obtain
        # and supply that information to IPA if it needs to write
        # the image to the volume.
    node.driver_internal_info = driver_internal_info
    node.save()
Exemple #4
0
    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.detach_volumes(task,
                                    connector=connector,
                                    aborting_attach=True)

        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.detach_volumes(task,
                                connector=connector,
                                aborting_attach=True)
            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:
            # Volumes that were already attached are
            # skipped. Updating target volume properties
            # for these volumes is nova's responsibility.
            if not volume.get('already_attached'):
                volume_uuid = volume['data']['ironic_volume_uuid']
                targets = objects.VolumeTarget.list_by_volume_id(
                    task.context, volume_uuid)

                for target in targets:
                    target.properties = volume['data']
                    target.save()
Exemple #5
0
def _init_client(task):
    """Obtain cinder client and return it for use.

    :param task: TaskManager instance representing the operation.

    :returns: A cinder client.
    :raises: StorageError If an exception is encountered creating the client.
    """
    node = task.node
    try:
        return get_client()
    except Exception as e:
        msg = (_('Failed to initialize cinder client for operations on node '
                 '%(uuid)s: %(err)s') % {'uuid': node.uuid, 'err': e})
        LOG.error(msg)
        raise exception.StorageError(msg)
Exemple #6
0
    def test_detach_volumes_failure_raises_exception(self,
                                                     mock_log,
                                                     mock_detach):
        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)
Exemple #7
0
 def _handle_errors(msg):
     if allow_errors:
         LOG.warning(msg)
     else:
         LOG.error(msg)
         raise exception.StorageError(msg)
Exemple #8
0
def attach_volumes(task, volume_list, connector):
    """Attach volumes to a node.

       Enumerate through the provided list of volumes and attach the volumes
       to the node defined in the task utilizing the provided connector
       information.

       If an attachment appears to already exist, we will skip attempting to
       attach the volume. If use of the volume fails, a user may need to
       remove any lingering pre-existing/unused attachment records since
       we have no way to validate if the connector profile data differs
       from what was provided to cinder.

       :param task: TaskManager instance representing the operation.
       :param volume_list: List of volume_id UUID values representing volumes.
       :param connector: Dictionary object representing the node sufficiently
                         to attach a volume. This value can vary based upon
                         the node's configuration, capability, and ultimately
                         the back-end storage driver. As cinder was designed
                         around iSCSI, the 'ip' and 'initiator' keys are
                         generally expected by cinder drivers.
                         For FiberChannel, the key 'wwpns' can be used
                         with a list of port addresses.
                         Some drivers support a 'multipath' boolean key,
                         although it is generally False. The 'host' key
                         is generally used for logging by drivers.
                         Example:

                             {
                             'wwpns': ['list','of','port','wwns'],
                             'ip': 'ip address',
                             'initiator': 'initiator iqn',
                             'multipath': False,
                             'host': 'hostname',
                             }

       :raises: StorageError If storage subsystem exception is raised.
       :returns: List of connected volumes, including volumes that were
                 already connected to desired nodes. The returned list
                 can be relatively consistent depending on the end storage
                 driver that the volume is configured for, however
                 the 'driver_volume_type' key should not be relied upon
                 as it is a free-form value returned by the driver.
                 The accompanying 'data' key contains the actual target
                 details which will indicate either target WWNs and a LUN
                 or a target portal and IQN. It also always contains
                 volume ID in cinder and ironic. Except for these two IDs,
                 each driver may return somewhat different data although
                 the same keys are used if the target is FC or iSCSI,
                 so any logic should be based upon the returned contents.
                 For already attached volumes, the structure contains
                 'already_attached': True key-value pair. In such case,
                 connection info for the node is already in the database,
                 'data' structure contains only basic info of volume ID in
                 cinder and ironic, so any logic based on that should
                 retrieve it from the database.
                 Example:

                 [{
                 'driver_volume_type': 'fibre_channel'
                 'data': {
                     'encrypted': False,
                     'target_lun': 1,
                     'target_wwn': ['1234567890123', '1234567890124'],
                     'volume_id': '00000000-0000-0000-0000-000000000001',
                     'ironic_volume_id':
                     '11111111-0000-0000-0000-000000000001'}
                 },
                 {
                 'driver_volume_type': 'iscsi'
                 'data': {
                     'target_iqn': 'iqn.2010-10.org.openstack:volume-00000002',
                     'target_portal': '127.0.0.0.1:3260',
                     'volume_id': '00000000-0000-0000-0000-000000000002',
                     'ironic_volume_id':
                     '11111111-0000-0000-0000-000000000002',
                     'target_lun': 2}
                 },
                 {
                 'already_attached': True
                 'data': {
                     'volume_id': '00000000-0000-0000-0000-000000000002',
                     'ironic_volume_id':
                     '11111111-0000-0000-0000-000000000002'}
                 }]
       """
    node = task.node
    client = _init_client(task)

    connected = []
    for volume_id in volume_list:
        try:
            volume = client.volumes.get(volume_id)
        except cinder_exceptions.ClientException as e:
            msg = (_('Failed to get volume %(vol_id)s from cinder for node '
                     '%(uuid)s: %(err)s') %
                   {'vol_id': volume_id, 'uuid': node.uuid, 'err': e})
            LOG.error(msg)
            raise exception.StorageError(msg)
        if is_volume_attached(node, volume):
            LOG.debug('Volume %(vol_id)s is already attached to node '
                      '%(uuid)s. Skipping attachment.',
                      {'vol_id': volume_id, 'uuid': node.uuid})

            # NOTE(jtaryma): Actual connection info of already connected
            # volume will be provided by nova. Adding this dictionary to
            # 'connected' list so it contains also already connected volumes.
            connection = {'data': {'ironic_volume_uuid': volume.id,
                                   'volume_id': volume_id},
                          'already_attached': True}
            connected.append(connection)
            continue

        try:
            client.volumes.reserve(volume_id)
        except cinder_exceptions.ClientException as e:
            msg = (_('Failed to reserve volume %(vol_id)s for node %(node)s: '
                     '%(err)s)') %
                   {'vol_id': volume_id, 'node': node.uuid, 'err': e})
            LOG.error(msg)
            raise exception.StorageError(msg)

        try:
            # Provide connector information to cinder
            connection = client.volumes.initialize_connection(volume_id,
                                                              connector)
        except cinder_exceptions.ClientException as e:
            msg = (_('Failed to initialize connection for volume '
                     '%(vol_id)s to node %(node)s: %(err)s') %
                   {'vol_id': volume_id, 'node': node.uuid, 'err': e})
            LOG.error(msg)
            raise exception.StorageError(msg)

        if 'volume_id' not in connection['data']:
            connection['data']['volume_id'] = volume_id
        connection['data']['ironic_volume_uuid'] = volume.id
        connected.append(connection)

        LOG.info('Successfully initialized volume %(vol_id)s for '
                 'node %(node)s.', {'vol_id': volume_id, 'node': node.uuid})

        instance_uuid = node.instance_uuid or node.uuid

        try:
            # NOTE(TheJulia): The final step of the cinder volume
            # attachment process involves updating the volume
            # database record to indicate that the attachment has
            # been completed, which moves the volume to the
            # 'attached' state. This action also sets a mountpoint
            # for the volume, if known. In our use case, there is
            # no way for us to know what the mountpoint is inside of
            # the operating system, thus we send None.
            client.volumes.attach(volume_id, instance_uuid, None)

        except cinder_exceptions.ClientException as e:
            msg = (_('Failed to inform cinder that the attachment for volume '
                     '%(vol_id)s for node %(node)s has been completed: '
                     '%(err)s') %
                   {'vol_id': volume_id, 'node': node.uuid, 'err': e})
            LOG.error(msg)
            raise exception.StorageError(msg)

        try:
            # Set metadata to assist a user in volume identification
            client.volumes.set_metadata(
                volume_id,
                _create_metadata_dictionary(node, 'attached'))

        except cinder_exceptions.ClientException as e:
            LOG.warning('Failed to update volume metadata for volume '
                        '%(vol_id)s for node %(node)s: %(err)s',
                        {'vol_id': volume_id, 'node': node.uuid, 'err': e})
    return connected
Exemple #9
0
    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