Statistics
| Branch: | Tag: | Revision:

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

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
from kamaki.clients.commissioning import 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
    else:
114
        quotaholder = DummyQuotaholderClient()
115

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

    
121

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

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

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

132
    """
133

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

    
151

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

156
    @param serials: List of QuotaHolderSerial objects
157

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

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

    
170

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

174
    @param serials: List of QuotaHolderSerial objects
175

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

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

    
188

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

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

195
    """
196

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

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

    
214

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

    
220

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

    
225
    return issue_commission(**commission_info)
226

    
227

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

    
236

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

    
241
    return issue_commission(**commission_info)
242

    
243

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

    
247

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

    
251

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

    
266
##
267
## Reconcile pending commissions
268
##
269

    
270

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

    
277

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

    
284

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

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

    
298

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

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

308
    """
309

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

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

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

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

    
323
    return (accepted, rejected)
324

    
325

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

    
332

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

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