1# Copyright (c) 2014 VMware, Inc. 2# All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); you may 5# not use this file except in compliance with the License. You may obtain 6# a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13# License for the specific language governing permissions and limitations 14# under the License. 15 16""" 17Classes and utility methods for datastore selection. 18""" 19 20import random 21 22from oslo_log import log as logging 23from oslo_vmware import pbm 24from oslo_vmware import vim_util 25 26from cinder import coordination 27from cinder.volume.drivers.vmware import exceptions as vmdk_exceptions 28 29 30LOG = logging.getLogger(__name__) 31 32 33class DatastoreType(object): 34 """Supported datastore types.""" 35 36 NFS = "nfs" 37 VMFS = "vmfs" 38 VSAN = "vsan" 39 VVOL = "vvol" 40 41 _ALL_TYPES = {NFS, VMFS, VSAN, VVOL} 42 43 @staticmethod 44 def get_all_types(): 45 return DatastoreType._ALL_TYPES 46 47 48class DatastoreSelector(object): 49 """Class for selecting datastores which satisfy input requirements.""" 50 51 HARD_AFFINITY_DS_TYPE = "hardAffinityDatastoreTypes" 52 HARD_ANTI_AFFINITY_DS = "hardAntiAffinityDatastores" 53 SIZE_BYTES = "sizeBytes" 54 PROFILE_NAME = "storageProfileName" 55 56 # TODO(vbala) Remove dependency on volumeops. 57 def __init__(self, vops, session, max_objects): 58 self._vops = vops 59 self._session = session 60 self._max_objects = max_objects 61 self._profile_id_cache = {} 62 63 @coordination.synchronized('vmware-datastore-profile-{profile_name}') 64 def get_profile_id(self, profile_name): 65 """Get vCenter profile ID for the given profile name. 66 67 :param profile_name: profile name 68 :return: vCenter profile ID 69 :raises ProfileNotFoundException: 70 """ 71 if profile_name in self._profile_id_cache: 72 LOG.debug("Returning cached ID for profile: %s.", profile_name) 73 return self._profile_id_cache[profile_name] 74 75 profile_id = pbm.get_profile_id_by_name(self._session, profile_name) 76 if profile_id is None: 77 LOG.error("Storage profile: %s cannot be found in vCenter.", 78 profile_name) 79 raise vmdk_exceptions.ProfileNotFoundException( 80 storage_profile=profile_name) 81 82 self._profile_id_cache[profile_name] = profile_id 83 LOG.debug("Storage profile: %(name)s resolved to vCenter profile ID: " 84 "%(id)s.", 85 {'name': profile_name, 86 'id': profile_id}) 87 return profile_id 88 89 def _filter_by_profile(self, datastores, profile_id): 90 """Filter out input datastores that do not match the given profile.""" 91 cf = self._session.pbm.client.factory 92 hubs = pbm.convert_datastores_to_hubs(cf, datastores) 93 hubs = pbm.filter_hubs_by_profile(self._session, hubs, profile_id) 94 hub_ids = [hub.hubId for hub in hubs] 95 return {k: v for k, v in datastores.items() if k.value in hub_ids} 96 97 def _filter_datastores(self, 98 datastores, 99 size_bytes, 100 profile_id, 101 hard_anti_affinity_ds, 102 hard_affinity_ds_types, 103 valid_host_refs=None): 104 105 if not datastores: 106 return 107 108 def _is_valid_ds_type(summary): 109 ds_type = summary.type.lower() 110 return (ds_type in DatastoreType.get_all_types() and 111 (hard_affinity_ds_types is None or 112 ds_type in hard_affinity_ds_types)) 113 114 def _is_ds_usable(summary): 115 return summary.accessible and not self._vops._in_maintenance( 116 summary) 117 118 valid_host_refs = valid_host_refs or [] 119 valid_hosts = [host_ref.value for host_ref in valid_host_refs] 120 121 def _is_ds_accessible_to_valid_host(host_mounts): 122 for host_mount in host_mounts: 123 if host_mount.key.value in valid_hosts: 124 return True 125 126 def _is_ds_valid(ds_ref, ds_props): 127 summary = ds_props.get('summary') 128 host_mounts = ds_props.get('host') 129 if (summary is None or host_mounts is None): 130 return False 131 132 if (hard_anti_affinity_ds and 133 ds_ref.value in hard_anti_affinity_ds): 134 return False 135 136 if summary.freeSpace < size_bytes: 137 return False 138 139 if (valid_hosts and 140 not _is_ds_accessible_to_valid_host(host_mounts)): 141 return False 142 143 return _is_valid_ds_type(summary) and _is_ds_usable(summary) 144 145 datastores = {k: v for k, v in datastores.items() 146 if _is_ds_valid(k, v)} 147 148 if datastores and profile_id: 149 datastores = self._filter_by_profile(datastores, profile_id) 150 151 return datastores 152 153 def _get_object_properties(self, obj_content): 154 props = {} 155 if hasattr(obj_content, 'propSet'): 156 prop_set = obj_content.propSet 157 if prop_set: 158 props = {prop.name: prop.val for prop in prop_set} 159 return props 160 161 def _get_datastores(self): 162 datastores = {} 163 retrieve_result = self._session.invoke_api( 164 vim_util, 165 'get_objects', 166 self._session.vim, 167 'Datastore', 168 self._max_objects, 169 properties_to_collect=['host', 'summary']) 170 171 while retrieve_result: 172 if retrieve_result.objects: 173 for obj_content in retrieve_result.objects: 174 props = self._get_object_properties(obj_content) 175 if ('host' in props and 176 hasattr(props['host'], 'DatastoreHostMount')): 177 props['host'] = props['host'].DatastoreHostMount 178 datastores[obj_content.obj] = props 179 retrieve_result = self._session.invoke_api(vim_util, 180 'continue_retrieval', 181 self._session.vim, 182 retrieve_result) 183 184 return datastores 185 186 def _get_host_properties(self, host_ref): 187 retrieve_result = self._session.invoke_api(vim_util, 188 'get_object_properties', 189 self._session.vim, 190 host_ref, 191 ['runtime', 'parent']) 192 193 if retrieve_result: 194 return self._get_object_properties(retrieve_result[0]) 195 196 def _get_resource_pool(self, cluster_ref): 197 return self._session.invoke_api(vim_util, 198 'get_object_property', 199 self._session.vim, 200 cluster_ref, 201 'resourcePool') 202 203 def _select_best_datastore(self, datastores, valid_host_refs=None): 204 205 if not datastores: 206 return 207 208 def _sort_key(ds_props): 209 host = ds_props.get('host') 210 summary = ds_props.get('summary') 211 space_utilization = (1.0 - 212 (summary.freeSpace / float(summary.capacity))) 213 return (-len(host), space_utilization) 214 215 host_prop_map = {} 216 217 def _is_host_usable(host_ref): 218 props = host_prop_map.get(host_ref.value) 219 if props is None: 220 props = self._get_host_properties(host_ref) 221 host_prop_map[host_ref.value] = props 222 223 runtime = props.get('runtime') 224 parent = props.get('parent') 225 if runtime and parent: 226 return (runtime.connectionState == 'connected' and 227 not runtime.inMaintenanceMode) 228 else: 229 return False 230 231 valid_host_refs = valid_host_refs or [] 232 valid_hosts = [host_ref.value for host_ref in valid_host_refs] 233 234 def _select_host(host_mounts): 235 random.shuffle(host_mounts) 236 for host_mount in host_mounts: 237 if valid_hosts and host_mount.key.value not in valid_hosts: 238 continue 239 if (self._vops._is_usable(host_mount.mountInfo) and 240 _is_host_usable(host_mount.key)): 241 return host_mount.key 242 243 sorted_ds_props = sorted(datastores.values(), key=_sort_key) 244 for ds_props in sorted_ds_props: 245 host_ref = _select_host(ds_props['host']) 246 if host_ref: 247 rp = self._get_resource_pool( 248 host_prop_map[host_ref.value]['parent']) 249 return (host_ref, rp, ds_props['summary']) 250 251 def select_datastore(self, req, hosts=None): 252 """Selects a datastore satisfying the given requirements. 253 254 A datastore which is connected to maximum number of hosts is 255 selected. Ties if any are broken based on space utilization-- 256 datastore with least space utilization is preferred. It returns 257 the selected datastore's summary along with a host and resource 258 pool where the volume can be created. 259 260 :param req: selection requirements 261 :param hosts: list of hosts to consider 262 :return: (host, resourcePool, summary) 263 """ 264 LOG.debug("Using requirements: %s for datastore selection.", req) 265 266 hard_affinity_ds_types = req.get( 267 DatastoreSelector.HARD_AFFINITY_DS_TYPE) 268 hard_anti_affinity_datastores = req.get( 269 DatastoreSelector.HARD_ANTI_AFFINITY_DS) 270 size_bytes = req[DatastoreSelector.SIZE_BYTES] 271 profile_name = req.get(DatastoreSelector.PROFILE_NAME) 272 273 profile_id = None 274 if profile_name is not None: 275 profile_id = self.get_profile_id(profile_name) 276 277 datastores = self._get_datastores() 278 datastores = self._filter_datastores(datastores, 279 size_bytes, 280 profile_id, 281 hard_anti_affinity_datastores, 282 hard_affinity_ds_types, 283 valid_host_refs=hosts) 284 res = self._select_best_datastore(datastores, valid_host_refs=hosts) 285 LOG.debug("Selected (host, resourcepool, datastore): %s", res) 286 return res 287 288 def is_datastore_compliant(self, datastore, profile_name): 289 """Check if the datastore is compliant with given profile. 290 291 :param datastore: datastore to check the compliance 292 :param profile_name: profile to check the compliance against 293 :return: True if the datastore is compliant; False otherwise 294 :raises ProfileNotFoundException: 295 """ 296 LOG.debug("Checking datastore: %(datastore)s compliance against " 297 "profile: %(profile)s.", 298 {'datastore': datastore, 299 'profile': profile_name}) 300 if profile_name is None: 301 # Any datastore is trivially compliant with a None profile. 302 return True 303 304 profile_id = self.get_profile_id(profile_name) 305 # _filter_by_profile expects a map of datastore references to its 306 # properties. It only uses the properties to construct a map of 307 # filtered datastores to its properties. Here we don't care about 308 # the datastore property, so pass it as None. 309 is_compliant = bool(self._filter_by_profile({datastore: None}, 310 profile_id)) 311 LOG.debug("Compliance is %(is_compliant)s for datastore: " 312 "%(datastore)s against profile: %(profile)s.", 313 {'is_compliant': is_compliant, 314 'datastore': datastore, 315 'profile': profile_name}) 316 return is_compliant 317