1# Copyright (c) 2014 VMware, Inc. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may 4# not use this file except in compliance with the License. You may obtain 5# a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations 13# under the License. 14 15import logging 16import posixpath 17import random 18import re 19 20import http.client as httplib 21import urllib.parse as urlparse 22 23from oslo_vmware._i18n import _ 24from oslo_vmware import constants 25from oslo_vmware import exceptions 26from oslo_vmware import vim_util 27 28LOG = logging.getLogger(__name__) 29 30 31def get_datastore_by_ref(session, ds_ref): 32 """Returns a datastore object for a given reference. 33 34 :param session: a vmware api session object 35 :param ds_ref: managed object reference of a datastore 36 :rtype: a datastore object 37 """ 38 lst_properties = ["summary.type", 39 "summary.name", 40 "summary.capacity", 41 "summary.freeSpace", 42 "summary.uncommitted"] 43 44 props = session.invoke_api( 45 vim_util, 46 "get_object_properties_dict", 47 session.vim, 48 ds_ref, 49 lst_properties) 50 # TODO(sabari): Instantiate with datacenter info. 51 return Datastore(ds_ref, props["summary.name"], 52 capacity=props.get("summary.capacity"), 53 freespace=props.get("summary.freeSpace"), 54 uncommitted=props.get("summary.uncommitted"), 55 type=props.get("summary.type")) 56 57 58def get_recommended_datastore(session, sp_spec): 59 spr = session.invoke_api( 60 session.vim, 61 "RecommendDatastores", 62 session.vim.service_content.storageResourceManager, 63 storageSpec=sp_spec) 64 if not hasattr(spr, 'recommendations'): 65 LOG.error("Unable to find suitable datastore") 66 return 67 return spr.recommendations[0].key 68 69 70def get_recommended_datastore_clone(session, 71 dsc_ref, 72 clone_spec, 73 vm_ref, 74 folder, 75 name, 76 resource_pool=None, 77 host_ref=None): 78 """Returns a key which identifies the most recommended datastore from the 79 specified datastore cluster where the specified VM can be cloned to. 80 """ 81 sp_spec = vim_util.storage_placement_spec(session.vim.client.factory, 82 dsc_ref, 83 'clone', 84 clone_spec=clone_spec, 85 vm_ref=vm_ref, 86 folder=folder, 87 clone_name=name, 88 res_pool_ref=resource_pool, 89 host_ref=host_ref) 90 return get_recommended_datastore(session, sp_spec) 91 92 93def get_recommended_datastore_create(session, 94 dsc_ref, 95 config_spec, 96 resource_pool, 97 folder, 98 host_ref=None): 99 """Returns SDRS recommendation key for creating a VM.""" 100 sp_spec = vim_util.storage_placement_spec(session.vim.client.factory, 101 dsc_ref, 102 'create', 103 config_spec=config_spec, 104 folder=folder, 105 res_pool_ref=resource_pool, 106 host_ref=host_ref) 107 return get_recommended_datastore(session, sp_spec) 108 109 110def get_dsc_ref_and_name(session, dsc_val): 111 """Return reference and name of the specified datastore cluster. 112 113 :param ds_val: datastore cluster name or datastore cluster moid 114 :return: tuple of dastastore cluster moref and datastore cluster name 115 """ 116 if re.match(r"group-p\d+", dsc_val): 117 # the configured value is moid 118 dsc_ref = vim_util.get_moref(dsc_val, 'StoragePod') 119 try: 120 dsc_name = session.invoke_api(vim_util, 'get_object_property', 121 session.vim, dsc_ref, 'name') 122 return dsc_ref, dsc_name 123 except exceptions.ManagedObjectNotFoundException: 124 # not a moid, try as a datastore cluster name 125 pass 126 127 result = session.invoke_api(vim_util, 'get_objects', session.vim, 128 'StoragePod', 100, ['name']) 129 with vim_util.WithRetrieval(session.vim, result) as objs: 130 for obj in objs: 131 if not hasattr(obj, 'propSet'): 132 continue 133 if obj.propSet[0].val == dsc_val: 134 return obj.obj, dsc_val 135 return None, None 136 137 138def sdrs_enabled(session, dsc_ref): 139 """Check if Storage DRS is enabled for the given datastore cluster. 140 141 :param session: VMwareAPISession object 142 :param dsc_ref: datastore cluster moref 143 """ 144 pod_sdrs_entry = session.invoke_api(vim_util, 145 'get_object_property', 146 session.vim, 147 dsc_ref, 148 'podStorageDrsEntry') 149 return pod_sdrs_entry.storageDrsConfig.podConfig.enabled 150 151 152class Datastore(object): 153 154 def __init__(self, ref, name, capacity=None, freespace=None, 155 uncommitted=None, type=None, datacenter=None): 156 """Datastore object holds ref and name together for convenience. 157 158 :param ref: a vSphere reference to a datastore 159 :param name: vSphere unique name for this datastore 160 :param capacity: (optional) capacity in bytes of this datastore 161 :param freespace: (optional) free space in bytes of datastore 162 :param uncommitted: (optional) Total additional storage space 163 in bytes of datastore 164 :param type: (optional) datastore type 165 :param datacenter: (optional) oslo_vmware Datacenter object 166 """ 167 if name is None: 168 raise ValueError(_("Datastore name cannot be None")) 169 if ref is None: 170 raise ValueError(_("Datastore reference cannot be None")) 171 if freespace is not None and capacity is None: 172 raise ValueError(_("Invalid capacity")) 173 if capacity is not None and freespace is not None: 174 if capacity < freespace: 175 raise ValueError(_("Capacity is smaller than free space")) 176 177 self.ref = ref 178 self.name = name 179 self.capacity = capacity 180 self.freespace = freespace 181 self.uncommitted = uncommitted 182 self.type = type 183 self.datacenter = datacenter 184 185 def build_path(self, *paths): 186 """Constructs and returns a DatastorePath. 187 188 :param paths: list of path components, for constructing a path relative 189 to the root directory of the datastore 190 :return: a DatastorePath object 191 """ 192 return DatastorePath(self.name, *paths) 193 194 def build_url(self, scheme, server, rel_path, datacenter_name=None): 195 """Constructs and returns a DatastoreURL. 196 197 :param scheme: scheme of the URL (http, https). 198 :param server: hostname or ip 199 :param rel_path: relative path of the file on the datastore 200 :param datacenter_name: (optional) datacenter name 201 :return: a DatastoreURL object 202 """ 203 if self.datacenter is None and datacenter_name is None: 204 raise ValueError(_("datacenter must be set to build url")) 205 if datacenter_name is None: 206 datacenter_name = self.datacenter.name 207 return DatastoreURL(scheme, server, rel_path, datacenter_name, 208 self.name) 209 210 def __str__(self): 211 return '[%s]' % self.name 212 213 def get_summary(self, session): 214 """Get datastore summary. 215 216 :param datastore: Reference to the datastore 217 :return: 'summary' property of the datastore 218 """ 219 return session.invoke_api(vim_util, 'get_object_property', 220 session.vim, self.ref, 'summary') 221 222 def get_connected_hosts(self, session): 223 """Get a list of usable (accessible, mounted, read-writable) hosts where 224 the datastore is mounted. 225 226 :param: session: session 227 :return: list of HostSystem managed object references 228 """ 229 hosts = [] 230 summary = self.get_summary(session) 231 if not summary.accessible: 232 return hosts 233 host_mounts = session.invoke_api(vim_util, 'get_object_property', 234 session.vim, self.ref, 'host') 235 if not hasattr(host_mounts, 'DatastoreHostMount'): 236 return hosts 237 for host_mount in host_mounts.DatastoreHostMount: 238 if self.is_datastore_mount_usable(host_mount.mountInfo): 239 hosts.append(host_mount.key) 240 connectables = [] 241 if hosts: 242 host_runtimes = session.invoke_api( 243 vim_util, 244 'get_properties_for_a_collection_of_objects', 245 session.vim, 'HostSystem', hosts, ['runtime']) 246 for host_object in host_runtimes.objects: 247 host_props = vim_util.propset_dict(host_object.propSet) 248 host_runtime = host_props.get('runtime') 249 if hasattr(host_runtime, 'inMaintenanceMode') and ( 250 not host_runtime.inMaintenanceMode): 251 connectables.append(host_object.obj) 252 return connectables 253 254 @staticmethod 255 def is_datastore_mount_usable(mount_info): 256 """Check if a datastore is usable as per the given mount info. 257 258 The datastore is considered to be usable for a host only if it is 259 writable, mounted and accessible. 260 261 :param mount_info: HostMountInfo data object 262 :return: True if datastore is usable 263 """ 264 writable = mount_info.accessMode == 'readWrite' 265 mounted = getattr(mount_info, 'mounted', True) 266 accessible = getattr(mount_info, 'accessible', False) 267 268 return writable and mounted and accessible 269 270 @staticmethod 271 def choose_host(hosts): 272 if not hosts: 273 return None 274 275 i = random.SystemRandom().randrange(0, len(hosts)) 276 return hosts[i] 277 278 279class DatastorePath(object): 280 281 """Class for representing a directory or file path in a vSphere datatore. 282 283 This provides various helper methods to access components and useful 284 variants of the datastore path. 285 286 Example usage: 287 288 DatastorePath("datastore1", "_base/foo", "foo.vmdk") creates an 289 object that describes the "[datastore1] _base/foo/foo.vmdk" datastore 290 file path to a virtual disk. 291 292 Note: 293 294 - Datastore path representations always uses forward slash as separator 295 (hence the use of the posixpath module). 296 - Datastore names are enclosed in square brackets. 297 - Path part of datastore path is relative to the root directory 298 of the datastore, and is always separated from the [ds_name] part with 299 a single space. 300 """ 301 302 def __init__(self, datastore_name, *paths): 303 if datastore_name is None or datastore_name == '': 304 raise ValueError(_("Datastore name cannot be empty")) 305 self._datastore_name = datastore_name 306 self._rel_path = '' 307 if paths: 308 if None in paths: 309 raise ValueError(_("Path component cannot be None")) 310 self._rel_path = posixpath.join(*paths) 311 312 def __str__(self): 313 """Full datastore path to the file or directory.""" 314 if self._rel_path != '': 315 return "[%s] %s" % (self._datastore_name, self.rel_path) 316 return "[%s]" % self._datastore_name 317 318 @property 319 def datastore(self): 320 return self._datastore_name 321 322 @property 323 def parent(self): 324 return DatastorePath(self.datastore, posixpath.dirname(self._rel_path)) 325 326 @property 327 def basename(self): 328 return posixpath.basename(self._rel_path) 329 330 @property 331 def dirname(self): 332 return posixpath.dirname(self._rel_path) 333 334 @property 335 def rel_path(self): 336 return self._rel_path 337 338 def join(self, *paths): 339 """Join one or more path components intelligently into a datastore path. 340 341 If any component is an absolute path, all previous components are 342 thrown away, and joining continues. The return value is the 343 concatenation of the paths with exactly one slash ('/') inserted 344 between components, unless p is empty. 345 346 :return: A datastore path 347 """ 348 if paths: 349 if None in paths: 350 raise ValueError(_("Path component cannot be None")) 351 return DatastorePath(self.datastore, self._rel_path, *paths) 352 return self 353 354 def __eq__(self, other): 355 return (isinstance(other, DatastorePath) and 356 self._datastore_name == other._datastore_name and 357 self._rel_path == other._rel_path) 358 359 @classmethod 360 def parse(cls, datastore_path): 361 """Constructs a DatastorePath object given a datastore path string.""" 362 if not datastore_path: 363 raise ValueError(_("Datastore path cannot be empty")) 364 365 spl = datastore_path.split('[', 1)[1].split(']', 1) 366 path = "" 367 if len(spl) == 1: 368 datastore_name = spl[0] 369 else: 370 datastore_name, path = spl 371 return cls(datastore_name, path.strip()) 372 373 374class DatastoreURL(object): 375 376 """Class for representing a URL to HTTP access a file in a datastore. 377 378 This provides various helper methods to access components and useful 379 variants of the datastore URL. 380 """ 381 382 def __init__(self, scheme, server, path, datacenter_path, datastore_name): 383 self._scheme = scheme 384 self._server = server 385 self._path = path 386 self._datacenter_path = datacenter_path 387 self._datastore_name = datastore_name 388 params = {'dcPath': self._datacenter_path, 389 'dsName': self._datastore_name} 390 self._query = urlparse.urlencode(params) 391 392 @classmethod 393 def urlparse(cls, url): 394 scheme, server, path, params, query, fragment = urlparse.urlparse(url) 395 if not query: 396 path = path.split('?') 397 query = path[1] 398 path = path[0] 399 params = urlparse.parse_qs(query) 400 dc_path = params.get('dcPath') 401 if dc_path is not None and len(dc_path) > 0: 402 datacenter_path = dc_path[0] 403 ds_name = params.get('dsName') 404 if ds_name is not None and len(ds_name) > 0: 405 datastore_name = ds_name[0] 406 path = path[len('/folder'):] 407 return cls(scheme, server, path, datacenter_path, datastore_name) 408 409 @property 410 def path(self): 411 return self._path.strip('/') 412 413 @property 414 def datacenter_path(self): 415 return self._datacenter_path 416 417 @property 418 def datastore_name(self): 419 return self._datastore_name 420 421 def __str__(self): 422 return '%s://%s/folder/%s?%s' % (self._scheme, self._server, 423 self.path, self._query) 424 425 def connect(self, method, content_length, cookie): 426 try: 427 if self._scheme == 'http': 428 conn = httplib.HTTPConnection(self._server) 429 elif self._scheme == 'https': 430 # TODO(browne): This needs to be changed to use python requests 431 conn = httplib.HTTPSConnection(self._server) # nosec 432 else: 433 excep_msg = _("Invalid scheme: %s.") % self._scheme 434 LOG.error(excep_msg) 435 raise ValueError(excep_msg) 436 conn.putrequest(method, '/folder/%s?%s' % (self.path, self._query)) 437 conn.putheader('User-Agent', constants.USER_AGENT) 438 conn.putheader('Content-Length', content_length) 439 conn.putheader('Cookie', cookie) 440 conn.endheaders() 441 LOG.debug("Created HTTP connection to transfer the file with " 442 "URL = %s.", str(self)) 443 return conn 444 except (httplib.InvalidURL, httplib.CannotSendRequest, 445 httplib.CannotSendHeader) as excep: 446 excep_msg = _("Error occurred while creating HTTP connection " 447 "to write to file with URL = %s.") % str(self) 448 LOG.exception(excep_msg) 449 raise exceptions.VimConnectionException(excep_msg, excep) 450 451 def get_transfer_ticket(self, session, method): 452 client_factory = session.vim.client.factory 453 spec = vim_util.get_http_service_request_spec(client_factory, method, 454 str(self)) 455 ticket = session.invoke_api( 456 session.vim, 457 'AcquireGenericServiceTicket', 458 session.vim.service_content.sessionManager, 459 spec=spec) 460 return '%s="%s"' % (constants.CGI_COOKIE_KEY, ticket.id) 461