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