VMware: Support Multiple Datastores

Support for VMware store to use multiple datastore backends.

Spec (approved):
https://review.openstack.org/#/c/146723/

tl;dr:
1. Adds a new config option vmware_datastores to configure
   multiple datastores.
2. Implements a selection logic based on priority to choose
   from the list of datastores.
3. Modifies StoreLocation parsing logic to identify datastore
   related info from location URI.

DocImpact

Implements-Blueprint: vmware-store-multiple-datastores

Change-Id: I176f1143cd2d9b0a01a0f4f4256e7ac7d9b09afd
This commit is contained in:
Sabari Kumar Murugesan 2015-01-19 00:35:50 -08:00 committed by Sabari
parent 547dc2fcfb
commit aa10f66ee0
3 changed files with 391 additions and 46 deletions

View File

@ -21,11 +21,16 @@ import logging
import os import os
import socket import socket
from oslo.vmware import api
from oslo.vmware import constants
from oslo_config import cfg from oslo_config import cfg
from oslo_utils import excutils from oslo_utils import excutils
from oslo_utils import units 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 # NOTE(jokke): simplified transition to py3, behaves like py2 xrange
from six.moves import range from six.moves import range
import six.moves.urllib.parse as urlparse import six.moves.urllib.parse as urlparse
@ -82,7 +87,14 @@ _VMWARE_OPTS = [
cfg.BoolOpt('vmware_api_insecure', cfg.BoolOpt('vmware_api_insecure',
default=False, default=False,
help=_('Allow to perform insecure SSL requests to ESX/VC.')), 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): 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 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): def process_specs(self):
self.scheme = self.specs.get('scheme', STORE_SCHEME) self.scheme = self.specs.get('scheme', STORE_SCHEME)
self.server_host = self.specs.get('server_host') self.server_host = self.specs.get('server_host')
self.path = os.path.join(DS_URL_PREFIX, self.path = os.path.join(DS_URL_PREFIX,
self.specs.get('image_dir').strip('/'), self.specs.get('image_dir').strip('/'),
self.specs.get('image_id')) self.specs.get('image_id'))
dc_path = self.specs.get('datacenter_path') self.datacenter_path = self.specs.get('datacenter_path')
if dc_path is not None: self.datstore_name = self.specs.get('datastore_name')
param_list = {'dcPath': self.specs.get('datacenter_path'), param_list = {'dsName': self.datstore_name}
'dsName': self.specs.get('datastore_name')} if self.datacenter_path:
else: param_list['dcPath'] = self.datacenter_path
param_list = {'dsName': self.specs.get('datastore_name')}
self.query = urlparse.urlencode(param_list) self.query = urlparse.urlencode(param_list)
def get_uri(self): def get_uri(self):
@ -218,6 +234,13 @@ class StoreLocation(location.StoreLocation):
# reason = 'Badly formed VMware datastore URI %(uri)s.' % {'uri': uri} # reason = 'Badly formed VMware datastore URI %(uri)s.' % {'uri': uri}
# LOG.debug(reason) # LOG.debug(reason)
# raise exceptions.BadStoreUri(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): 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. # FIXME(arnaud): re-visit this code once the store API is cleaned up.
_VMW_SESSION = None _VMW_SESSION = None
def __init__(self, conf):
super(Store, self).__init__(conf)
self.datastores = {}
def reset_session(self, force=False): def reset_session(self, force=False):
if Store._VMW_SESSION is None or force: if Store._VMW_SESSION is None or force:
Store._VMW_SESSION = api.VMwareAPISession( Store._VMW_SESSION = api.VMwareAPISession(
@ -248,12 +275,29 @@ class Store(glance_store.Store):
LOG.error(msg) LOG.error(msg)
raise exceptions.BadStoreConfiguration( raise exceptions.BadStoreConfiguration(
store_name='vmware_datastore', reason=msg) store_name='vmware_datastore', reason=msg)
if self.conf.glance_store.vmware_task_poll_interval <= 0: if self.conf.glance_store.vmware_task_poll_interval <= 0:
msg = _('vmware_task_poll_interval should be greater than zero') msg = _('vmware_task_poll_interval should be greater than zero')
LOG.error(msg) LOG.error(msg)
raise exceptions.BadStoreConfiguration( raise exceptions.BadStoreConfiguration(
store_name='vmware_datastore', reason=msg) 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): def configure(self):
self._sanity_check() self._sanity_check()
self.scheme = STORE_SCHEME self.scheme = STORE_SCHEME
@ -265,31 +309,122 @@ class Store(glance_store.Store):
self.api_insecure = self.conf.glance_store.vmware_api_insecure self.api_insecure = self.conf.glance_store.vmware_api_insecure
super(Store, self).configure() super(Store, self).configure()
def configure_add(self): def _get_datacenter(self, datacenter_path):
self.datacenter_path = self.conf.glance_store.vmware_datacenter_path search_index_moref = self.session.vim.service_content.searchIndex
self.datastore_name = self._option_get('vmware_datastore_name') dc_moref = self.session.invoke_api(
global _datastore_info_valid self.session.vim,
if not _datastore_info_valid: 'FindByInventoryPath',
search_index_moref = ( search_index_moref,
self.session.vim.service_content.searchIndex) 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' def _get_datastore(self, datacenter_path, datastore_name):
% (self.datacenter_path, self.datastore_name)) dc_obj = self._get_datacenter(datacenter_path)
ds_moref = self.session.invoke_api( datastore_ret = self.session.invoke_api(
self.session.vim, 'FindByInventoryPath', vim_util, 'get_object_property', self.session.vim, dc_obj.ref,
search_index_moref, inventoryPath=inventory_path) 'datastore')
if ds_moref is None: 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 <weight>:<Datastore>
"""
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 " msg = (_("Could not find datastore %(ds_name)s "
"in datacenter %(dc_path)s") "in datacenter %(dc_path)s")
% {'ds_name': self.datastore_name, % {'ds_name': name,
'dc_path': self.datacenter_path}) 'dc_path': dc_path})
LOG.error(msg) LOG.error(msg)
raise exceptions.BadStoreConfiguration( raise exceptions.BadStoreConfiguration(
store_name='vmware_datastore', reason=msg) store_name='vmware_datastore', reason=msg)
else: ds_map.setdefault(int(weight), []).append(ds_obj)
_datastore_info_valid = True 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 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): def _option_get(self, param):
result = getattr(self.conf.glance_store, param) result = getattr(self.conf.glance_store, param)
if not result: if not result:
@ -325,6 +460,7 @@ class Store(glance_store.Store):
request returned an unexpected status. The expected responses request returned an unexpected status. The expected responses
are 201 Created and 200 OK. are 201 Created and 200 OK.
""" """
ds = self.select_datastore(image_size)
if image_size > 0: if image_size > 0:
headers = {'Content-Length': image_size} headers = {'Content-Length': image_size}
image_file = _Reader(image_file) image_file = _Reader(image_file)
@ -337,8 +473,8 @@ class Store(glance_store.Store):
loc = StoreLocation({'scheme': self.scheme, loc = StoreLocation({'scheme': self.scheme,
'server_host': self.server_host, 'server_host': self.server_host,
'image_dir': self.store_image_dir, 'image_dir': self.store_image_dir,
'datacenter_path': self.datacenter_path, 'datacenter_path': ds.datacenter.path,
'datastore_name': self.datastore_name, 'datastore_name': ds.name,
'image_id': image_id}, self.conf) 'image_id': image_id}, self.conf)
# NOTE(arnaud): use a decorator when the config is not tied to self # NOTE(arnaud): use a decorator when the config is not tied to self
cookie = self._build_vim_cookie_header(True) cookie = self._build_vim_cookie_header(True)
@ -425,18 +561,15 @@ class Store(glance_store.Store):
:raises NotFound if image does not exist :raises NotFound if image does not exist
""" """
file_path = '[%s] %s' % ( file_path = '[%s] %s' % (
self.datastore_name, location.store_location.datastore_name,
location.store_location.path[len(DS_URL_PREFIX):]) location.store_location.path[len(DS_URL_PREFIX):])
search_index_moref = self.session.vim.service_content.searchIndex dc_obj = self._get_datacenter(location.store_location.datacenter_path)
dc_moref = self.session.invoke_api(
self.session.vim, 'FindByInventoryPath', search_index_moref,
inventoryPath=self.datacenter_path)
delete_task = self.session.invoke_api( delete_task = self.session.invoke_api(
self.session.vim, self.session.vim,
'DeleteDatastoreFile_Task', 'DeleteDatastoreFile_Task',
self.session.vim.service_content.fileManager, self.session.vim.service_content.fileManager,
name=file_path, name=file_path,
datacenter=dc_moref) datacenter=dc_obj.ref)
try: try:
self.session.wait_for_task(delete_task) self.session.wait_for_task(delete_task)
except Exception: except Exception:

View File

@ -113,6 +113,7 @@ class OptsTestCase(base.StoreBaseTest):
'vmware_api_retry_count', 'vmware_api_retry_count',
'vmware_datacenter_path', 'vmware_datacenter_path',
'vmware_datastore_name', 'vmware_datastore_name',
'vmware_datastores',
'vmware_server_host', 'vmware_server_host',
'vmware_server_password', 'vmware_server_password',
'vmware_server_username', 'vmware_server_username',

View File

@ -19,8 +19,10 @@ import hashlib
import uuid import uuid
import mock import mock
from oslo.vmware import api
from oslo_utils import units 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 six
import glance_store._drivers.vmware_datastore as vm_store import glance_store._drivers.vmware_datastore as vm_store
@ -79,11 +81,20 @@ class FakeHTTPConnection(object):
pass 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, class TestStore(base.StoreBaseTest,
test_store_capabilities.TestStoreCapabilitiesChecking): test_store_capabilities.TestStoreCapabilitiesChecking):
@mock.patch('oslo.vmware.api.VMwareAPISession', autospec=True) @mock.patch.object(vm_store.Store, '_get_datastore')
def setUp(self, mock_session): def setUp(self, mock_get_datastore):
"""Establish a clean test environment.""" """Establish a clean test environment."""
super(TestStore, self).setUp() super(TestStore, self).setUp()
@ -98,6 +109,7 @@ class TestStore(base.StoreBaseTest,
vmware_datastore_name=VMWARE_DS['vmware_datastore_name'], vmware_datastore_name=VMWARE_DS['vmware_datastore_name'],
vmware_datacenter_path=VMWARE_DS['vmware_datacenter_path']) vmware_datacenter_path=VMWARE_DS['vmware_datacenter_path'])
mock_get_datastore.side_effect = fake_datastore_obj
backend.create_stores(self.conf) backend.create_stores(self.conf)
self.store = backend.get_store_from_scheme('vsphere') self.store = backend.get_store_from_scheme('vsphere')
@ -105,7 +117,8 @@ class TestStore(base.StoreBaseTest,
self.store.store_image_dir = ( self.store.store_image_dir = (
VMWARE_DS['vmware_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.""" """Test a "normal" retrieval of an image in chunks."""
expected_image_size = 31 expected_image_size = 31
expected_returns = ['I am a teapot, short and stout\n'] 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] chunks = [c for c in image_file]
self.assertEqual(expected_returns, chunks) 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 Test that trying to retrieve an image that doesn't exist
raises an error raises an error
@ -131,9 +145,12 @@ class TestStore(base.StoreBaseTest,
HttpConn.return_value = FakeHTTPConnection(status=404) HttpConn.return_value = FakeHTTPConnection(status=404)
self.assertRaises(exceptions.NotFound, self.store.get, loc) self.assertRaises(exceptions.NotFound, self.store.get, loc)
@mock.patch.object(vm_store.Store, 'select_datastore')
@mock.patch.object(vm_store._Reader, 'size') @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.""" """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_image_id = str(uuid.uuid4())
expected_size = FIVE_KB expected_size = FIVE_KB
expected_contents = "*" * expected_size expected_contents = "*" * expected_size
@ -159,12 +176,16 @@ class TestStore(base.StoreBaseTest,
self.assertEqual(expected_size, size) self.assertEqual(expected_size, size)
self.assertEqual(expected_checksum, checksum) self.assertEqual(expected_checksum, checksum)
@mock.patch.object(vm_store.Store, 'select_datastore')
@mock.patch.object(vm_store._Reader, 'size') @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, Test that when specifying size zero for the image to add,
the actual size of the image is returned. 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_image_id = str(uuid.uuid4())
expected_size = FIVE_KB expected_size = FIVE_KB
expected_contents = "*" * expected_size expected_contents = "*" * expected_size
@ -189,7 +210,8 @@ class TestStore(base.StoreBaseTest,
self.assertEqual(expected_size, size) self.assertEqual(expected_size, size)
self.assertEqual(expected_checksum, checksum) 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.""" """Test we can delete an existing image in the VMware store."""
loc = location.get_location_from_uri( loc = location.get_location_from_uri(
"vsphere://127.0.0.1/folder/openstack_glance/%s?" "vsphere://127.0.0.1/folder/openstack_glance/%s?"
@ -202,7 +224,8 @@ class TestStore(base.StoreBaseTest,
HttpConn.return_value = FakeHTTPConnection(status=404) HttpConn.return_value = FakeHTTPConnection(status=404)
self.assertRaises(exceptions.NotFound, self.store.get, loc) 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 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) image_size = self.store.get_size(loc)
self.assertEqual(image_size, 31) 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 Test that trying to retrieve an image size that doesn't exist
raises an error raises an error
@ -329,8 +353,64 @@ class TestStore(base.StoreBaseTest,
except exceptions.BadStoreConfiguration: except exceptions.BadStoreConfiguration:
self.fail() 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') @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_image_id = str(uuid.uuid4())
expected_size = FIVE_KB expected_size = FIVE_KB
expected_contents = "*" * expected_size expected_contents = "*" * expected_size
@ -344,6 +424,9 @@ class TestStore(base.StoreBaseTest,
@mock.patch.object(api, 'VMwareAPISession') @mock.patch.object(api, 'VMwareAPISession')
def test_reset_session(self, mock_api_session): 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.store.reset_session(force=False)
self.assertFalse(mock_api_session.called) self.assertFalse(mock_api_session.called)
self.store.reset_session() self.store.reset_session()
@ -353,6 +436,9 @@ class TestStore(base.StoreBaseTest,
@mock.patch.object(api, 'VMwareAPISession') @mock.patch.object(api, 'VMwareAPISession')
def test_build_vim_cookie_header_active(self, mock_api_session): 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 = mock.Mock()
self.store.session.is_current_session_active.return_value = True self.store.session.is_current_session_active.return_value = True
self.store._build_vim_cookie_header(True) self.store._build_vim_cookie_header(True)
@ -360,6 +446,9 @@ class TestStore(base.StoreBaseTest,
@mock.patch.object(api, 'VMwareAPISession') @mock.patch.object(api, 'VMwareAPISession')
def test_build_vim_cookie_header_expired(self, mock_api_session): 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 = mock.Mock()
self.store.session.is_current_session_active.return_value = False self.store.session.is_current_session_active.return_value = False
self.store._build_vim_cookie_header(True) self.store._build_vim_cookie_header(True)
@ -367,13 +456,18 @@ class TestStore(base.StoreBaseTest,
@mock.patch.object(api, 'VMwareAPISession') @mock.patch.object(api, 'VMwareAPISession')
def test_build_vim_cookie_header_expired_noverify(self, mock_api_session): 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 = mock.Mock()
self.store.session.is_current_session_active.return_value = False self.store.session.is_current_session_active.return_value = False
self.store._build_vim_cookie_header() self.store._build_vim_cookie_header()
self.assertFalse(mock_api_session.called) self.assertFalse(mock_api_session.called)
@mock.patch.object(vm_store.Store, 'select_datastore')
@mock.patch.object(api, 'VMwareAPISession') @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_image_id = str(uuid.uuid4())
expected_size = FIVE_KB expected_size = FIVE_KB
expected_contents = "*" * expected_size expected_contents = "*" * expected_size
@ -390,3 +484,120 @@ class TestStore(base.StoreBaseTest,
exp_url = 'scheme://example.com/path?key1=val1%3Fsort%3Dtrue&key2=val2' exp_url = 'scheme://example.com/path?key1=val1%3Fsort%3Dtrue&key2=val2'
self.assertEqual(exp_url, self.assertEqual(exp_url,
utils.sort_url_by_qs_keys(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)