Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / quotas / __init__.py @ bd40abfa

History | View | Annotate | Download (12.1 kB)

1
# Copyright 2012 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or without
4
# modification, are permitted provided that the following conditions
5
# are met:
6
#
7
#   1. Redistributions of source code must retain the above copyright
8
#      notice, this list of conditions and the following disclaimer.
9
#
10
#  2. Redistributions in binary form must reproduce the above copyright
11
#     notice, this list of conditions and the following disclaimer in the
12
#     documentation and/or other materials provided with the distribution.
13
#
14
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
15
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17
# ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
18
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24
# SUCH DAMAGE.
25
#
26
# The views and conclusions contained in the software and documentation are
27
# those of the authors and should not be interpreted as representing official
28
# policies, either expressed or implied, of GRNET S.A.
29

    
30
from functools import wraps
31
from contextlib import contextmanager
32

    
33
from snf_django.lib.api import faults
34
from synnefo.db.models import QuotaHolderSerial, VirtualMachine, Network
35
from synnefo.settings import CYCLADES_USE_QUOTAHOLDER
36

    
37
if CYCLADES_USE_QUOTAHOLDER:
38
    from synnefo.settings import (CYCLADES_QUOTAHOLDER_URL,
39
                                  CYCLADES_QUOTAHOLDER_TOKEN,
40
                                  CYCLADES_QUOTAHOLDER_POOLSIZE)
41
    from synnefo.lib.quotaholder import QuotaholderClient
42
else:
43
    from synnefo.settings import (VMS_USER_QUOTA, MAX_VMS_PER_USER,
44
                                  NETWORKS_USER_QUOTA, MAX_NETWORKS_PER_USER)
45

    
46
from synnefo.lib.quotaholder.api import (NoCapacityError, NoQuantityError,
47
                                         NoEntityError, CallError)
48

    
49
import logging
50
log = logging.getLogger(__name__)
51

    
52

    
53
class DummySerial(QuotaHolderSerial):
54
    accepted = True
55
    rejected = True
56
    pending = True
57
    id = None
58

    
59
    def save(*args, **kwargs):
60
        pass
61

    
62

    
63
class DummyQuotaholderClient(object):
64
    def issue_commission(self, **commission_info):
65
        provisions = commission_info["provisions"]
66
        userid = commission_info["target"]
67
        for provision in provisions:
68
            entity, resource, size = provision
69
            if resource == "cyclades.vm" and size > 0:
70
                user_vms = VirtualMachine.objects.filter(userid=userid,
71
                                                         deleted=False).count()
72
                user_vm_limit = VMS_USER_QUOTA.get(userid, MAX_VMS_PER_USER)
73
                log.debug("Users VMs %s User Limits %s", user_vms,
74
                          user_vm_limit)
75
                if user_vms + size > user_vm_limit:
76
                    raise NoQuantityError(source='cyclades',
77
                                          target=userid,
78
                                          resource=resource,
79
                                          requested=size,
80
                                          current=user_vms,
81
                                          limit=user_vm_limit)
82
            if resource == "cyclades.network.private" and size > 0:
83
                user_networks = Network.objects.filter(userid=userid,
84
                                                       deleted=False).count()
85
                user_network_limit =\
86
                    NETWORKS_USER_QUOTA.get(userid, MAX_NETWORKS_PER_USER)
87
                if user_networks + size > user_network_limit:
88
                    raise NoQuantityError(source='cyclades',
89
                                          target=userid,
90
                                          resource=resource,
91
                                          requested=size,
92
                                          current=user_networks,
93
                                          limit=user_network_limit)
94

    
95
        return None
96

    
97
    def accept_commission(self, *args, **kwargs):
98
        pass
99

    
100
    def reject_commission(self, *args, **kwargs):
101
        pass
102

    
103
    def get_pending_commissions(self, *args, **kwargs):
104
        return []
105

    
106

    
107
@contextmanager
108
def get_quota_holder():
109
    """Context manager for using a QuotaHolder."""
