Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.8 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

    
34
from synnefo.db.models import QuotaHolderSerial, VirtualMachine, Network
35
from synnefo.api.faults import OverLimit
36
from synnefo.settings import CYCLADES_USE_QUOTAHOLDER
37

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

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

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

    
53

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

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

    
63

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

    
96
        return None
97

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

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

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

    
107

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

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

    
123

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

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

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

134
    """
135

    
136
    @wraps(func)
137
    def wrapper(*args, **kwargs):
138
        try:
139
            serials = []
140
            ret = func(serials, *args, **kwargs)
141
            if serials:
142
                accept_commission(serials)
143
            return ret
144
        except CallError:
145
            log.exception("Unexpected error")
146
            raise
147
        except:
148
            if serials:
149
                reject_commission(serials=serials)
150
            raise
151
    return wrapper
152

    
153

    
154
## FIXME: Wrap the following two functions inside transaction ?
155
def accept_commission(serials, update_db=True):
156
    """Accept a list of pending commissions.
157

158
    @param serials: List of QuotaHolderSerial objects
159

160
    """
161
    if update_db:
162
        for s in serials:
163
            if s.pending:
164
                s.accepted = True
165
                s.save()
166

    
167
    with get_quota_holder() as qh:
168
        qh.accept_commission(context={},
169
                             clientkey='cyclades',
170
                             serials=[s.serial for s in serials])
171

    
172

    
173
def reject_commission(serials, update_db=True):
174
    """Reject a list of pending commissions.
175

176
    @param serials: List of QuotaHolderSerial objects
177

178
    """
179
    if update_db:
180
        for s in serials:
181
            if s.pending:
182
                s.rejected = True
183
                s.save()
184

    
185
    with get_quota_holder() as qh:
186
        qh.reject_commission(context={},
187
                             clientkey='cyclades',
188
                             serials=[s.serial for s in serials])
189

    
190

    
191
def issue_commission(**commission_info):
192
    """Issue a new commission to the quotaholder.
193

194
    Issue a new commission to the quotaholder, and create the
195
    corresponing QuotaHolderSerial object in DB.
196

197
    """
198

    
199
    with get_quota_holder() as qh:
200
        try:
201
            serial = qh.issue_commission(**commission_info)
202
        except (NoCapacityError, NoQuantityError) as e:
203
            msg, details = render_quotaholder_exception(e)
204
            raise OverLimit(msg, details=details)
205
        except CallError as e:
206
            log.exception("Unexpected error")
207
            raise
208

    
209
    if serial:
210
        return QuotaHolderSerial.objects.create(serial=serial)
211
    elif not CYCLADES_USE_QUOTAHOLDER:
212
        return DummySerial()
213
    else:
214
        raise Exception("No serial")
215

    
216

    
217
# Wrapper functions for issuing commissions for each resource type.  Each
218
# functions creates the `commission_info` dictionary as expected by the
219
# `issue_commision` function. Commissions for deleting a resource, are the same
220
# as for creating the same resource, but with negative resource sizes.
221

    
222

    
223
def issue_vm_commission(user, flavor, delete=False):
224
    resources = get_server_resources(flavor)
225
    commission_info = create_commission(user, resources, delete)
226

    
227
    return issue_commission(**commission_info)
228

    
229

    
230
def get_server_resources(flavor):
231
    return {'vm': 1,
232
            'cpu': flavor.cpu,
233
            'disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
234
            # 'public_ip': 1,
235
            #'disk_template': flavor.disk_template,
236
            'ram': 1048576 * flavor.ram}  # flavor.ram is in MB
237

    
238

    
239
def issue_network_commission(user, delete=False):
240
    resources = get_network_resources()
241
    commission_info = create_commission(user, resources, delete)
242

    
243
    return issue_commission(**commission_info)
244

    
245

    
246
def get_network_resources():
247
    return {"network.private": 1}
248

    
249

    
250
def invert_resources(resources_dict):
251
    return dict((r, -s) for r, s in resources_dict.items())
252

    
253

    
254
def create_commission(user, resources, delete=False):
255
    if delete:
256
        resources = invert_resources(resources)
257
    provisions = [('cyclades', 'cyclades.' + r, s)
258
                  for r, s in resources.items()]
259
    return {"context": {},
260
            "target": user,
261
            "key": "1",
262
            "clientkey": "cyclades",
263
            #"owner":      "",
264
            #"ownerkey":   "1",
265
            "name": "",
266
            "provisions": provisions}
267

    
268
##
269
## Reconcile pending commissions
270
##
271

    
272

    
273
def accept_commissions(accepted):
274
    with get_quota_holder() as qh:
275
        qh.accept_commission(context={},
276
                             clientkey='cyclades',
277
                             serials=accepted)
278

    
279

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

    
286

    
287
def fix_pending_commissions():
288
    (accepted, rejected) = resolve_pending_commissions()
289

    
290
    with get_quota_holder() as qh:
291
        if accepted:
292
            qh.accept_commission(context={},
293
                                 clientkey='cyclades',
294
                                 serials=accepted)
295
        if rejected:
296
            qh.reject_commission(context={},
297
                                 clientkey='cyclades',
298
                                 serials=rejected)
299

    
300

    
301
def resolve_pending_commissions():
302
    """Resolve quotaholder pending commissions.
303

304
    Get pending commissions from the quotaholder and resolve them
305
    to accepted and rejected, according to the state of the
306
    QuotaHolderSerial DB table. A pending commission in the quotaholder
307
    can exist in the QuotaHolderSerial table and be either accepted or
308
    rejected, or can not exist in this table, so it is rejected.
309

310
    """
311

    
312
    qh_pending = get_quotaholder_pending()
313
    if not qh_pending:
314
        return ([], [])
315

    
316
    qh_pending.sort()
317
    min_ = qh_pending[0]
318

    
319
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
320
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
321
    accepted = filter(lambda x: x in qh_pending, accepted)
322

    
323
    rejected = list(set(qh_pending) - set(accepted))
324

    
325
    return (accepted, rejected)
326

    
327

    
328
def get_quotaholder_pending():
329
    with get_quota_holder() as qh:
330
        pending_serials = qh.get_pending_commissions(context={},
331
                                                     clientkey='cyclades')
332
    return pending_serials
333

    
334

    
335
def render_quotaholder_exception(e):
336
    resource_name = {"vm": "Virtual Machine",
337
                     "cpu": "CPU",
338
                     "ram": "RAM",
339
                     "network.private": "Private Network"}
340
    res = e.resource.replace("cyclades.", "", 1)
341
    try:
342
        resource = resource_name[res]
343
    except KeyError:
344
        resource = res
345

    
346
    requested = e.requested
347
    current = e.current
348
    limit = e.limit
349
    msg = "Resource Limit Exceeded for your account."
350
    details = "Limit for resource '%s' exceeded for your account."\
351
              " Current value: %s, Limit: %s, Requested: %s"\
352
              % (resource, current, limit, requested)
353
    return msg, details