-# Copyright (c) 2015 Alex Meade. All rights reserved.
+# Copyright (c) 2015 Alex Meade. All rights reserved.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
from cinder import exception
from cinder import test
-from cinder.tests.unit.volume.drivers.netapp.eseries import fakes as \
- eseries_fakes
+from cinder.tests.unit.volume.drivers.netapp.eseries \
+ import fakes as eseries_fakes
from cinder.volume.drivers.netapp.eseries import host_mapper
from cinder.volume.drivers.netapp.eseries import utils
"detached", "status": "available"
}
+FAKE_MAPPINGS = [{u'lun': 1}]
+
+FAKE_USED_UP_MAPPINGS = map(lambda n: {u'lun': n}, range(256))
+
+FAKE_USED_UP_LUN_ID_DICT = {n: 1 for n in range(256)}
+
+FAKE_UNUSED_LUN_ID = set([])
+
+FAKE_USED_LUN_ID_DICT = ({0: 1, 1: 1})
+
+FAKE_USED_LUN_IDS = [1, 2]
+
+FAKE_SINGLE_USED_LUN_ID = 1
+
+FAKE_USED_UP_LUN_IDS = range(256)
+
class NetAppEseriesHostMapperTestCase(test.TestCase):
def setUp(self):
host_mapper.map_volume_to_single_host(self.client, get_fake_volume(),
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- None)
+ None,
+ False)
self.assertTrue(self.client.create_volume_mapping.called)
get_fake_volume(),
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- eseries_fakes.VOLUME_MAPPING)
+ eseries_fakes.VOLUME_MAPPING,
+ False)
self.assertFalse(self.client.create_volume_mapping.called)
get_fake_volume(),
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- fake_mapping_to_other_host)
+ fake_mapping_to_other_host,
+ False)
self.assertTrue(self.client.move_volume_mapping_via_symbol.called)
self.client, fake_volume,
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- fake_mapping_to_other_host)
+ fake_mapping_to_other_host,
+ False)
self.assertIn('multiattach is disabled', six.text_type(err))
self.client, fake_volume,
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- fake_mapping_to_other_host)
+ fake_mapping_to_other_host,
+ False)
self.assertIn('multiattach is disabled', six.text_type(err))
self.client, get_fake_volume(),
eseries_fakes.VOLUME,
eseries_fakes.HOST,
- fake_mapping_to_other_host)
+ fake_mapping_to_other_host,
+ False)
self.assertIn('multiattach is disabled', six.text_type(err))
fake_volume_mapping)
self.assertIn("unsupported host group", six.text_type(err))
+
+ def test_get_unused_lun_ids(self):
+ unused_lun_ids = host_mapper._get_unused_lun_ids(FAKE_MAPPINGS)
+ self.assertEqual(set(range(2, 256)), unused_lun_ids)
+
+ def test_get_unused_lun_id_counter(self):
+ used_lun_id_count = host_mapper._get_used_lun_id_counter(
+ FAKE_MAPPINGS)
+ self.assertEqual(FAKE_USED_LUN_ID_DICT, used_lun_id_count)
+
+ def test_get_unused_lun_ids_used_up_luns(self):
+ unused_lun_ids = host_mapper._get_unused_lun_ids(
+ FAKE_USED_UP_MAPPINGS)
+ self.assertEqual(FAKE_UNUSED_LUN_ID, unused_lun_ids)
+
+ def test_get_lun_id_counter_used_up_luns(self):
+ used_lun_ids = host_mapper._get_used_lun_id_counter(
+ FAKE_USED_UP_MAPPINGS)
+ self.assertEqual(FAKE_USED_UP_LUN_ID_DICT, used_lun_ids)
+
+ def test_host_not_full(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.assertFalse(host_mapper._is_host_full(self.client, fake_host))
+
+ def test_host_full(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+ self.assertTrue(host_mapper._is_host_full(self.client, fake_host))
+
+ def test_get_free_lun(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ with mock.patch('random.sample') as mock_random:
+ mock_random.return_value = [3]
+ lun = host_mapper._get_free_lun(self.client, fake_host, False)
+ self.assertEqual(3, lun)
+
+ def test_get_free_lun_host_full(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.mock_object(host_mapper, '_is_host_full',
+ mock.Mock(return_value=True))
+ self.assertRaises(
+ exception.NetAppDriverException,
+ host_mapper._get_free_lun,
+ self.client, fake_host, False)
+
+ def test_get_free_lun_no_unused_luns(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.mock_object(self.client, 'get_volume_mappings',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+ lun = host_mapper._get_free_lun(self.client, fake_host, False)
+ self.assertEqual(255, lun)
+
+ def test_get_free_lun_no_unused_luns_host_not_full(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.mock_object(self.client, 'get_volume_mappings',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+ self.mock_object(host_mapper, '_is_host_full',
+ mock.Mock(return_value=False))
+ lun = host_mapper._get_free_lun(self.client, fake_host, False)
+ self.assertEqual(255, lun)
+
+ def test_get_free_lun_no_lun_available(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST_3)
+ self.mock_object(self.client, 'get_volume_mappings',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+ self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+
+ self.assertRaises(exception.NetAppDriverException,
+ host_mapper._get_free_lun,
+ self.client, fake_host, False)
+
+ def test_get_free_lun_multiattach_enabled_no_unused_ids(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST_3)
+ self.mock_object(self.client, 'get_volume_mappings',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+
+ self.assertRaises(exception.NetAppDriverException,
+ host_mapper._get_free_lun,
+ self.client, fake_host, True)
+
+ def test_get_lun_by_mapping(self):
+ used_luns = host_mapper._get_used_lun_ids_for_mappings(FAKE_MAPPINGS)
+ self.assertEqual(set([0, 1]), used_luns)
+
+ def test_get_lun_by_mapping_no_mapping(self):
+ used_luns = host_mapper._get_used_lun_ids_for_mappings([])
+ self.assertEqual(set([0]), used_luns)
+
+ def test_lun_id_available_on_host(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST)
+ self.assertTrue(host_mapper._is_lun_id_available_on_host(
+ self.client, fake_host, FAKE_UNUSED_LUN_ID))
+
+ def test_no_lun_id_available_on_host(self):
+ fake_host = copy.deepcopy(eseries_fakes.HOST_3)
+ self.mock_object(host_mapper, '_get_vol_mapping_for_host_frm_array',
+ mock.Mock(return_value=FAKE_USED_UP_MAPPINGS))
+
+ self.assertFalse(host_mapper._is_lun_id_available_on_host(
+ self.client, fake_host, FAKE_SINGLE_USED_LUN_ID))
-# Copyright (c) 2015 Alex Meade. All Rights Reserved.
+# Copyright (c) 2015 Alex Meade. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
Groups.
"""
+import collections
+import random
+
from oslo_log import log as logging
-from six.moves import xrange
from cinder import exception
from cinder.i18n import _
@cinder_utils.synchronized('map_es_volume')
def map_volume_to_single_host(client, volume, eseries_vol, host,
- vol_map):
+ vol_map, multiattach_enabled):
"""Maps the e-series volume to host with initiator."""
LOG.debug("Attempting to map volume %s to single host.", volume['id'])
# If volume is not mapped on the backend, map directly to host
if not vol_map:
- mappings = _get_vol_mapping_for_host_frm_array(client, host['hostRef'])
- lun = _get_free_lun(client, host, mappings)
+ lun = _get_free_lun(client, host, multiattach_enabled)
return client.create_volume_mapping(eseries_vol['volumeRef'],
host['hostRef'], lun)
LOG.debug("Volume %(vol)s is not currently attached, moving "
"existing mapping to host %(host)s.",
{'vol': volume['id'], 'host': host['label']})
- mappings = _get_vol_mapping_for_host_frm_array(
- client, host['hostRef'])
- lun = _get_free_lun(client, host, mappings)
+ lun = _get_free_lun(client, host, multiattach_enabled)
return client.move_volume_mapping_via_symbol(
vol_map.get('mapRef'), host['hostRef'], lun
)
return mapping
-def _get_free_lun(client, host, maps=None):
- """Gets free LUN for given host."""
- ref = host['hostRef']
- luns = maps or _get_vol_mapping_for_host_frm_array(client, ref)
- if host['clusterRef'] != utils.NULL_REF:
- host_group_maps = _get_vol_mapping_for_host_group_frm_array(
- client, host['clusterRef'])
- luns.extend(host_group_maps)
- used_luns = set(map(lambda lun: int(lun['lun']), luns))
- for lun in xrange(utils.MAX_LUNS_PER_HOST):
- if lun not in used_luns:
- return lun
- msg = _("No free LUNs. Host might have exceeded max number of LUNs.")
- raise exception.NetAppDriverException(msg)
+def _get_free_lun(client, host, multiattach_enabled):
+ """Returns least used LUN ID available on the given host."""
+ mappings = client.get_volume_mappings()
+ if not _is_host_full(client, host):
+ unused_luns = _get_unused_lun_ids(mappings)
+ if unused_luns:
+ chosen_lun = random.sample(unused_luns, 1)
+ return chosen_lun[0]
+ elif multiattach_enabled:
+ msg = _("No unused LUN IDs are available on the host; "
+ "multiattach is enabled which requires that all LUN IDs "
+ "to be unique across the entire host group.")
+ raise exception.NetAppDriverException(msg)
+ used_lun_counts = _get_used_lun_id_counter(mappings)
+ # most_common returns an arbitrary tuple of members with same frequency
+ for lun_id, __ in reversed(used_lun_counts.most_common()):
+ if _is_lun_id_available_on_host(client, host, lun_id):
+ return lun_id
+ msg = _("No free LUN IDs left. Maximum number of volumes that can be "
+ "attached to host (%s) has been exceeded.")
+ raise exception.NetAppDriverException(msg % utils.MAX_LUNS_PER_HOST)
+
+
+def _get_unused_lun_ids(mappings):
+ """Returns unused LUN IDs given mappings."""
+ used_luns = _get_used_lun_ids_for_mappings(mappings)
+
+ unused_luns = (set(range(utils.MAX_LUNS_PER_HOST)) - set(used_luns))
+ return unused_luns
+
+
+def _get_used_lun_id_counter(mapping):
+ """Returns used LUN IDs with count as a dictionary."""
+ used_luns = _get_used_lun_ids_for_mappings(mapping)
+ used_lun_id_counter = collections.Counter(used_luns)
+ return used_lun_id_counter
+
+
+def _is_host_full(client, host):
+ """Checks whether maximum volumes attached to a host have been reached."""
+ luns = _get_vol_mapping_for_host_frm_array(client, host['hostRef'])
+ return len(luns) >= utils.MAX_LUNS_PER_HOST
+
+
+def _is_lun_id_available_on_host(client, host, lun_id):
+ """Returns a boolean value depending on whether a LUN ID is available."""
+ mapping = _get_vol_mapping_for_host_frm_array(client, host['hostRef'])
+ used_lun_ids = _get_used_lun_ids_for_mappings(mapping)
+ return lun_id not in used_lun_ids
+
+
+def _get_used_lun_ids_for_mappings(mappings):
+ """Returns used LUNs when provided with mappings."""
+ used_luns = set(map(lambda lun: int(lun['lun']), mappings))
+ # E-Series uses LUN ID 0 for special purposes and should not be
+ # assigned for general use
+ used_luns.add(0)
+ return used_luns
def _get_vol_mapping_for_host_frm_array(client, host_ref):