1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5#      http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13from collections import defaultdict
14import itertools
15import logging
16
17from django.utils.translation import ugettext_lazy as _
18
19from horizon import exceptions
20from horizon.utils.memoized import memoized
21
22from openstack_dashboard.api import base
23from openstack_dashboard.api import cinder
24from openstack_dashboard.api import neutron
25from openstack_dashboard.api import nova
26from openstack_dashboard.contrib.developer.profiler import api as profiler
27from openstack_dashboard.utils import futurist_utils
28
29
30LOG = logging.getLogger(__name__)
31
32
33NOVA_COMPUTE_QUOTA_FIELDS = {
34    "metadata_items",
35    "cores",
36    "instances",
37    "injected_files",
38    "injected_file_content_bytes",
39    "injected_file_path_bytes",
40    "ram",
41    "key_pairs",
42    "server_groups",
43    "server_group_members",
44}
45
46# We no longer supports nova-network, so network related quotas from nova
47# are not considered.
48NOVA_QUOTA_FIELDS = NOVA_COMPUTE_QUOTA_FIELDS
49
50NOVA_QUOTA_LIMIT_MAP = {
51    'instances': {
52        'limit': 'maxTotalInstances',
53        'usage': 'totalInstancesUsed'
54    },
55    'cores': {
56        'limit': 'maxTotalCores',
57        'usage': 'totalCoresUsed'
58    },
59    'ram': {
60        'limit': 'maxTotalRAMSize',
61        'usage': 'totalRAMUsed'
62    },
63    'key_pairs': {
64        'limit': 'maxTotalKeypairs',
65        'usage': None
66    },
67}
68
69CINDER_QUOTA_FIELDS = {"volumes",
70                       "snapshots",
71                       "gigabytes"}
72
73CINDER_QUOTA_LIMIT_MAP = {
74    'volumes': {'usage': 'totalVolumesUsed',
75                'limit': 'maxTotalVolumes'},
76    'gigabytes': {'usage': 'totalGigabytesUsed',
77                  'limit': 'maxTotalVolumeGigabytes'},
78    'snapshots': {'usage': 'totalSnapshotsUsed',
79                  'limit': 'maxTotalSnapshots'},
80}
81
82NEUTRON_QUOTA_FIELDS = {"network",
83                        "subnet",
84                        "port",
85                        "router",
86                        "floatingip",
87                        "security_group",
88                        "security_group_rule",
89                        }
90
91QUOTA_FIELDS = NOVA_QUOTA_FIELDS | CINDER_QUOTA_FIELDS | NEUTRON_QUOTA_FIELDS
92
93QUOTA_NAMES = {
94    # nova
95    "metadata_items": _('Metadata Items'),
96    "cores": _('VCPUs'),
97    "instances": _('Instances'),
98    "injected_files": _('Injected Files'),
99    "injected_file_content_bytes": _('Injected File Content Bytes'),
100    "ram": _('RAM (MB)'),
101    "key_pairs": _('Key Pairs'),
102    "injected_file_path_bytes": _('Injected File Path Bytes'),
103    # cinder
104    "volumes": _('Volumes'),
105    "snapshots": _('Volume Snapshots'),
106    "gigabytes": _('Total Size of Volumes and Snapshots (GB)'),
107    # neutron
108    "network": _("Networks"),
109    "subnet": _("Subnets"),
110    "port": _("Ports"),
111    "router": _("Routers"),
112    "floatingip": _('Floating IPs'),
113    "security_group": _("Security Groups"),
114    "security_group_rule": _("Security Group Rules")
115}
116
117
118class QuotaUsage(dict):
119    """Tracks quota limit, used, and available for a given set of quotas."""
120
121    def __init__(self):
122        self.usages = defaultdict(dict)
123
124    def __contains__(self, key):
125        return key in self.usages
126
127    def __getitem__(self, key):
128        return self.usages[key]
129
130    def __setitem__(self, key, value):
131        raise NotImplementedError("Directly setting QuotaUsage values is not "
132                                  "supported. Please use the add_quota and "
133                                  "tally methods.")
134
135    def __repr__(self):
136        return repr(dict(self.usages))
137
138    def __bool__(self):
139        return bool(self.usages)
140
141    def get(self, key, default=None):
142        return self.usages.get(key, default)
143
144    def add_quota(self, quota):
145        """Adds an internal tracking reference for the given quota."""
146        if quota.limit in (None, -1, float('inf')):
147            # Handle "unlimited" quotas.
148            self.usages[quota.name]['quota'] = float("inf")
149            self.usages[quota.name]['available'] = float("inf")
150        else:
151            self.usages[quota.name]['quota'] = int(quota.limit)
152
153    def tally(self, name, value):
154        """Adds to the "used" metric for the given quota."""
155        value = value or 0  # Protection against None.
156        # Start at 0 if this is the first value.
157        if 'used' not in self.usages[name]:
158            self.usages[name]['used'] = 0
159        # Increment our usage and update the "available" metric.
160        self.usages[name]['used'] += int(value)  # Fail if can't coerce to int.
161        self.update_available(name)
162
163    def update_available(self, name):
164        """Updates the "available" metric for the given quota."""
165        quota = self.usages.get(name, {}).get('quota', float('inf'))
166        available = quota - self.usages[name]['used']
167        if available < 0:
168            available = 0
169        self.usages[name]['available'] = available
170
171
172@profiler.trace
173def get_default_quota_data(request, disabled_quotas=None, tenant_id=None):
174    quotasets = []
175    if not tenant_id:
176        tenant_id = request.user.tenant_id
177    if disabled_quotas is None:
178        disabled_quotas = get_disabled_quotas(request)
179
180    if NOVA_QUOTA_FIELDS - disabled_quotas:
181        try:
182            quotasets.append(nova.default_quota_get(request, tenant_id))
183        except Exception:
184            disabled_quotas.update(NOVA_QUOTA_FIELDS)
185            msg = _('Unable to retrieve Nova quota information.')
186            exceptions.handle(request, msg)
187
188    if CINDER_QUOTA_FIELDS - disabled_quotas:
189        try:
190            quotasets.append(cinder.default_quota_get(request, tenant_id))
191        except cinder.cinder_exception.ClientException:
192            disabled_quotas.update(CINDER_QUOTA_FIELDS)
193            msg = _("Unable to retrieve volume quota information.")
194            exceptions.handle(request, msg)
195
196    if NEUTRON_QUOTA_FIELDS - disabled_quotas:
197        try:
198            quotasets.append(neutron.default_quota_get(request,
199                                                       tenant_id=tenant_id))
200        except Exception:
201            disabled_quotas.update(NEUTRON_QUOTA_FIELDS)
202            msg = _('Unable to retrieve Neutron quota information.')
203            exceptions.handle(request, msg)
204
205    qs = base.QuotaSet()
206    for quota in itertools.chain(*quotasets):
207        if quota.name not in disabled_quotas and quota.name in QUOTA_FIELDS:
208            qs[quota.name] = quota.limit
209    return qs
210
211
212@profiler.trace
213def get_tenant_quota_data(request, disabled_quotas=None, tenant_id=None):
214    quotasets = []
215    if not tenant_id:
216        tenant_id = request.user.tenant_id
217    if disabled_quotas is None:
218        disabled_quotas = get_disabled_quotas(request)
219
220    if NOVA_QUOTA_FIELDS - disabled_quotas:
221        try:
222            quotasets.append(nova.tenant_quota_get(request, tenant_id))
223        except Exception:
224            disabled_quotas.update(NOVA_QUOTA_FIELDS)
225            msg = _('Unable to retrieve Nova quota information.')
226            exceptions.handle(request, msg)
227
228    if CINDER_QUOTA_FIELDS - disabled_quotas:
229        try:
230            quotasets.append(cinder.tenant_quota_get(request, tenant_id))
231        except cinder.cinder_exception.ClientException:
232            disabled_quotas.update(CINDER_QUOTA_FIELDS)
233            msg = _("Unable to retrieve volume limit information.")
234            exceptions.handle(request, msg)
235
236    if NEUTRON_QUOTA_FIELDS - disabled_quotas:
237        try:
238            quotasets.append(neutron.tenant_quota_get(request, tenant_id))
239        except Exception:
240            disabled_quotas.update(NEUTRON_QUOTA_FIELDS)
241            msg = _('Unable to retrieve Neutron quota information.')
242            exceptions.handle(request, msg)
243
244    qs = base.QuotaSet()
245    for quota in itertools.chain(*quotasets):
246        if quota.name not in disabled_quotas and quota.name in QUOTA_FIELDS:
247            qs[quota.name] = quota.limit
248    return qs
249
250
251# TOOD(amotoki): Do not use neutron specific quota field names.
252# At now, quota names from nova-network are used in the dashboard code,
253# but get_disabled_quotas() returns quota names from neutron API.
254# It is confusing and makes the code complicated. They should be push away.
255# Check Identity Project panel and System Defaults panel too.
256@profiler.trace
257def get_disabled_quotas(request, targets=None):
258    if targets:
259        candidates = set(targets)
260    else:
261        candidates = QUOTA_FIELDS
262
263    # We no longer supports nova network, so we always disable
264    # network related nova quota fields.
265    disabled_quotas = set()
266
267    # Cinder
268    if candidates & CINDER_QUOTA_FIELDS:
269        if not cinder.is_volume_service_enabled(request):
270            disabled_quotas.update(CINDER_QUOTA_FIELDS)
271
272    # Neutron
273    if not (candidates & NEUTRON_QUOTA_FIELDS):
274        pass
275    elif not base.is_service_enabled(request, 'network'):
276        disabled_quotas.update(NEUTRON_QUOTA_FIELDS)
277    else:
278        if ({'security_group', 'security_group_rule'} & candidates and
279                not neutron.is_extension_supported(request, 'security-group')):
280            disabled_quotas.update(['security_group', 'security_group_rule'])
281
282        if ({'router', 'floatingip'} & candidates and
283                not neutron.is_router_enabled(request)):
284            disabled_quotas.update(['router', 'floatingip'])
285
286        try:
287            if not neutron.is_quotas_extension_supported(request):
288                disabled_quotas.update(NEUTRON_QUOTA_FIELDS)
289        except Exception:
290            LOG.exception("There was an error checking if the Neutron "
291                          "quotas extension is enabled.")
292
293    # Nova
294    if candidates & NOVA_QUOTA_FIELDS:
295        if not (base.is_service_enabled(request, 'compute') and
296                nova.can_set_quotas()):
297            disabled_quotas.update(NOVA_QUOTA_FIELDS)
298
299    enabled_quotas = candidates - disabled_quotas
300    disabled_quotas = set(QUOTA_FIELDS) - enabled_quotas
301
302    # There appear to be no glance quota fields currently
303    return disabled_quotas
304
305
306def _add_limit_and_usage(usages, name, limit, usage, disabled_quotas):
307    if name not in disabled_quotas:
308        usages.add_quota(base.Quota(name, limit))
309        if usage is not None:
310            usages.tally(name, usage)
311
312
313def _add_limit_and_usage_neutron(usages, name, quota_name,
314                                 detail, disabled_quotas):
315    if quota_name in disabled_quotas:
316        return
317    usages.add_quota(base.Quota(name, detail['limit']))
318    usages.tally(name, detail['used'] + detail['reserved'])
319
320
321@profiler.trace
322def _get_tenant_compute_usages(request, usages, disabled_quotas, tenant_id):
323    enabled_compute_quotas = NOVA_COMPUTE_QUOTA_FIELDS - disabled_quotas
324    if not enabled_compute_quotas:
325        return
326
327    if not base.is_service_enabled(request, 'compute'):
328        return
329
330    try:
331        limits = nova.tenant_absolute_limits(request, reserved=True,
332                                             tenant_id=tenant_id)
333    except nova.nova_exceptions.ClientException:
334        msg = _("Unable to retrieve compute limit information.")
335        exceptions.handle(request, msg)
336
337    for quota_name, limit_keys in NOVA_QUOTA_LIMIT_MAP.items():
338        if limit_keys['usage']:
339            usage = limits[limit_keys['usage']]
340        else:
341            usage = None
342        _add_limit_and_usage(usages, quota_name,
343                             limits[limit_keys['limit']],
344                             usage,
345                             disabled_quotas)
346
347
348@profiler.trace
349def _get_tenant_network_usages(request, usages, disabled_quotas, tenant_id):
350    enabled_quotas = NEUTRON_QUOTA_FIELDS - disabled_quotas
351    if not enabled_quotas:
352        return
353
354    details = neutron.tenant_quota_detail_get(request, tenant_id)
355    for quota_name in NEUTRON_QUOTA_FIELDS:
356        if quota_name in disabled_quotas:
357            continue
358        detail = details[quota_name]
359        usages.add_quota(base.Quota(quota_name, detail['limit']))
360        usages.tally(quota_name, detail['used'] + detail['reserved'])
361
362
363@profiler.trace
364def _get_tenant_volume_usages(request, usages, disabled_quotas, tenant_id):
365    enabled_volume_quotas = CINDER_QUOTA_FIELDS - disabled_quotas
366    if not enabled_volume_quotas:
367        return
368
369    try:
370        limits = cinder.tenant_absolute_limits(request, tenant_id)
371    except cinder.cinder_exception.ClientException:
372        msg = _("Unable to retrieve volume limit information.")
373        exceptions.handle(request, msg)
374
375    for quota_name, limit_keys in CINDER_QUOTA_LIMIT_MAP.items():
376        _add_limit_and_usage(usages, quota_name,
377                             limits[limit_keys['limit']],
378                             limits[limit_keys['usage']],
379                             disabled_quotas)
380
381
382@profiler.trace
383@memoized
384def tenant_quota_usages(request, tenant_id=None, targets=None):
385    """Get our quotas and construct our usage object.
386
387    :param tenant_id: Target tenant ID. If no tenant_id is provided,
388        a the request.user.project_id is assumed to be used.
389    :param targets: A tuple of quota names to be retrieved.
390        If unspecified, all quota and usage information is retrieved.
391    """
392    if not tenant_id:
393        tenant_id = request.user.project_id
394
395    disabled_quotas = get_disabled_quotas(request, targets)
396    usages = QuotaUsage()
397
398    futurist_utils.call_functions_parallel(
399        (_get_tenant_compute_usages,
400         [request, usages, disabled_quotas, tenant_id]),
401        (_get_tenant_network_usages,
402         [request, usages, disabled_quotas, tenant_id]),
403        (_get_tenant_volume_usages,
404         [request, usages, disabled_quotas, tenant_id]))
405
406    return usages
407
408
409def enabled_quotas(request):
410    """Returns the list of quotas available minus those that are disabled"""
411    return QUOTA_FIELDS - get_disabled_quotas(request)
412