c20778fe6b
vCenter supports different types of datastores such as VMFS, NFS etc. Currently the VMDK driver supports only vSAN, VMFS and NFS datastores for storing volume vmdks. This patch adds a check to skip unsupported datastore types during volume creation. Closes-bug: #1519316 Change-Id: I314abbe0de09bb304859c72b27054784eeef9044
469 lines
20 KiB
Python
469 lines
20 KiB
Python
# Copyright (c) 2014 VMware, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Unit tests for datastore module.
|
|
"""
|
|
|
|
import mock
|
|
from oslo_utils import units
|
|
from oslo_vmware import exceptions
|
|
|
|
from cinder import test
|
|
from cinder.volume.drivers.vmware import datastore as ds_sel
|
|
from cinder.volume.drivers.vmware import exceptions as vmdk_exceptions
|
|
|
|
|
|
class DatastoreTest(test.TestCase):
|
|
"""Unit tests for Datastore."""
|
|
|
|
def setUp(self):
|
|
super(DatastoreTest, self).setUp()
|
|
self._session = mock.Mock()
|
|
self._vops = mock.Mock()
|
|
self._ds_sel = ds_sel.DatastoreSelector(self._vops, self._session)
|
|
|
|
@mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
|
|
def test_get_profile_id(self, get_profile_id_by_name):
|
|
profile_id = mock.sentinel.profile_id
|
|
get_profile_id_by_name.return_value = profile_id
|
|
profile_name = mock.sentinel.profile_name
|
|
|
|
self.assertEqual(profile_id, self._ds_sel.get_profile_id(profile_name))
|
|
get_profile_id_by_name.assert_called_once_with(self._session,
|
|
profile_name)
|
|
|
|
@mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
|
|
def test_get_profile_id_with_invalid_profile(self, get_profile_id_by_name):
|
|
get_profile_id_by_name.return_value = None
|
|
profile_name = mock.sentinel.profile_name
|
|
|
|
self.assertRaises(vmdk_exceptions.ProfileNotFoundException,
|
|
self._ds_sel.get_profile_id,
|
|
profile_name)
|
|
get_profile_id_by_name.assert_called_once_with(self._session,
|
|
profile_name)
|
|
|
|
def _create_datastore(self, moref):
|
|
return mock.Mock(value=moref)
|
|
|
|
def _create_summary(
|
|
self, ds, free_space=units.Mi, _type=ds_sel.DatastoreType.VMFS,
|
|
capacity=2 * units.Mi):
|
|
return mock.Mock(datastore=ds, freeSpace=free_space, type=_type,
|
|
capacity=capacity)
|
|
|
|
def _create_host(self, value):
|
|
host = mock.Mock(spec=['_type', 'value'])
|
|
host._type = 'HostSystem'
|
|
host.value = value
|
|
return host
|
|
|
|
def test_filter_datastores_with_unsupported_type(self):
|
|
ds_1 = self._create_datastore('ds-1')
|
|
ds_2 = self._create_datastore('ds-2')
|
|
datastores = [ds_1, ds_2]
|
|
|
|
self._vops.get_summary.side_effect = [
|
|
self._create_summary(ds_1),
|
|
self._create_summary(ds_2, _type='foo')]
|
|
|
|
res = self._ds_sel._filter_datastores(
|
|
datastores, units.Ki, None, None, None)
|
|
|
|
self.assertEqual(1, len(res))
|
|
self.assertEqual(ds_1, res[0].datastore)
|
|
|
|
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
|
'_filter_by_profile')
|
|
def test_filter_datastores(self, filter_by_profile):
|
|
# Test with empty datastore list.
|
|
datastores = []
|
|
size_bytes = 2 * units.Mi
|
|
profile_id = mock.sentinel.profile_id
|
|
hard_anti_affinity_datastores = None
|
|
hard_affinity_ds_types = None
|
|
|
|
self.assertEqual([], self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types))
|
|
|
|
# Test with single datastore with hard anti-affinity.
|
|
ds_1 = self._create_datastore('ds-1')
|
|
datastores = [ds_1]
|
|
hard_anti_affinity_datastores = [ds_1.value]
|
|
|
|
self.assertEqual([], self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types))
|
|
|
|
# Extend previous case with a profile non-compliant datastore.
|
|
ds_2 = self._create_datastore('ds-2')
|
|
datastores.append(ds_2)
|
|
filter_by_profile.return_value = []
|
|
|
|
self.assertEqual([], self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types))
|
|
filter_by_profile.assert_called_once_with([ds_2], profile_id)
|
|
|
|
# Extend previous case with a less free space datastore.
|
|
ds_3 = self._create_datastore('ds-3')
|
|
datastores.append(ds_3)
|
|
filter_by_profile.return_value = [ds_3]
|
|
|
|
free_space_list = [units.Mi]
|
|
type_list = [ds_sel.DatastoreType.NFS]
|
|
self._vops.get_summary.side_effect = (
|
|
lambda ds: self._create_summary(ds,
|
|
free_space_list.pop(0),
|
|
type_list.pop(0)))
|
|
|
|
self.assertEqual([], self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types))
|
|
|
|
# Extend previous case with a datastore not satisfying hard affinity
|
|
# datastore type requirement.
|
|
ds_4 = self._create_datastore('ds-4')
|
|
datastores.append(ds_4)
|
|
filter_by_profile.return_value = [ds_3, ds_4]
|
|
|
|
free_space_list = [units.Mi, 4 * units.Mi]
|
|
type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN]
|
|
hard_affinity_ds_types = [ds_sel.DatastoreType.NFS]
|
|
|
|
self.assertEqual([], self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types))
|
|
|
|
# Modify the previous case to remove hard affinity datastore type
|
|
# requirement.
|
|
free_space_list = [units.Mi, 4 * units.Mi]
|
|
type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN]
|
|
hard_affinity_ds_types = None
|
|
|
|
res = self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types)
|
|
self.assertTrue(len(res) == 1)
|
|
self.assertEqual(ds_4, res[0].datastore)
|
|
|
|
# Extend the previous case by adding a datastore satisfying
|
|
# hard affinity datastore type requirement.
|
|
ds_5 = self._create_datastore('ds-5')
|
|
datastores.append(ds_5)
|
|
filter_by_profile.return_value = [ds_3, ds_4, ds_5]
|
|
|
|
free_space_list = [units.Mi, 4 * units.Mi, 5 * units.Mi]
|
|
type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN,
|
|
ds_sel.DatastoreType.VMFS]
|
|
hard_affinity_ds_types = [ds_sel.DatastoreType.VMFS]
|
|
|
|
res = self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types)
|
|
self.assertTrue(len(res) == 1)
|
|
self.assertEqual(ds_5, res[0].datastore)
|
|
|
|
# Modify the previous case to have two datastores satisfying
|
|
# hard affinity datastore type requirement.
|
|
free_space_list = [units.Mi, 4 * units.Mi, 5 * units.Mi]
|
|
type_list = [ds_sel.DatastoreType.NFS, ds_sel.DatastoreType.VSAN,
|
|
ds_sel.DatastoreType.VSAN]
|
|
hard_affinity_ds_types = [ds_sel.DatastoreType.VSAN]
|
|
|
|
res = self._ds_sel._filter_datastores(
|
|
datastores, size_bytes, profile_id, hard_anti_affinity_datastores,
|
|
hard_affinity_ds_types)
|
|
self.assertTrue(len(res) == 2)
|
|
self.assertEqual(ds_4, res[0].datastore)
|
|
self.assertEqual(ds_5, res[1].datastore)
|
|
|
|
# Clear side effects.
|
|
self._vops.get_summary.side_effect = None
|
|
|
|
def test_select_best_summary(self):
|
|
# No tie-- all datastores with different host mount count.
|
|
summary_1 = self._create_summary(mock.sentinel.ds_1,
|
|
free_space=units.Mi,
|
|
capacity=2 * units.Mi)
|
|
summary_2 = self._create_summary(mock.sentinel.ds_2,
|
|
free_space=units.Mi,
|
|
capacity=3 * units.Mi)
|
|
summary_3 = self._create_summary(mock.sentinel.ds_3,
|
|
free_space=units.Mi,
|
|
capacity=4 * units.Mi)
|
|
|
|
host_1 = self._create_host('host-1')
|
|
host_2 = self._create_host('host-2')
|
|
host_3 = self._create_host('host-3')
|
|
|
|
connected_hosts = {mock.sentinel.ds_1: [host_1.value],
|
|
mock.sentinel.ds_2: [host_1.value, host_2.value],
|
|
mock.sentinel.ds_3: [host_1.value, host_2.value,
|
|
host_3.value]}
|
|
self._vops.get_connected_hosts.side_effect = (
|
|
lambda summary: connected_hosts[summary])
|
|
|
|
summaries = [summary_1, summary_2, summary_3]
|
|
(best_summary, best_utilization) = self._ds_sel._select_best_summary(
|
|
summaries)
|
|
|
|
self.assertEqual(summary_3, best_summary)
|
|
self.assertEqual(3 / 4.0, best_utilization)
|
|
|
|
# Tie-- two datastores with max host mount count.
|
|
summary_4 = self._create_summary(mock.sentinel.ds_4,
|
|
free_space=2 * units.Mi,
|
|
capacity=4 * units.Mi)
|
|
connected_hosts[mock.sentinel.ds_4] = (
|
|
connected_hosts[mock.sentinel.ds_3])
|
|
summaries.append(summary_4)
|
|
(best_summary, best_utilization) = self._ds_sel._select_best_summary(
|
|
summaries)
|
|
|
|
self.assertEqual(summary_4, best_summary)
|
|
self.assertEqual(1 / 2.0, best_utilization)
|
|
|
|
# Clear side effects.
|
|
self._vops.get_connected_hosts.side_effect = None
|
|
|
|
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
|
'get_profile_id')
|
|
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
|
'_filter_datastores')
|
|
def test_select_datastore(self, filter_datastores, get_profile_id):
|
|
# Test with no hosts.
|
|
size_bytes = units.Ki
|
|
req = {self._ds_sel.SIZE_BYTES: size_bytes}
|
|
self._vops.get_hosts.return_value = mock.Mock(objects=[])
|
|
|
|
self.assertEqual((), self._ds_sel.select_datastore(req))
|
|
self._vops.get_hosts.assert_called_once_with()
|
|
|
|
# Test with single host with no valid datastores.
|
|
host_1 = self._create_host('host-1')
|
|
self._vops.get_hosts.return_value = mock.Mock(
|
|
objects=[mock.Mock(obj=host_1)])
|
|
self._vops.continue_retrieval.return_value = None
|
|
self._vops.get_dss_rp.side_effect = exceptions.VimException('error')
|
|
|
|
self.assertEqual((), self._ds_sel.select_datastore(req))
|
|
self._vops.get_dss_rp.assert_called_once_with(host_1)
|
|
|
|
# Test with three hosts and vCenter connection problem while fetching
|
|
# datastores for the second host.
|
|
self._vops.get_dss_rp.reset_mock()
|
|
host_2 = self._create_host('host-2')
|
|
host_3 = self._create_host('host-3')
|
|
self._vops.get_hosts.return_value = mock.Mock(
|
|
objects=[mock.Mock(obj=host_1),
|
|
mock.Mock(obj=host_2),
|
|
mock.Mock(obj=host_3)])
|
|
self._vops.get_dss_rp.side_effect = [
|
|
exceptions.VimException('no valid datastores'),
|
|
exceptions.VimConnectionException('connection error')]
|
|
|
|
self.assertRaises(exceptions.VimConnectionException,
|
|
self._ds_sel.select_datastore,
|
|
req)
|
|
get_dss_rp_exp_calls = [mock.call(host_1), mock.call(host_2)]
|
|
self.assertEqual(get_dss_rp_exp_calls,
|
|
self._vops.get_dss_rp.call_args_list)
|
|
|
|
# Modify previous case to return datastores for second and third host,
|
|
# where none of them meet the requirements which include a storage
|
|
# profile and affinity requirements.
|
|
aff_ds_types = [ds_sel.DatastoreType.VMFS]
|
|
req[ds_sel.DatastoreSelector.HARD_AFFINITY_DS_TYPE] = aff_ds_types
|
|
|
|
ds_1a = mock.sentinel.ds_1a
|
|
anti_affinity_ds = [ds_1a]
|
|
req[ds_sel.DatastoreSelector.HARD_ANTI_AFFINITY_DS] = anti_affinity_ds
|
|
|
|
profile_name = mock.sentinel.profile_name
|
|
req[ds_sel.DatastoreSelector.PROFILE_NAME] = profile_name
|
|
|
|
profile_id = mock.sentinel.profile_id
|
|
get_profile_id.return_value = profile_id
|
|
|
|
ds_2a = mock.sentinel.ds_2a
|
|
ds_2b = mock.sentinel.ds_2b
|
|
ds_3a = mock.sentinel.ds_3a
|
|
|
|
self._vops.get_dss_rp.reset_mock()
|
|
rp_2 = mock.sentinel.rp_2
|
|
rp_3 = mock.sentinel.rp_3
|
|
self._vops.get_dss_rp.side_effect = [
|
|
exceptions.VimException('no valid datastores'),
|
|
([ds_2a, ds_2b], rp_2),
|
|
([ds_3a], rp_3)]
|
|
|
|
filter_datastores.return_value = []
|
|
|
|
self.assertEqual((), self._ds_sel.select_datastore(req))
|
|
get_profile_id.assert_called_once_with(profile_name)
|
|
get_dss_rp_exp_calls.append(mock.call(host_3))
|
|
self.assertEqual(get_dss_rp_exp_calls,
|
|
self._vops.get_dss_rp.call_args_list)
|
|
filter_datastores_exp_calls = [
|
|
mock.call([ds_2a, ds_2b], size_bytes, profile_id, anti_affinity_ds,
|
|
aff_ds_types),
|
|
mock.call([ds_3a], size_bytes, profile_id, anti_affinity_ds,
|
|
aff_ds_types)]
|
|
self.assertEqual(filter_datastores_exp_calls,
|
|
filter_datastores.call_args_list)
|
|
|
|
# Modify previous case to have a non-empty summary list after filtering
|
|
# with preferred utilization threshold unset.
|
|
self._vops.get_dss_rp.side_effect = [
|
|
exceptions.VimException('no valid datastores'),
|
|
([ds_2a, ds_2b], rp_2),
|
|
([ds_3a], rp_3)]
|
|
|
|
summary_2b = self._create_summary(ds_2b, free_space=0.5 * units.Mi,
|
|
capacity=units.Mi)
|
|
filter_datastores.side_effect = [[summary_2b]]
|
|
self._vops.get_connected_hosts.return_value = [host_1]
|
|
|
|
self.assertEqual((host_2, rp_2, summary_2b),
|
|
self._ds_sel.select_datastore(req))
|
|
|
|
# Modify previous case to have a preferred utilization threshold
|
|
# satsified by one datastore.
|
|
self._vops.get_dss_rp.side_effect = [
|
|
exceptions.VimException('no valid datastores'),
|
|
([ds_2a, ds_2b], rp_2),
|
|
([ds_3a], rp_3)]
|
|
|
|
req[ds_sel.DatastoreSelector.PREF_UTIL_THRESH] = 0.4
|
|
summary_3a = self._create_summary(ds_3a, free_space=0.7 * units.Mi,
|
|
capacity=units.Mi)
|
|
filter_datastores.side_effect = [[summary_2b], [summary_3a]]
|
|
|
|
self.assertEqual((host_3, rp_3, summary_3a),
|
|
self._ds_sel.select_datastore(req))
|
|
|
|
# Modify previous case to have a preferred utilization threshold
|
|
# which cannot be satisfied.
|
|
self._vops.get_dss_rp.side_effect = [
|
|
exceptions.VimException('no valid datastores'),
|
|
([ds_2a, ds_2b], rp_2),
|
|
([ds_3a], rp_3)]
|
|
filter_datastores.side_effect = [[summary_2b], [summary_3a]]
|
|
|
|
req[ds_sel.DatastoreSelector.PREF_UTIL_THRESH] = 0.2
|
|
summary_2b.freeSpace = 0.75 * units.Mi
|
|
|
|
self.assertEqual((host_2, rp_2, summary_2b),
|
|
self._ds_sel.select_datastore(req))
|
|
|
|
# Clear side effects.
|
|
self._vops.get_dss_rp.side_effect = None
|
|
|
|
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
|
'_filter_datastores')
|
|
def test_select_datastore_with_single_host(self, filter_datastores):
|
|
host = self._create_host('host-1')
|
|
req = {self._ds_sel.SIZE_BYTES: units.Gi}
|
|
|
|
ds = mock.sentinel.ds
|
|
rp = mock.sentinel.rp
|
|
self._vops.get_dss_rp.return_value = ([ds], rp)
|
|
|
|
summary = self._create_summary(ds, free_space=2 * units.Gi,
|
|
capacity=3 * units.Gi)
|
|
filter_datastores.return_value = [summary]
|
|
self._vops.get_connected_hosts.return_value = [host.value]
|
|
|
|
self.assertEqual((host, rp, summary),
|
|
self._ds_sel.select_datastore(req, [host]))
|
|
|
|
# reset mocks
|
|
self._vops.get_dss_rp.reset_mock()
|
|
self._vops.get_dss_rp.return_value = None
|
|
self._vops.get_connected_hosts.reset_mock()
|
|
self._vops.get_connected_hosts.return_value = None
|
|
|
|
def test_select_datastore_with_empty_host_list(self):
|
|
size_bytes = units.Ki
|
|
req = {self._ds_sel.SIZE_BYTES: size_bytes}
|
|
self._vops.get_hosts.return_value = mock.Mock(objects=[])
|
|
|
|
self.assertEqual((), self._ds_sel.select_datastore(req, hosts=[]))
|
|
self._vops.get_hosts.assert_called_once_with()
|
|
|
|
@mock.patch('oslo_vmware.pbm.get_profile_id_by_name')
|
|
@mock.patch('cinder.volume.drivers.vmware.datastore.DatastoreSelector.'
|
|
'_filter_by_profile')
|
|
def test_is_datastore_compliant(self, filter_by_profile,
|
|
get_profile_id_by_name):
|
|
# Test with empty profile.
|
|
profile_name = None
|
|
datastore = mock.sentinel.datastore
|
|
self.assertTrue(self._ds_sel.is_datastore_compliant(datastore,
|
|
profile_name))
|
|
|
|
# Test with invalid profile.
|
|
profile_name = mock.sentinel.profile_name
|
|
get_profile_id_by_name.return_value = None
|
|
self.assertRaises(vmdk_exceptions.ProfileNotFoundException,
|
|
self._ds_sel.is_datastore_compliant,
|
|
datastore,
|
|
profile_name)
|
|
get_profile_id_by_name.assert_called_once_with(self._session,
|
|
profile_name)
|
|
|
|
# Test with valid profile and non-compliant datastore.
|
|
get_profile_id_by_name.reset_mock()
|
|
profile_id = mock.sentinel.profile_id
|
|
get_profile_id_by_name.return_value = profile_id
|
|
filter_by_profile.return_value = []
|
|
self.assertFalse(self._ds_sel.is_datastore_compliant(datastore,
|
|
profile_name))
|
|
get_profile_id_by_name.assert_called_once_with(self._session,
|
|
profile_name)
|
|
filter_by_profile.assert_called_once_with([datastore], profile_id)
|
|
|
|
# Test with valid profile and compliant datastore.
|
|
get_profile_id_by_name.reset_mock()
|
|
filter_by_profile.reset_mock()
|
|
filter_by_profile.return_value = [datastore]
|
|
self.assertTrue(self._ds_sel.is_datastore_compliant(datastore,
|
|
profile_name))
|
|
get_profile_id_by_name.assert_called_once_with(self._session,
|
|
profile_name)
|
|
filter_by_profile.assert_called_once_with([datastore], profile_id)
|
|
|
|
def test_get_all_hosts(self):
|
|
host_1 = self._create_host('host-1')
|
|
host_2 = self._create_host('host-2')
|
|
hosts = mock.Mock(objects=[mock.Mock(obj=host_1),
|
|
mock.Mock(obj=host_2)])
|
|
|
|
self._vops.get_hosts.return_value = hosts
|
|
self._vops.continue_retrieval.return_value = None
|
|
# host_1 is usable and host_2 is not usable
|
|
self._vops.is_host_usable.side_effect = [True, False]
|
|
|
|
ret = self._ds_sel._get_all_hosts()
|
|
|
|
self.assertEqual([host_1], ret)
|
|
self._vops.get_hosts.assert_called_once_with()
|
|
self._vops.continue_retrieval.assert_called_once_with(hosts)
|
|
exp_calls = [mock.call(host_1), mock.call(host_2)]
|
|
self.assertEqual(exp_calls, self._vops.is_host_usable.call_args_list)
|