110
    if CYCLADES_USE_QUOTAHOLDER:
111
        quotaholder = QuotaholderClient(CYCLADES_QUOTAHOLDER_URL,
112
                                        token=CYCLADES_QUOTAHOLDER_TOKEN,
113
                                        poolsize=CYCLADES_QUOTAHOLDER_POOLSIZE)
114
    else:
115
        quotaholder = DummyQuotaholderClient()
116

    
117
    try:
118
        yield quotaholder
119
    finally:
120
        pass
121

    
122

    
123
def uses_commission(func):
124
    """Decorator for wrapping functions that needs commission.
125

126
    All decorated functions must take as first argument the `serials` list in
127
    order to extend them with the needed serial numbers, as return by the
128
    Quotaholder
129

130
    On successful competition of the decorated function, all serials are
131
    accepted to the quotaholder, otherwise they are rejected.
132

133
    """
134

    
135
    @wraps(func)
136
    def wrapper(*args, **kwargs):
137
        try:
138
            serials = []
139
            ret = func(serials, *args, **kwargs)
140
        except:
141
            log.exception("Unexpected error")
142
            try:
143
                if serials:
144
                    reject_commission(serials=serials)
145
            except:
146
                log.exception("Exception while rejecting serials %s", serials)
147
                raise
148
            raise
149

    
150
        # func has completed successfully. accept serials
151
        try:
152
            if serials:
153
                accept_commission(serials)
154
            return ret
155
        except:
156
            log.exception("Exception while accepting serials %s", serials)
157
            raise
158
    return wrapper
159

    
160

    
161
## FIXME: Wrap the following two functions inside transaction ?
162
def accept_commission(serials, update_db=True):
163
    """Accept a list of pending commissions.
164

165
    @param serials: List of QuotaHolderSerial objects
166

167
    """
168
    if update_db:
169
        for s in serials:
170
            if s.pending:
171
                s.accepted = True
172
                s.save()
173

    
174
    with get_quota_holder() as qh:
175
        qh.accept_commission(context={},
176
                             clientkey='cyclades',
177
                             serials=[s.serial for s in serials])
178

    
179

    
180
def reject_commission(serials, update_db=True):
181
    """Reject a list of pending commissions.
182

183
    @param serials: List of QuotaHolderSerial objects
184

185
    """
186
    if update_db:
187
        for s in serials:
188
            if s.pending:
189
                s.rejected = True
190
                s.save()
191

    
192
    with get_quota_holder() as qh:
193
        qh.reject_commission(context={},
194
                             clientkey='cyclades',
195
                             serials=[s.serial for s in serials])
196

    
197

    
198
def issue_commission(**commission_info):
199
    """Issue a new commission to the quotaholder.
200

201
    Issue a new commission to the quotaholder, and create the
202
    corresponing QuotaHolderSerial object in DB.
203

204
    """
205

    
206
    with get_quota_holder() as qh:
207
        try:
208
            serial = qh.issue_commission(**commission_info)
209
        except (NoCapacityError, NoQuantityError) as e:
210
            msg, details = render_quotaholder_exception(e)
211
            raise faults.OverLimit(msg, details=details)
212
        except CallError as e:
213
            log.exception("Unexpected error")
214
            raise
215

    
216
    if serial:
217
        return QuotaHolderSerial.objects.create(serial=serial)
218
    elif not CYCLADES_USE_QUOTAHOLDER:
219
        return DummySerial()
220
    else:
221
        raise Exception("No serial")
222

    
223

    
224
# Wrapper functions for issuing commissions for each resource type.  Each
225
# functions creates the `commission_info` dictionary as expected by the
226
# `issue_commision` function. Commissions for deleting a resource, are the same
227
# as for creating the same resource, but with negative resource sizes.
228

    
229

    
230
def issue_vm_commission(user, flavor, delete=False):
231
    resources = get_server_resources(flavor)
232
    commission_info = create_commission(user, resources, delete)
233

    
234
    return issue_commission(**commission_info)
