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