Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (11.7 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
    from kamaki.clients.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 kamaki.clients.quotaholder.api import (NoCapacityError, NoQuantityError,
47
                                            NoEntityError)
48
from kamaki.clients.commissioning import 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
    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
            if serials:
141
                accept_commission(serials)
142
            return ret
143
        except CallError:
144
            log.exception("Unexpected error")
145
            raise
146
        except:
147
            if serials:
148
                reject_commission(serials=serials)
149
            raise
150
    return wrapper
151

    
152

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

157
    @param serials: List of QuotaHolderSerial objects
158

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

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

    
171

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

175
    @param serials: List of QuotaHolderSerial objects
176

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

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

    
189

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

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

196
    """
197

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

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

    
215

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

    
221

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

    
226
    return issue_commission(**commission_info)
227

    
228

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

    
237

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

    
242
    return issue_commission(**commission_info)
243

    
244

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

    
248

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

    
252

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

    
267
##
268
## Reconcile pending commissions
269
##
270

    
271

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

    
278

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

    
285

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

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

    
299

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

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

309
    """
310

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

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

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

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

    
324
    return (accepted, rejected)
325

    
326

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

    
333

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

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