diff --git a/glance_store/_drivers/vmware_datastore.py b/glance_store/_drivers/vmware_datastore.py index 8f992f20..22e70931 100644 --- a/glance_store/_drivers/vmware_datastore.py +++ b/glance_store/_drivers/vmware_datastore.py @@ -21,11 +21,16 @@ import logging import os import socket -from oslo.vmware import api -from oslo.vmware import constants from oslo_config import cfg from oslo_utils import excutils from oslo_utils import units +from oslo_vmware import api +from oslo_vmware import constants +from oslo_vmware.objects import datacenter as oslo_datacenter +from oslo_vmware.objects import datastore as oslo_datastore +from oslo_vmware import vim_util + +import six # NOTE(jokke): simplified transition to py3, behaves like py2 xrange from six.moves import range import six.moves.urllib.parse as urlparse @@ -82,7 +87,14 @@ _VMWARE_OPTS = [ cfg.BoolOpt('vmware_api_insecure', default=False, help=_('Allow to perform insecure SSL requests to ESX/VC.')), -] + cfg.MultiStrOpt('vmware_datastores', + help=_('The datastores where the images are stored inside ' + 'vCenter. The expected format is ' + 'datacenter_path:datastore_name:weight. The weight ' + 'will be used unless there is not enough free ' + 'space to store the image. If the weights are ' + 'equal, the datastore with most free space ' + 'is chosen.'))] def is_valid_ipv6(address): @@ -170,18 +182,22 @@ class StoreLocation(location.StoreLocation): vsphere://server_host/folder/file_path?dcPath=dc_path&dsName=ds_name """ + def __init__(self, store_specs, conf): + super(StoreLocation, self).__init__(store_specs, conf) + self.datacenter_path = None + self.datastore_name = None + def process_specs(self): self.scheme = self.specs.get('scheme', STORE_SCHEME) self.server_host = self.specs.get('server_host') self.path = os.path.join(DS_URL_PREFIX, self.specs.get('image_dir').strip('/'), self.specs.get('image_id')) - dc_path = self.specs.get('datacenter_path') - if dc_path is not None: - param_list = {'dcPath': self.specs.get('datacenter_path'), - 'dsName': self.specs.get('datastore_name')} - else: - param_list = {'dsName': self.specs.get('datastore_name')} + self.datacenter_path = self.specs.get('datacenter_path') + self.datstore_name = self.specs.get('datastore_name') + param_list = {'dsName': self.datstore_name} + if self.datacenter_path: + param_list['dcPath'] = self.datacenter_path self.query = urlparse.urlencode(param_list) def get_uri(self): @@ -218,6 +234,13 @@ class StoreLocation(location.StoreLocation): # reason = 'Badly formed VMware datastore URI %(uri)s.' % {'uri': uri} # LOG.debug(reason) # raise exceptions.BadStoreUri(reason) + parts = urlparse.parse_qs(self.query) + dc_path = parts.get('dcPath') + if dc_path: + self.datacenter_path = dc_path[0] + ds_name = parts.get('dsName') + if ds_name: + self.datastore_name = ds_name[0] class Store(glance_store.Store): @@ -230,6 +253,10 @@ class Store(glance_store.Store): # FIXME(arnaud): re-visit this code once the store API is cleaned up. _VMW_SESSION = None + def __init__(self, conf): + super(Store, self).__init__(conf) + self.datastores = {} + def reset_session(self, force=False): if Store._VMW_SESSION is None or force: Store._VMW_SESSION = api.VMwareAPISession( @@ -248,12 +275,29 @@ class Store(glance_store.Store): LOG.error(msg) raise exceptions.BadStoreConfiguration( store_name='vmware_datastore', reason=msg) + if self.conf.glance_store.vmware_task_poll_interval <= 0: msg = _('vmware_task_poll_interval should be greater than zero') LOG.error(msg) raise exceptions.BadStoreConfiguration( store_name='vmware_datastore', reason=msg) + if not (self.conf.glance_store.vmware_datastore_name + or self.conf.glance_store.vmware_datastores): + msg = (_("Specify at least 'vmware_datastore_name' or " + "'vmware_datastores' option")) + LOG.error(msg) + raise exceptions.BadStoreConfiguration( + store_name='vmware_datastore', reason=msg) + + if (self.conf.glance_store.vmware_datastore_name and + self.conf.glance_store.vmware_datastores): + msg = (_("Specify either 'vmware_datastore_name' or " + "'vmware_datastores' option")) + LOG.error(msg) + raise exceptions.BadStoreConfiguration( + store_name='vmware_datastore', reason=msg) + def configure(self): self._sanity_check() self.scheme = STORE_SCHEME @@ -265,31 +309,122 @@ class Store(glance_store.Store): self.api_insecure = self.conf.glance_store.vmware_api_insecure super(Store, self).configure() - def configure_add(self): - self.datacenter_path = self.conf.glance_store.vmware_datacenter_path - self.datastore_name = self._option_get('vmware_datastore_name') - global _datastore_info_valid - if not _datastore_info_valid: - search_index_moref = ( - self.session.vim.service_content.searchIndex) + def _get_datacenter(self, datacenter_path): + search_index_moref = self.session.vim.service_content.searchIndex + dc_moref = self.session.invoke_api( + self.session.vim, + 'FindByInventoryPath', + search_index_moref, + inventoryPath=datacenter_path) + dc_name = datacenter_path.rsplit('/', 1)[-1] + # TODO(sabari): Add datacenter_path attribute in oslo.vmware + dc_obj = oslo_datacenter.Datacenter(ref=dc_moref, name=dc_name) + dc_obj.path = datacenter_path + return dc_obj - inventory_path = ('%s/datastore/%s' - % (self.datacenter_path, self.datastore_name)) - ds_moref = self.session.invoke_api( - self.session.vim, 'FindByInventoryPath', - search_index_moref, inventoryPath=inventory_path) - if ds_moref is None: + def _get_datastore(self, datacenter_path, datastore_name): + dc_obj = self._get_datacenter(datacenter_path) + datastore_ret = self.session.invoke_api( + vim_util, 'get_object_property', self.session.vim, dc_obj.ref, + 'datastore') + if datastore_ret: + datastore_refs = datastore_ret.ManagedObjectReference + for ds_ref in datastore_refs: + ds_obj = oslo_datastore.get_datastore_by_ref(self.session, + ds_ref) + if ds_obj.name == datastore_name: + ds_obj.datacenter = dc_obj + return ds_obj + + def _get_freespace(self, ds_obj): + # TODO(sabari): Move this function into oslo_vmware's datastore object. + return self.session.invoke_api( + vim_util, 'get_object_property', self.session.vim, ds_obj.ref, + 'summary.freeSpace') + + def _parse_datastore_info_and_weight(self, datastore): + weight = 0 + parts = map(lambda x: x.strip(), datastore.rsplit(":", 2)) + if len(parts) < 2: + msg = _('vmware_datastores format must be ' + 'datacenter_path:datastore_name:weight or ' + 'datacenter_path:datastore_name') + LOG.error(msg) + raise exceptions.BadStoreConfiguration( + store_name='vmware_datastore', reason=msg) + if len(parts) == 3 and parts[2]: + weight = parts[2] + if not weight.isdigit(): + msg = (_('Invalid weight value %(weight)s in ' + 'vmware_datastores configuration') % + {'weight': weight}) + LOG.exception(msg) + raise exceptions.BadStoreConfiguration( + store_name="vmware_datastore", reason=msg) + datacenter_path, datastore_name = parts[0], parts[1] + if not datacenter_path or not datastore_name: + msg = _('Invalid datacenter_path or datastore_name specified ' + 'in vmware_datastores configuration') + LOG.exception(msg) + raise exceptions.BadStoreConfiguration( + store_name="vmware_datastore", reason=msg) + return datacenter_path, datastore_name, weight + + def _build_datastore_weighted_map(self, datastores): + """Build an ordered map where the key is a weight and the value is a + Datastore object. + + :param: a list of datastores in the format + datacenter_path:datastore_name:weight + :return: a map with key-value : + """ + ds_map = {} + for ds in datastores: + dc_path, name, weight = self._parse_datastore_info_and_weight(ds) + # Fetch the server side reference. + ds_obj = self._get_datastore(dc_path, name) + if not ds_obj: msg = (_("Could not find datastore %(ds_name)s " "in datacenter %(dc_path)s") - % {'ds_name': self.datastore_name, - 'dc_path': self.datacenter_path}) + % {'ds_name': name, + 'dc_path': dc_path}) LOG.error(msg) raise exceptions.BadStoreConfiguration( store_name='vmware_datastore', reason=msg) - else: - _datastore_info_valid = True + ds_map.setdefault(int(weight), []).append(ds_obj) + return ds_map + + def configure_add(self): + if self.conf.glance_store.vmware_datastores: + datastores = self.conf.glance_store.vmware_datastores + else: + # Backwards compatibility for vmware_datastore_name and + # vmware_datacenter_path. + datacenter_path = self.conf.glance_store.vmware_datacenter_path + datastore_name = self._option_get('vmware_datastore_name') + datastores = ['%s:%s:%s' % (datacenter_path, datastore_name, 0)] + + self.datastores = self._build_datastore_weighted_map(datastores) self.store_image_dir = self.conf.glance_store.vmware_store_image_dir + def select_datastore(self, image_size): + """Select a datastore with free space larger than image size.""" + for k, v in sorted(six.iteritems(self.datastores), reverse=True): + max_ds = None + max_fs = 0 + for ds in v: + # Update with current freespace + ds.freespace = self._get_freespace(ds) + if ds.freespace > max_fs: + max_ds = ds + max_fs = ds.freespace + if max_ds and max_ds.freespace >= image_size: + return max_ds + msg = _LE("No datastore found with enough free space to contain an " + "image of size %d") % image_size + LOG.error(msg) + raise exceptions.StorageFull() + def _option_get(self, param): result = getattr(self.conf.glance_store, param) if not result: @@ -325,6 +460,7 @@ class Store(glance_store.Store): request returned an unexpected status. The expected responses are 201 Created and 200 OK. """ + ds = self.select_datastore(image_size) if image_size > 0: headers = {'Content-Length': image_size} image_file = _Reader(image_file) @@ -337,8 +473,8 @@ class Store(glance_store.Store): loc = StoreLocation({'scheme': self.scheme, 'server_host': self.server_host, 'image_dir': self.store_image_dir, - 'datacenter_path': self.datacenter_path, - 'datastore_name': self.datastore_name, + 'datacenter_path': ds.datacenter.path, + 'datastore_name': ds.name, 'image_id': image_id}, self.conf) # NOTE(arnaud): use a decorator when the config is not tied to self cookie = self._build_vim_cookie_header(True) @@ -425,18 +561,15 @@ class Store(glance_store.Store): :raises NotFound if image does not exist """ file_path = '[%s] %s' % ( - self.datastore_name, + location.store_location.datastore_name, location.store_location.path[len(DS_URL_PREFIX):]) - search_index_moref = self.session.vim.service_content.searchIndex - dc_moref = self.session.invoke_api( - self.session.vim, 'FindByInventoryPath', search_index_moref, - inventoryPath=self.datacenter_path) + dc_obj = self._get_datacenter(location.store_location.datacenter_path) delete_task = self.session.invoke_api( self.session.vim, 'DeleteDatastoreFile_Task', self.session.vim.service_content.fileManager, name=file_path, - datacenter=dc_moref) + datacenter=dc_obj.ref) try: self.session.wait_for_task(delete_task) except Exception: diff --git a/tests/unit/test_opts.py b/tests/unit/test_opts.py index cc4f7787..5559dec1 100644 --- a/tests/unit/test_opts.py +++ b/tests/unit/test_opts.py @@ -113,6 +113,7 @@ class OptsTestCase(base.StoreBaseTest): 'vmware_api_retry_count', 'vmware_datacenter_path', 'vmware_datastore_name', + 'vmware_datastores', 'vmware_server_host', 'vmware_server_password', 'vmware_server_username', diff --git a/tests/unit/test_vmware_store.py b/tests/unit/test_vmware_store.py index 773e135f..22c3ac9a 100644 --- a/tests/unit/test_vmware_store.py +++ b/tests/unit/test_vmware_store.py @@ -19,8 +19,10 @@ import hashlib import uuid import mock -from oslo.vmware import api from oslo_utils import units +from oslo_vmware import api +from oslo_vmware.objects import datacenter as oslo_datacenter +from oslo_vmware.objects import datastore as oslo_datastore import six import glance_store._drivers.vmware_datastore as vm_store @@ -79,11 +81,20 @@ class FakeHTTPConnection(object): pass +def fake_datastore_obj(*args, **kwargs): + dc_obj = oslo_datacenter.Datacenter(ref='fake-ref', + name='fake-name') + dc_obj.path = args[0] + return oslo_datastore.Datastore(ref='fake-ref', + datacenter=dc_obj, + name=args[1]) + + class TestStore(base.StoreBaseTest, test_store_capabilities.TestStoreCapabilitiesChecking): - @mock.patch('oslo.vmware.api.VMwareAPISession', autospec=True) - def setUp(self, mock_session): + @mock.patch.object(vm_store.Store, '_get_datastore') + def setUp(self, mock_get_datastore): """Establish a clean test environment.""" super(TestStore, self).setUp() @@ -98,6 +109,7 @@ class TestStore(base.StoreBaseTest, vmware_datastore_name=VMWARE_DS['vmware_datastore_name'], vmware_datacenter_path=VMWARE_DS['vmware_datacenter_path']) + mock_get_datastore.side_effect = fake_datastore_obj backend.create_stores(self.conf) self.store = backend.get_store_from_scheme('vsphere') @@ -105,7 +117,8 @@ class TestStore(base.StoreBaseTest, self.store.store_image_dir = ( VMWARE_DS['vmware_store_image_dir']) - def test_get(self): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_get(self, mock_api_session): """Test a "normal" retrieval of an image in chunks.""" expected_image_size = 31 expected_returns = ['I am a teapot, short and stout\n'] @@ -119,7 +132,8 @@ class TestStore(base.StoreBaseTest, chunks = [c for c in image_file] self.assertEqual(expected_returns, chunks) - def test_get_non_existing(self): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_get_non_existing(self, mock_api_session): """ Test that trying to retrieve an image that doesn't exist raises an error @@ -131,9 +145,12 @@ class TestStore(base.StoreBaseTest, HttpConn.return_value = FakeHTTPConnection(status=404) self.assertRaises(exceptions.NotFound, self.store.get, loc) + @mock.patch.object(vm_store.Store, 'select_datastore') @mock.patch.object(vm_store._Reader, 'size') - def test_add(self, fake_size): + @mock.patch.object(api, 'VMwareAPISession') + def test_add(self, fake_api_session, fake_size, fake_select_datastore): """Test that we can add an image via the VMware backend.""" + fake_select_datastore.return_value = self.store.datastores[0][0] expected_image_id = str(uuid.uuid4()) expected_size = FIVE_KB expected_contents = "*" * expected_size @@ -159,12 +176,16 @@ class TestStore(base.StoreBaseTest, self.assertEqual(expected_size, size) self.assertEqual(expected_checksum, checksum) + @mock.patch.object(vm_store.Store, 'select_datastore') @mock.patch.object(vm_store._Reader, 'size') - def test_add_size_zero(self, fake_size): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_add_size_zero(self, mock_api_session, fake_size, + fake_select_datastore): """ Test that when specifying size zero for the image to add, the actual size of the image is returned. """ + fake_select_datastore.return_value = self.store.datastores[0][0] expected_image_id = str(uuid.uuid4()) expected_size = FIVE_KB expected_contents = "*" * expected_size @@ -189,7 +210,8 @@ class TestStore(base.StoreBaseTest, self.assertEqual(expected_size, size) self.assertEqual(expected_checksum, checksum) - def test_delete(self): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_delete(self, mock_api_session): """Test we can delete an existing image in the VMware store.""" loc = location.get_location_from_uri( "vsphere://127.0.0.1/folder/openstack_glance/%s?" @@ -202,7 +224,8 @@ class TestStore(base.StoreBaseTest, HttpConn.return_value = FakeHTTPConnection(status=404) self.assertRaises(exceptions.NotFound, self.store.get, loc) - def test_get_size(self): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_get_size(self, mock_api_session): """ Test we can get the size of an existing image in the VMware store """ @@ -214,7 +237,8 @@ class TestStore(base.StoreBaseTest, image_size = self.store.get_size(loc) self.assertEqual(image_size, 31) - def test_get_size_non_existing(self): + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_get_size_non_existing(self, mock_api_session): """ Test that trying to retrieve an image size that doesn't exist raises an error @@ -329,8 +353,64 @@ class TestStore(base.StoreBaseTest, except exceptions.BadStoreConfiguration: self.fail() + def test_sanity_check_multiple_datastores(self): + self.store.conf.glance_store.vmware_api_retry_count = 1 + self.store.conf.glance_store.vmware_task_poll_interval = 1 + # Check both vmware_datastore_name and vmware_datastores defined. + self.store.conf.glance_store.vmware_datastores = ['a:b:0'] + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._sanity_check) + # Both vmware_datastore_name and vmware_datastores are not defined. + self.store.conf.glance_store.vmware_datastore_name = None + self.store.conf.glance_store.vmware_datastores = None + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._sanity_check) + self.store.conf.glance_store.vmware_datastore_name = None + self.store.conf.glance_store.vmware_datastores = ['a:b:0', 'a:d:0'] + try: + self.store._sanity_check() + except exceptions.BadStoreConfiguration: + self.fail() + + def test_parse_datastore_info_and_weight_less_opts(self): + datastore = 'a' + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._parse_datastore_info_and_weight, + datastore) + + def test_parse_datastore_info_and_weight_invalid_weight(self): + datastore = 'a:b:c' + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._parse_datastore_info_and_weight, + datastore) + + def test_parse_datastore_info_and_weight_empty_opts(self): + datastore = 'a: :0' + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._parse_datastore_info_and_weight, + datastore) + datastore = ':b:0' + self.assertRaises(exceptions.BadStoreConfiguration, + self.store._parse_datastore_info_and_weight, + datastore) + + def test_parse_datastore_info_and_weight(self): + datastore = 'a:b:100' + parts = self.store._parse_datastore_info_and_weight(datastore) + self.assertEqual('a', parts[0]) + self.assertEqual('b', parts[1]) + self.assertEqual('100', parts[2]) + + def test_parse_datastore_info_and_weight_default_weight(self): + datastore = 'a:b' + parts = self.store._parse_datastore_info_and_weight(datastore) + self.assertEqual('a', parts[0]) + self.assertEqual('b', parts[1]) + self.assertEqual(0, parts[2]) + + @mock.patch.object(vm_store.Store, 'select_datastore') @mock.patch.object(api, 'VMwareAPISession') - def test_unexpected_status(self, mock_api_session): + def test_unexpected_status(self, mock_api_session, mock_select_datastore): expected_image_id = str(uuid.uuid4()) expected_size = FIVE_KB expected_contents = "*" * expected_size @@ -344,6 +424,9 @@ class TestStore(base.StoreBaseTest, @mock.patch.object(api, 'VMwareAPISession') def test_reset_session(self, mock_api_session): + # Initialize session and reset mock before testing. + self.store.reset_session() + mock_api_session.reset_mock() self.store.reset_session(force=False) self.assertFalse(mock_api_session.called) self.store.reset_session() @@ -353,6 +436,9 @@ class TestStore(base.StoreBaseTest, @mock.patch.object(api, 'VMwareAPISession') def test_build_vim_cookie_header_active(self, mock_api_session): + # Initialize session and reset mock before testing. + self.store.reset_session() + mock_api_session.reset_mock() self.store.session.is_current_session_active = mock.Mock() self.store.session.is_current_session_active.return_value = True self.store._build_vim_cookie_header(True) @@ -360,6 +446,9 @@ class TestStore(base.StoreBaseTest, @mock.patch.object(api, 'VMwareAPISession') def test_build_vim_cookie_header_expired(self, mock_api_session): + # Initialize session and reset mock before testing. + self.store.reset_session() + mock_api_session.reset_mock() self.store.session.is_current_session_active = mock.Mock() self.store.session.is_current_session_active.return_value = False self.store._build_vim_cookie_header(True) @@ -367,13 +456,18 @@ class TestStore(base.StoreBaseTest, @mock.patch.object(api, 'VMwareAPISession') def test_build_vim_cookie_header_expired_noverify(self, mock_api_session): + # Initialize session and reset mock before testing. + self.store.reset_session() + mock_api_session.reset_mock() self.store.session.is_current_session_active = mock.Mock() self.store.session.is_current_session_active.return_value = False self.store._build_vim_cookie_header() self.assertFalse(mock_api_session.called) + @mock.patch.object(vm_store.Store, 'select_datastore') @mock.patch.object(api, 'VMwareAPISession') - def test_add_ioerror(self, mock_api_session): + def test_add_ioerror(self, mock_api_session, mock_select_datastore): + mock_select_datastore.return_value = self.store.datastores[0][0] expected_image_id = str(uuid.uuid4()) expected_size = FIVE_KB expected_contents = "*" * expected_size @@ -390,3 +484,120 @@ class TestStore(base.StoreBaseTest, exp_url = 'scheme://example.com/path?key1=val1%3Fsort%3Dtrue&key2=val2' self.assertEqual(exp_url, utils.sort_url_by_qs_keys(url)) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(api, 'VMwareAPISession') + def test_build_datastore_weighted_map(self, mock_api_session, mock_ds_obj): + datastores = ['a:b:100', 'c:d:100', 'e:f:200'] + mock_ds_obj.side_effect = fake_datastore_obj + ret = self.store._build_datastore_weighted_map(datastores) + ds = ret[200] + self.assertEqual('e', ds[0].datacenter.path) + self.assertEqual('f', ds[0].name) + ds = ret[100] + self.assertEqual(2, len(ds)) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(api, 'VMwareAPISession') + def test_build_datastore_weighted_map_equal_weight(self, mock_api_session, + mock_ds_obj): + datastores = ['a:b:200', 'a:b:200'] + mock_ds_obj.side_effect = fake_datastore_obj + ret = self.store._build_datastore_weighted_map(datastores) + ds = ret[200] + self.assertEqual(2, len(ds)) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(api, 'VMwareAPISession') + def test_build_datastore_weighted_map_empty_list(self, mock_api_session, + mock_ds_ref): + datastores = [] + ret = self.store._build_datastore_weighted_map(datastores) + self.assertEqual({}, ret) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(vm_store.Store, '_get_freespace') + def test_select_datastore_insufficient_freespace(self, mock_get_freespace, + mock_ds_ref): + datastores = ['a:b:100', 'c:d:100', 'e:f:200'] + image_size = 10 + self.store.datastores = ( + self.store._build_datastore_weighted_map(datastores)) + freespaces = [5, 5, 5] + + def fake_get_fp(*args, **kwargs): + return freespaces.pop(0) + mock_get_freespace.side_effect = fake_get_fp + self.assertRaises(exceptions.StorageFull, + self.store.select_datastore, image_size) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(vm_store.Store, '_get_freespace') + def test_select_datastore_insufficient_fs_one_ds(self, mock_get_freespace, + mock_ds_ref): + # Tests if fs is updated with just one datastore. + datastores = ['a:b:100'] + image_size = 10 + self.store.datastores = ( + self.store._build_datastore_weighted_map(datastores)) + freespaces = [5] + + def fake_get_fp(*args, **kwargs): + return freespaces.pop(0) + mock_get_freespace.side_effect = fake_get_fp + self.assertRaises(exceptions.StorageFull, + self.store.select_datastore, image_size) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(vm_store.Store, '_get_freespace') + def test_select_datastore_equal_freespace(self, mock_get_freespace, + mock_ds_obj): + datastores = ['a:b:100', 'c:d:100', 'e:f:200'] + image_size = 10 + mock_ds_obj.side_effect = fake_datastore_obj + self.store.datastores = ( + self.store._build_datastore_weighted_map(datastores)) + freespaces = [11, 11, 11] + + def fake_get_fp(*args, **kwargs): + return freespaces.pop(0) + mock_get_freespace.side_effect = fake_get_fp + + ds = self.store.select_datastore(image_size) + self.assertEqual('e', ds.datacenter.path) + self.assertEqual('f', ds.name) + + @mock.patch.object(vm_store.Store, '_get_datastore') + @mock.patch.object(vm_store.Store, '_get_freespace') + def test_select_datastore_contention(self, mock_get_freespace, + mock_ds_obj): + datastores = ['a:b:100', 'c:d:100', 'e:f:200'] + image_size = 10 + mock_ds_obj.side_effect = fake_datastore_obj + self.store.datastores = ( + self.store._build_datastore_weighted_map(datastores)) + freespaces = [5, 11, 12] + + def fake_get_fp(*args, **kwargs): + return freespaces.pop(0) + mock_get_freespace.side_effect = fake_get_fp + ds = self.store.select_datastore(image_size) + self.assertEqual('c', ds.datacenter.path) + self.assertEqual('d', ds.name) + + def test_select_datastore_empty_list(self): + datastores = [] + self.store.datastores = ( + self.store._build_datastore_weighted_map(datastores)) + self.assertRaises(exceptions.StorageFull, + self.store.select_datastore, 10) + + @mock.patch('oslo_vmware.api.VMwareAPISession') + def test_get_datacenter_ref(self, mock_api_session): + datacenter_path = 'Datacenter1' + self.store._get_datacenter(datacenter_path) + self.store.session.invoke_api.assert_called_with( + self.store.session.vim, + 'FindByInventoryPath', + self.store.session.vim.service_content.searchIndex, + inventoryPath=datacenter_path)