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