235

    
236

    
237
def get_server_resources(flavor):
238
    return {'vm': 1,
239
            'cpu': flavor.cpu,
240
            'disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
241
            # 'public_ip': 1,
242
            #'disk_template': flavor.disk_template,
243
            'ram': 1048576 * flavor.ram}  # flavor.ram is in MB
244

    
245

    
246
def issue_network_commission(user, delete=False):
247
    resources = get_network_resources()
248
    commission_info = create_commission(user, resources, delete)
249

    
250
    return issue_commission(**commission_info)
251

    
252

    
253
def get_network_resources():
254
    return {"network.private": 1}
255

    
256

    
257
def invert_resources(resources_dict):
258
    return dict((r, -s) for r, s in resources_dict.items())
259

    
260

    
261
def create_commission(user, resources, delete=False):
262
    if delete:
263
        resources = invert_resources(resources)
264
    provisions = [('cyclades', 'cyclades.' + r, s)
265
                  for r, s in resources.items()]
266
    return {"context": {},
267
            "target": user,
268
            "key": "1",
269
            "clientkey": "cyclades",
270
            #"owner":      "",
271
            #"ownerkey":   "1",
272
            "name": "",
273
            "provisions": provisions}
274

    
275
##
276
## Reconcile pending commissions
277
##
278

    
279

    
280
def accept_commissions(accepted):
281
    with get_quota_holder() as qh:
282
        qh.accept_commission(context={},
283
                             clientkey='cyclades',
284
                             serials=accepted)
285

    
286

    
287
def reject_commissions(rejected):
288
    with get_quota_holder() as qh:
289
            qh.reject_commission(context={},
290
                                 clientkey='cyclades',
291
                                 serials=rejected)
292

    
293

    
294
def fix_pending_commissions():
295
    (accepted, rejected) = resolve_pending_commissions()
296

    
297
    with get_quota_holder() as qh:
298
        if accepted:
299
            qh.accept_commission(context={},
300
                                 clientkey='cyclades',
301
                                 serials=accepted)
302
        if rejected:
303
            qh.reject_commission(context={},
304
                                 clientkey='cyclades',
305
                                 serials=rejected)
306

    
307

    
308
def resolve_pending_commissions():
309
    """Resolve quotaholder pending commissions.
310

311
    Get pending commissions from the quotaholder and resolve them
312
    to accepted and rejected, according to the state of the
313
    QuotaHolderSerial DB table. A pending commission in the quotaholder
314
    can exist in the QuotaHolderSerial table and be either accepted or
315
    rejected, or can not exist in this table, so it is rejected.
316

317
    """
318

    
319
    qh_pending = get_quotaholder_pending()
320
    if not qh_pending:
321
        return ([], [])
322

    
323
    qh_pending.sort()
324
    min_ = qh_pending[0]
325

    
326
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
327
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
328
    accepted = filter(lambda x: x in qh_pending, accepted)
329

    
330
    rejected = list(set(qh_pending) - set(accepted))
331

    
332
    return (accepted, rejected)
333

    
334

    
335
def get_quotaholder_pending():
336
    with get_quota_holder() as qh:
337
        pending_serials = qh.get_pending_commissions(context={},
338
                                                     clientkey='cyclades')
339
    return pending_serials
340

    
341

    
342
def render_quotaholder_exception(e):
343
    resource_name = {"vm": "Virtual Machine",
344
                     "cpu": "CPU",
345
                     "ram": "RAM",
346
                     "network.private": "Private Network"}
347
    res = e.resource.replace("cyclades.", "", 1)
348
    try:
349
        resource = resource_name[res]
350
    except KeyError:
351
        resource = res
352

    
353
    requested = e.requested
354
    current = e.current
355
    limit = e.limit
356
    msg = "Resource Limit Exceeded for your account."
357
    details = "Limit for resource '%s' exceeded for your account."\
358
              " Current value: %s, Limit: %s, Requested: %s"\
359
              % (resource, current, limit, requested)
360
    return msg, details