summaryrefslogtreecommitdiff
path: root/trove/quota/quota.py
blob: 56891efefaaab5022dd396c071895b46c95eda74 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# Copyright 2011 OpenStack Foundation
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Quotas for DB instances and resources."""

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils

from trove.common import exception
from trove.quota.models import Quota
from trove.quota.models import QuotaUsage
from trove.quota.models import Reservation
from trove.quota.models import Resource

LOG = logging.getLogger(__name__)
CONF = cfg.CONF


class DbQuotaDriver(object):
    """
    Driver to perform necessary checks to enforce quotas and obtain
    quota information.  The default driver utilizes the local
    database.
    """

    def __init__(self, resources):
        self.resources = resources

    def get_quota_by_tenant(self, tenant_id, resource):
        """Get a specific quota by tenant."""

        quotas = Quota.find_all(tenant_id=tenant_id, resource=resource).all()
        if len(quotas) == 0:
            return Quota(tenant_id, resource, self.resources[resource].default)

        return quotas[0]

    def get_all_quotas_by_tenant(self, tenant_id, resources):
        """
        Retrieve the quotas for the given tenant.

        :param resources: A list of the registered resource to get.
        :param tenant_id: The ID of the tenant to return quotas for.
        """

        all_quotas = Quota.find_all(tenant_id=tenant_id).all()
        result_quotas = {quota.resource: quota for quota in all_quotas
                         if quota.resource in resources}

        if len(result_quotas) != len(resources):
            for resource in resources:
                # Not in the DB, return default value
                if resource not in result_quotas:
                    quota = Quota(tenant_id,
                                  resource,
                                  self.resources[resource].default)
                    result_quotas[resource] = quota

        return result_quotas

    def get_quota_usage_by_tenant(self, tenant_id, resource):
        """Get a specific quota usage by tenant."""

        quotas = QuotaUsage.find_all(tenant_id=tenant_id,
                                     resource=resource).all()
        if len(quotas) == 0:
            return QuotaUsage.create(tenant_id=tenant_id,
                                     in_use=0,
                                     reserved=0,
                                     resource=resource)
        return quotas[0]

    def get_all_quota_usages_by_tenant(self, tenant_id, resources):
        """
        Retrieve the quota usagess for the given tenant.

        :param tenant_id: The ID of the tenant to return quotas for.
        :param resources: A list of the registered resources to get.
        """

        all_usages = QuotaUsage.find_all(tenant_id=tenant_id).all()
        result_usages = {usage.resource: usage for usage in all_usages
                         if usage.resource in resources}
        if len(result_usages) != len(resources):
            for resource in resources:
                # Not in the DB, return default value
                if resource not in result_usages:
                    usage = QuotaUsage.create(tenant_id=tenant_id,
                                              in_use=0,
                                              reserved=0,
                                              resource=resource)
                    result_usages[resource] = usage

        return result_usages

    def get_defaults(self, resources):
        """Given a list of resources, retrieve the default quotas.

        :param resources: A list of the registered resources.
        """

        quotas = {}
        for resource in resources.values():
            quotas[resource.name] = resource.default

        return quotas

    def check_quotas(self, tenant_id, resources, deltas):
        """Check quotas for a tenant.

        This method checks quotas against current usage,
        reserved resources and the desired deltas.

        If any of the proposed values is over the defined quota, an
        QuotaExceeded exception will be raised with the sorted list of the
        resources which are too high.

        :param tenant_id: The ID of the tenant reserving the resources.
        :param resources: A dictionary of the registered resources.
        :param deltas: A dictionary of the proposed delta changes.
        """

        unregistered_resources = [delta for delta in deltas
                                  if delta not in resources]
        if unregistered_resources:
            raise exception.QuotaResourceUnknown(
                unknown=unregistered_resources)

        quotas = self.get_all_quotas_by_tenant(tenant_id, deltas.keys())
        quota_usages = self.get_all_quota_usages_by_tenant(tenant_id,
                                                           deltas.keys())

        overs = [resource for resource in deltas
                 if (int(deltas[resource]) > 0 and
                     quotas[resource].hard_limit >= 0 and
                     (quota_usages[resource].in_use +
                      quota_usages[resource].reserved +
                      int(deltas[resource])) > quotas[resource].hard_limit)]

        if overs:
            raise exception.QuotaExceeded(overs=sorted(overs))

    def reserve(self, tenant_id, resources, deltas):
        """Check quotas and reserve resources for a tenant.

        This method checks quotas against current usage,
        reserved resources and the desired deltas.

        If any of the proposed values is over the defined quota, an
        QuotaExceeded exception will be raised with the sorted list of the
        resources which are too high.  Otherwise, the method returns a
        list of reservation objects which were created.

        :param tenant_id: The ID of the tenant reserving the resources.
        :param resources: A dictionary of the registered resources.
        :param deltas: A dictionary of the proposed delta changes.
        """

        self.check_quotas(tenant_id, resources, deltas)
        quota_usages = self.get_all_quota_usages_by_tenant(tenant_id,
                                                           deltas.keys())

        reservations = []
        for resource in sorted(deltas):
            reserved = deltas[resource]
            usage = quota_usages[resource]
            usage.reserved += reserved
            usage.save()

            resv = Reservation.create(usage_id=usage.id,
                                      delta=reserved,
                                      status=Reservation.Statuses.RESERVED)
            reservations.append(resv)

        return reservations

    def commit(self, reservations):
        """Commit reservations.

        :param reservations: A list of the reservation UUIDs, as
                             returned by the reserve() method.
        """

        for reservation in reservations:
            usage = QuotaUsage.find_by(id=reservation.usage_id)
            usage.in_use += reservation.delta
            if usage.in_use < 0:
                usage.in_use = 0
            usage.reserved -= reservation.delta
            reservation.status = Reservation.Statuses.COMMITTED
            usage.save()
            reservation.save()

    def rollback(self, reservations):
        """Roll back reservations.

        :param reservations: A list of the reservation UUIDs, as
                             returned by the reserve() method.
        """

        for reservation in reservations:
            usage = QuotaUsage.find_by(id=reservation.usage_id)
            usage.reserved -= reservation.delta
            reservation.status = Reservation.Statuses.ROLLEDBACK
            usage.save()
            reservation.save()


class QuotaEngine(object):
    """Represent the set of recognized quotas."""

    def __init__(self, quota_driver_class=None):
        """Initialize a Quota object."""

        self._resources = {}

        if not quota_driver_class:
            quota_driver_class = CONF.quota_driver
        if isinstance(quota_driver_class, str):
            quota_driver_class = importutils.import_object(quota_driver_class,
                                                           self._resources)
        self._driver = quota_driver_class

    def __contains__(self, resource):
        return resource in self._resources

    def register_resource(self, resource):
        """Register a resource."""

        self._resources[resource.name] = resource

    def register_resources(self, resources):
        """Register a dictionary of resources."""

        for resource in resources:
            self.register_resource(resource)

    def get_quota_by_tenant(self, tenant_id, resource):
        """Get a specific quota by tenant."""

        return self._driver.get_quota_by_tenant(tenant_id, resource)

    def get_quota_usage(self, quota):
        """Get the usage for a quota."""

        return self._driver.get_quota_usage_by_tenant(quota.tenant_id,
                                                      quota.resource)

    def get_defaults(self):
        """Retrieve the default quotas."""

        return self._driver.get_defaults(self._resources)

    def get_all_quotas_by_tenant(self, tenant_id):
        """Retrieve the quotas for the given tenant.

        :param tenant_id: The ID of the tenant to return quotas for.
        """

        return self._driver.get_all_quotas_by_tenant(tenant_id,
                                                     self._resources)

    def get_all_quota_usages_by_tenant(self, tenant_id):
        """Retrieve the quota usages for the given tenant.

        :param tenant_id: The ID of the tenant to return quota usages for.
        """

        return self._driver.get_all_quota_usages_by_tenant(tenant_id,
                                                           self._resources)

    def check_quotas(self, tenant_id, **deltas):
        self._driver.check_quotas(tenant_id, self._resources, deltas)

    def reserve(self, tenant_id, **deltas):
        """Check quotas and reserve resources.

        For counting quotas--those quotas for which there is a usage
        synchronization function--this method checks quotas against
        current usage and the desired deltas.  The deltas are given as
        keyword arguments, and current usage and other reservations
        are factored into the quota check.

        This method will raise a QuotaResourceUnknown exception if a
        given resource is unknown or if it does not have a usage
        synchronization function.

        If any of the proposed values is over the defined quota, an
        QuotaExceeded exception will be raised with the sorted list of the
        resources which are too high.  Otherwise, the method returns a
        list of reservation UUIDs which were created.

        :param tenant_id: The ID of the tenant to reserve quotas for.
        """

        reservations = self._driver.reserve(tenant_id, self._resources, deltas)

        LOG.debug("Created reservations %(reservations)s",
                  {'reservations': reservations})

        return reservations

    def commit(self, reservations):
        """Commit reservations.

        :param reservations: A list of the reservation UUIDs, as
                             returned by the reserve() method.
        """

        try:
            self._driver.commit(reservations)
        except Exception:
            LOG.exception("Failed to commit reservations "
                          "%(reservations)s", {'reservations': reservations})

    def rollback(self, reservations):
        """Roll back reservations.

        :param reservations: A list of the reservation UUIDs, as
                             returned by the reserve() method.
        """

        try:
            self._driver.rollback(reservations)
        except Exception:
            LOG.exception("Failed to roll back reservations "
                          "%(reservations)s", {'reservations': reservations})

    @property
    def resources(self):
        return sorted(self._resources.keys())


QUOTAS = QuotaEngine()

''' Define all kind of resources here '''

resources = [Resource(Resource.INSTANCES, 'max_instances_per_tenant'),
             Resource(Resource.RAM, 'max_ram_per_tenant'),
             Resource(Resource.BACKUPS, 'max_backups_per_tenant'),
             Resource(Resource.VOLUMES, 'max_volumes_per_tenant')]

QUOTAS.register_resources(resources)


def run_with_quotas(tenant_id, deltas, f, *args, **kwargs):
    """Quota wrapper."""

    reservations = QUOTAS.reserve(tenant_id, **deltas)
    result = None
    try:
        result = f(*args, **kwargs)
    except Exception:
        QUOTAS.rollback(reservations)
        raise
    else:
        QUOTAS.commit(reservations)
    return result


def check_quotas(tenant_id, deltas):
    QUOTAS.check_quotas(tenant_id, **deltas)