Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.5 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 synnefo.lib.quotaholder.api import (NoCapacityError, NoQuantityError)
47
from synnefo.lib.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":
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()
77
            if resource == "cyclades.network.private":
78
                user_networks = Network.objects.filter(userid=userid,
79
                                                       deleted=False).count()
80
                user_network_limit = NETWORKS_USER_QUOTA.get(userid,
81
                                                         MAX_NETWORKS_PER_USER)
82
                if user_networks + size >= user_network_limit:
83
                    raise NoQuantityError()
84

    
85
        return None
86

    
87
    def accept_commission(self, *args, **kwargs):
88
        pass
89

    
90
    def reject_commission(self, *args, **kwargs):
91
        pass
92

    
93
    def get_pending_commissions(self, *args, **kwargs):
94
        return []
95

    
96

    
97
@contextmanager
98
def get_quota_holder():
99
    """Context manager for using a QuotaHolder."""
100
    if CYCLADES_USE_QUOTAHOLDER:
101
        quotaholder = QuotaholderClient(CYCLADES_QUOTAHOLDER_URL,
102
                                        token=CYCLADES_QUOTAHOLDER_TOKEN)
103
    else:
104
        quotaholder = DummyQuotaholderClient()
105

    
106
    try:
107
        yield quotaholder
108
    finally:
109
        pass
110

    
111

    
112
def uses_commission(func):
113
    """Decorator for wrapping functions that needs commission.
114

115
    All decorated functions must take as first argument the `serials` list in
116
    order to extend them with the needed serial numbers, as return by the
117
    Quotaholder
118

119
    On successful competition of the decorated function, all serials are
120
    accepted to the quotaholder, otherwise they are rejected.
121

122
    """
123

    
124
    @wraps(func)
125
    def wrapper(*args, **kwargs):
126
        try:
127
            serials = []
128
            ret = func(serials, *args, **kwargs)
129
            if serials:
130
                accept_commission(serials)
131
            return ret
132
        except CallError:
133
            log.exception("Unexpected error")
134
            raise
135
        except:
136
            if serials:
137
                reject_commission(serials=serials)
138
            raise
139
    return wrapper
140

    
141

    
142
## FIXME: Wrap the following two functions inside transaction ?
143
def accept_commission(serials, update_db=True):
144
    """Accept a list of pending commissions.
145

146
    @param serials: List of QuotaHolderSerial objects
147

148
    """
149
    if update_db:
150
        for s in serials:
151
            if s.pending:
152
                s.accepted = True
153
                s.save()
154

    
155
    with get_quota_holder() as qh:
156
        qh.accept_commission(context={},
157
                             clientkey='cyclades',
158
                             serials=[s.serial for s in serials])
159

    
160

    
161
def reject_commission(serials, update_db=True):
162
    """Reject a list of pending commissions.
163

164
    @param serials: List of QuotaHolderSerial objects
165

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

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

    
178

    
179
def issue_commission(**commission_info):
180
    """Issue a new commission to the quotaholder.
181

182
    Issue a new commission to the quotaholder, and create the
183
    corresponing QuotaHolderSerial object in DB.
184

185
    """
186

    
187
    with get_quota_holder() as qh:
188
        try:
189
            serial = qh.issue_commission(**commission_info)
190
        except (NoCapacityError, NoQuantityError):
191
            raise OverLimit("Limit exceeded for your account")
192
        except CallError as e:
193
            if e.call_error in ["NoCapacityError", "NoQuantityError"]:
194
                raise OverLimit("Limit exceeded for your account")
195
            raise
196

    
197
    if serial:
198
        return QuotaHolderSerial.objects.create(serial=serial)
199
    elif not CYCLADES_USE_QUOTAHOLDER:
200
        return DummySerial()
201
    else:
202
        raise Exception("No serial")
203

    
204

    
205
# Wrapper functions for issuing commissions for each resource type.  Each
206
# functions creates the `commission_info` dictionary as expected by the
207
# `issue_commision` function. Commissions for deleting a resource, are the same
208
# as for creating the same resource, but with negative resource sizes.
209

    
210

    
211
def issue_vm_commission(user, flavor, delete=False):
212
    resources = get_server_resources(flavor)
213
    commission_info = create_commission(user, resources, delete)
214

    
215
    return issue_commission(**commission_info)
216

    
217

    
218
def get_server_resources(flavor):
219
    return {'vm': 1,
220
            'cpu': flavor.cpu,
221
            'disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
222
            # 'public_ip': 1,
223
            #'disk_template': flavor.disk_template,
224
            'ram': 1048576 * flavor.ram}  # flavor.ram is in MB
225

    
226

    
227
def issue_network_commission(user, delete=False):
228
    resources = get_network_resources()
229
    commission_info = create_commission(user, resources, delete)
230

    
231
    return issue_commission(**commission_info)
232

    
233

    
234
def get_network_resources():
235
    return {"network.private": 1}
236

    
237

    
238
def invert_resources(resources_dict):
239
    return dict((r, -s) for r, s in resources_dict.items())
240

    
241

    
242
def create_commission(user, resources, delete=False):
243
    if delete:
244
        resources = invert_resources(resources)
245
    provisions = [('cyclades', 'cyclades.' + r, s)
246
                  for r, s in resources.items()]
247
    return  {"context":    {},
248
             "target":     user,
249
             "key":        "1",
250
             "clientkey":  "cyclades",
251
             #"owner":      "",
252
             #"ownerkey":   "1",
253
             "name":       "",
254
             "provisions": provisions}
255

    
256
##
257
## Reconcile pending commissions
258
##
259

    
260

    
261
def accept_commissions(accepted):
262
    with get_quota_holder() as qh:
263
        qh.accept_commission(context={},
264
                             clientkey='cyclades',
265
                             serials=accepted)
266

    
267

    
268
def reject_commissions(rejected):
269
    with get_quota_holder() as qh:
270
            qh.reject_commission(context={},
271
                                 clientkey='cyclades',
272
                                 serials=rejected)
273

    
274

    
275
def fix_pending_commissions():
276
    (accepted, rejected) = resolve_pending_commissions()
277

    
278
    with get_quota_holder() as qh:
279
        if accepted:
280
            qh.accept_commission(context={},
281
                                 clientkey='cyclades',
282
                                 serials=accepted)
283
        if rejected:
284
            qh.reject_commission(context={},
285
                                 clientkey='cyclades',
286
                                 serials=rejected)
287

    
288

    
289
def resolve_pending_commissions():
290
    """Resolve quotaholder pending commissions.
291

292
    Get pending commissions from the quotaholder and resolve them
293
    to accepted and rejected, according to the state of the
294
    QuotaHolderSerial DB table. A pending commission in the quotaholder
295
    can exist in the QuotaHolderSerial table and be either accepted or
296
    rejected, or can not exist in this table, so it is rejected.
297

298
    """
299

    
300
    qh_pending = get_quotaholder_pending()
301
    if not qh_pending:
302
        return ([], [])
303

    
304
    qh_pending.sort()
305
    min_ = qh_pending[0]
306

    
307
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
308
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
309
    accepted = filter(lambda x: x in qh_pending, accepted)
310

    
311
    rejected = list(set(qh_pending) - set(accepted))
312

    
313
    return (accepted, rejected)
314

    
315

    
316
def get_quotaholder_pending():
317
    with get_quota_holder() as qh:
318
        pending_serials = qh.get_pending_commissions(context={},
319
                                                     clientkey='cyclades')
320
    return pending_serials