1# Copyright 2013 OpenStack Foundation 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. 15from oslo_config import cfg 16from oslo_log import log as logging 17 18from keystoneauth1 import identity 19from keystoneauth1 import loading as ka_loading 20from keystoneclient import client 21from keystoneclient import exceptions 22 23from cinder import db 24from cinder import exception 25from cinder.i18n import _ 26 27CONF = cfg.CONF 28CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token.__init__', 29 'keystone_authtoken') 30 31LOG = logging.getLogger(__name__) 32 33 34class GenericProjectInfo(object): 35 """Abstraction layer for Keystone V2 and V3 project objects""" 36 def __init__(self, project_id, project_keystone_api_version, 37 project_parent_id=None, 38 project_subtree=None, 39 project_parent_tree=None, 40 is_admin_project=False): 41 self.id = project_id 42 self.keystone_api_version = project_keystone_api_version 43 self.parent_id = project_parent_id 44 self.subtree = project_subtree 45 self.parents = project_parent_tree 46 self.is_admin_project = is_admin_project 47 48 49def get_volume_type_reservation(ctxt, volume, type_id, 50 reserve_vol_type_only=False): 51 from cinder import quota 52 QUOTAS = quota.QUOTAS 53 # Reserve quotas for the given volume type 54 try: 55 reserve_opts = {'volumes': 1, 'gigabytes': volume['size']} 56 QUOTAS.add_volume_type_opts(ctxt, 57 reserve_opts, 58 type_id) 59 # If reserve_vol_type_only is True, just reserve volume_type quota, 60 # not volume quota. 61 if reserve_vol_type_only: 62 reserve_opts.pop('volumes') 63 reserve_opts.pop('gigabytes') 64 # Note that usually the project_id on the volume will be the same as 65 # the project_id in the context. But, if they are different then the 66 # reservations must be recorded against the project_id that owns the 67 # volume. 68 project_id = volume['project_id'] 69 reservations = QUOTAS.reserve(ctxt, 70 project_id=project_id, 71 **reserve_opts) 72 except exception.OverQuota as e: 73 process_reserve_over_quota(ctxt, e, 74 resource='volumes', 75 size=volume.size) 76 return reservations 77 78 79def _filter_domain_id_from_parents(domain_id, tree): 80 """Removes the domain_id from the tree if present""" 81 new_tree = None 82 if tree: 83 parent, children = next(iter(tree.items())) 84 # Don't add the domain id to the parents hierarchy 85 if parent != domain_id: 86 new_tree = {parent: _filter_domain_id_from_parents(domain_id, 87 children)} 88 89 return new_tree 90 91 92def get_project_hierarchy(context, project_id, subtree_as_ids=False, 93 parents_as_ids=False, is_admin_project=False): 94 """A Helper method to get the project hierarchy. 95 96 Along with hierarchical multitenancy in keystone API v3, projects can be 97 hierarchically organized. Therefore, we need to know the project 98 hierarchy, if any, in order to do nested quota operations properly. 99 If the domain is being used as the top most parent, it is filtered out from 100 the parent tree and parent_id. 101 """ 102 keystone = _keystone_client(context) 103 generic_project = GenericProjectInfo(project_id, keystone.version) 104 if keystone.version == 'v3': 105 project = keystone.projects.get(project_id, 106 subtree_as_ids=subtree_as_ids, 107 parents_as_ids=parents_as_ids) 108 109 generic_project.parent_id = None 110 if project.parent_id != project.domain_id: 111 generic_project.parent_id = project.parent_id 112 113 generic_project.subtree = ( 114 project.subtree if subtree_as_ids else None) 115 116 generic_project.parents = None 117 if parents_as_ids: 118 generic_project.parents = _filter_domain_id_from_parents( 119 project.domain_id, project.parents) 120 121 generic_project.is_admin_project = is_admin_project 122 123 return generic_project 124 125 126def get_parent_project_id(context, project_id): 127 return get_project_hierarchy(context, project_id).parent_id 128 129 130def get_all_projects(context): 131 # Right now this would have to be done as cloud admin with Keystone v3 132 return _keystone_client(context, (3, 0)).projects.list() 133 134 135def get_all_root_project_ids(context): 136 project_list = get_all_projects(context) 137 138 # Find every project which does not have a parent, meaning it is the 139 # root of the tree 140 project_roots = [project.id for project in project_list 141 if not project.parent_id] 142 143 return project_roots 144 145 146def update_alloc_to_next_hard_limit(context, resources, deltas, res, 147 expire, project_id): 148 from cinder import quota 149 QUOTAS = quota.QUOTAS 150 GROUP_QUOTAS = quota.GROUP_QUOTAS 151 reservations = [] 152 projects = get_project_hierarchy(context, project_id, 153 parents_as_ids=True).parents 154 hard_limit_found = False 155 # Update allocated values up the chain til we hit a hard limit or run out 156 # of parents 157 while projects and not hard_limit_found: 158 cur_proj_id = list(projects)[0] 159 projects = projects[cur_proj_id] 160 if res == 'groups': 161 cur_quota_lim = GROUP_QUOTAS.get_by_project_or_default( 162 context, cur_proj_id, res) 163 else: 164 cur_quota_lim = QUOTAS.get_by_project_or_default( 165 context, cur_proj_id, res) 166 hard_limit_found = (cur_quota_lim != -1) 167 cur_quota = {res: cur_quota_lim} 168 cur_delta = {res: deltas[res]} 169 try: 170 reservations += db.quota_reserve( 171 context, resources, cur_quota, cur_delta, expire, 172 CONF.until_refresh, CONF.max_age, cur_proj_id, 173 is_allocated_reserve=True) 174 except exception.OverQuota: 175 db.reservation_rollback(context, reservations) 176 raise 177 return reservations 178 179 180def validate_setup_for_nested_quota_use(ctxt, resources, 181 nested_quota_driver, 182 fix_allocated_quotas=False): 183 """Validates the setup supports using nested quotas. 184 185 Ensures that Keystone v3 or greater is being used, that the current 186 user is of the cloud admin role, and that the existing quotas make sense to 187 nest in the current hierarchy (e.g. that no child quota would be larger 188 than it's parent). 189 190 :param resources: the quota resources to validate 191 :param nested_quota_driver: nested quota driver used to validate each tree 192 :param fix_allocated_quotas: if True, parent projects "allocated" total 193 will be calculated based on the existing child limits and the DB will 194 be updated. If False, an exception is raised reporting any parent 195 allocated quotas are currently incorrect. 196 """ 197 try: 198 project_roots = get_all_root_project_ids(ctxt) 199 200 # Now that we've got the roots of each tree, validate the trees 201 # to ensure that each is setup logically for nested quotas 202 for root in project_roots: 203 root_proj = get_project_hierarchy(ctxt, root, 204 subtree_as_ids=True) 205 nested_quota_driver.validate_nested_setup( 206 ctxt, 207 resources, 208 {root_proj.id: root_proj.subtree}, 209 fix_allocated_quotas=fix_allocated_quotas 210 ) 211 except exceptions.VersionNotAvailable: 212 msg = _("Keystone version 3 or greater must be used to get nested " 213 "quota support.") 214 raise exception.CinderException(message=msg) 215 except exceptions.Forbidden: 216 msg = _("Must run this command as cloud admin using " 217 "a Keystone policy.json which allows cloud " 218 "admin to list and get any project.") 219 raise exception.CinderException(message=msg) 220 221 222def _keystone_client(context, version=(3, 0)): 223 """Creates and returns an instance of a generic keystone client. 224 225 :param context: The request context 226 :param version: version of Keystone to request 227 :return: keystoneclient.client.Client object 228 """ 229 auth_plugin = identity.Token( 230 auth_url=CONF.keystone_authtoken.auth_uri, 231 token=context.auth_token, 232 project_id=context.project_id) 233 234 client_session = ka_loading.session.Session().load_from_options( 235 auth=auth_plugin, 236 insecure=CONF.keystone_authtoken.insecure, 237 cacert=CONF.keystone_authtoken.cafile, 238 key=CONF.keystone_authtoken.keyfile, 239 cert=CONF.keystone_authtoken.certfile) 240 return client.Client(auth_url=CONF.keystone_authtoken.auth_uri, 241 session=client_session, version=version) 242 243 244OVER_QUOTA_RESOURCE_EXCEPTIONS = {'snapshots': exception.SnapshotLimitExceeded, 245 'backups': exception.BackupLimitExceeded, 246 'volumes': exception.VolumeLimitExceeded, 247 'groups': exception.GroupLimitExceeded} 248 249 250def process_reserve_over_quota(context, over_quota_exception, 251 resource, size=None): 252 """Handle OverQuota exception. 253 254 Analyze OverQuota exception, and raise new exception related to 255 resource type. If there are unexpected items in overs, 256 UnexpectedOverQuota is raised. 257 258 :param context: security context 259 :param over_quota_exception: OverQuota exception 260 :param resource: can be backups, snapshots, and volumes 261 :param size: requested size in reservation 262 """ 263 def _consumed(name): 264 return (usages[name]['reserved'] + usages[name]['in_use']) 265 266 overs = over_quota_exception.kwargs['overs'] 267 usages = over_quota_exception.kwargs['usages'] 268 quotas = over_quota_exception.kwargs['quotas'] 269 invalid_overs = [] 270 271 for over in overs: 272 if 'gigabytes' in over: 273 msg = ("Quota exceeded for %(s_pid)s, tried to create " 274 "%(s_size)dG %(s_resource)s (%(d_consumed)dG of " 275 "%(d_quota)dG already consumed).") 276 LOG.warning(msg, {'s_pid': context.project_id, 277 's_size': size, 278 's_resource': resource[:-1], 279 'd_consumed': _consumed(over), 280 'd_quota': quotas[over]}) 281 if resource == 'backups': 282 exc = exception.VolumeBackupSizeExceedsAvailableQuota 283 else: 284 exc = exception.VolumeSizeExceedsAvailableQuota 285 raise exc( 286 name=over, 287 requested=size, 288 consumed=_consumed(over), 289 quota=quotas[over]) 290 if (resource in OVER_QUOTA_RESOURCE_EXCEPTIONS.keys() and 291 resource in over): 292 msg = ("Quota exceeded for %(s_pid)s, tried to create " 293 "%(s_resource)s (%(d_consumed)d %(s_resource)ss " 294 "already consumed).") 295 LOG.warning(msg, {'s_pid': context.project_id, 296 'd_consumed': _consumed(over), 297 's_resource': resource[:-1]}) 298 raise OVER_QUOTA_RESOURCE_EXCEPTIONS[resource]( 299 allowed=quotas[over], 300 name=over) 301 invalid_overs.append(over) 302 303 if invalid_overs: 304 raise exception.UnexpectedOverQuota(name=', '.join(invalid_overs)) 305