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