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:
parent
547dc2fcfb
commit
aa10f66ee0
@ -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:
|
||||||
|
@ -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',
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user