Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (10.2 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 synnefo.settings import (CYCLADES_QUOTAHOLDER_URL, USE_QUOTAHOLDER,
34
                              VMS_USER_QUOTA, MAX_VMS_PER_USER,
35
                              NETWORKS_USER_QUOTA, MAX_NETWORKS_PER_USER)
36

    
37
from synnefo.db.models import QuotaHolderSerial, VirtualMachine, Network
38
from synnefo.api.faults import OverLimit
39

    
40
from kamaki.clients.quotaholder import QuotaholderClient
41
from synnefo.lib.quotaholder.api import (NoCapacityError, NoQuantityError)
42
from synnefo.lib.commissioning import CallError
43

    
44
import logging
45
log = logging.getLogger(__name__)
46

    
47

    
48
class DummySerial(QuotaHolderSerial):
49
    accepted = True
50
    rejected = True
51
    pending = True
52
    id = None
53

    
54
    def save(*args, **kwargs):
55
        pass
56

    
57

    
58
class DummyQuotaholderClient(object):
59
    def issue_commission(self, **commission_info):
60
        provisions = commission_info["provisions"]
61
        userid = commission_info["target"]
62
        for provision in provisions:
63
            entity, resource, size = provision
64
            if resource == "cyclades.vm":
65
                user_vms = VirtualMachine.objects.filter(userid=userid,
66
                                                         deleted=False).count()
67
                user_vm_limit = VMS_USER_QUOTA.get(userid, MAX_VMS_PER_USER)
68
                log.warning("Users VMs %s User Limits %s", user_vms,
69
                        user_vm_limit)
70
                if user_vms + size >= user_vm_limit:
71
                    raise NoQuantityError()
72
            if resource == "cyclades.network.private":
73
                user_networks = Network.objects.filter(userid=userid,
74
                                                       deleted=False).count()
75
                user_network_limit = NETWORKS_USER_QUOTA.get(userid,
76
                                                         MAX_NETWORKS_PER_USER)
77
                if user_networks + size >= user_network_limit:
78
                    raise NoQuantityError()
79

    
80
        return None
81

    
82
    def accept_commission(self, *args, **kwargs):
83
        pass
84

    
85
    def reject_commission(self, *args, **kwargs):
86
        pass
87

    
88
    def get_pending_commissions(self, *args, **kwargs):
89
        return []
90

    
91

    
92
@contextmanager
93
def get_quota_holder():
94
    """Context manager for using a QuotaHolder."""
95
    if USE_QUOTAHOLDER:
96
        quotaholder = QuotaholderClient(CYCLADES_QUOTAHOLDER_URL)
97
    else:
98
        quotaholder = DummyQuotaholderClient()
99

    
100
    try:
101
        yield quotaholder
102
    finally:
103
        pass
104

    
105

    
106
def uses_commission(func):
107
    """Decorator for wrapping functions that needs commission.
108

109
    All decorated functions must take as first argument the `serials` list in
110
    order to extend them with the needed serial numbers, as return by the
111
    Quotaholder
112

113
    On successful competition of the decorated function, all serials are
114
    accepted to the quotaholder, otherwise they are rejected.
115

116
    """
117

    
118
    @wraps(func)
119
    def wrapper(*args, **kwargs):
120
        try:
121
            serials = []
122
            ret = func(serials, *args, **kwargs)
123
            if serials:
124
                accept_commission(serials)
125
            return ret
126
        except CallError:
127
            log.exception("Unexpected error")
128
            raise
129
        except:
130
            if serials:
131
                reject_commission(serials=serials)
132
            raise
133
    return wrapper
134

    
135

    
136
## FIXME: Wrap the following two functions inside transaction ?
137
def accept_commission(serials, update_db=True):
138
    """Accept a list of pending commissions.
139

140
    @param serials: List of QuotaHolderSerial objects
141

142
    """
143
    if update_db:
144
        for s in serials:
145
            if s.pending:
146
                s.accepted = True
147
                s.save()
148

    
149
    with get_quota_holder() as qh:
150
        qh.accept_commission(context={},
151
                             clientkey='cyclades',
152
                             serials=[s.serial for s in serials])
153

    
154

    
155
def reject_commission(serials, update_db=True):
156
    """Reject 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.rejected = True
165
                s.save()
166

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

    
172

    
173
def issue_commission(**commission_info):
174
    """Issue a new commission to the quotaholder.
175

176
    Issue a new commission to the quotaholder, and create the
177
    corresponing QuotaHolderSerial object in DB.
178

179
    """
180

    
181
    with get_quota_holder() as qh:
182
        try:
183
            serial = qh.issue_commission(**commission_info)
184
        except (NoCapacityError, NoQuantityError):
185
            raise OverLimit("Limit exceeded for your account")
186
        except CallError as e:
187
            if e.call_error in ["NoCapacityError", "NoQuantityError"]:
188
                raise OverLimit("Limit exceeded for your account")
189

    
190
    if serial:
191
        return QuotaHolderSerial.objects.create(serial=serial)
192
    elif not USE_QUOTAHOLDER:
193
        return DummySerial()
194
    else:
195
        raise Exception("No serial")
196

    
197

    
198
# Wrapper functions for issuing commissions for each resource type.  Each
199
# functions creates the `commission_info` dictionary as expected by the
200
# `issue_commision` function. Commissions for deleting a resource, are the same
201
# as for creating the same resource, but with negative resource sizes.
202

    
203

    
204
def issue_vm_commission(user, flavor, delete=False):
205
    resources = get_server_resources(flavor)
206
    commission_info = create_commission(user, resources, delete)
207

    
208
    return issue_commission(**commission_info)
209

    
210

    
211
def get_server_resources(flavor):
212
    return {'vm': 1,
213
            'cpu': flavor.cpu,
214
            'disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
215
            # 'public_ip': 1,
216
            #'disk_template': flavor.disk_template,
217
            'ram': 1048576 * flavor.ram}  # flavor.ram is in MB
218

    
219

    
220
def issue_network_commission(user, delete=False):
221
    resources = get_network_resources()
222
    commission_info = create_commission(user, resources, delete)
223

    
224
    return issue_commission(**commission_info)
225

    
226

    
227
def get_network_resources():
228
    return {"network.private": 1}
229

    
230

    
231
def invert_resources(resources_dict):
232
    return dict((r, -s) for r, s in resources_dict.items())
233

    
234

    
235
def create_commission(user, resources, delete=False):
236
    if delete:
237
        resources = invert_resources(resources)
238
    provisions = [('cyclades', 'cyclades.' + r, s)
239
                  for r, s in resources.items()]
240
    return  {"context":    {},
241
             "target":     user,
242
             "key":        "1",
243
             "clientkey":  "cyclades",
244
             #"owner":      "",
245
             #"ownerkey":   "1",
246
             "name":       "",
247
             "provisions": provisions}
248

    
249
##
250
## Reconcile pending commissions
251
##
252

    
253

    
254
def accept_commissions(accepted):
255
    with get_quota_holder() as qh:
256
        qh.accept_commission(context={},
257
                             clientkey='cyclades',
258
                             serials=accepted)
259

    
260

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

    
267

    
268
def fix_pending_commissions():
269
    (accepted, rejected) = resolve_pending_commissions()
270

    
271
    with get_quota_holder() as qh:
272
        if accepted:
273
            qh.accept_commission(context={},
274
                                 clientkey='cyclades',
275
                                 serials=accepted)
276
        if rejected:
277
            qh.reject_commission(context={},
278
                                 clientkey='cyclades',
279
                                 serials=rejected)
280

    
281

    
282
def resolve_pending_commissions():
283
    """Resolve quotaholder pending commissions.
284

285
    Get pending commissions from the quotaholder and resolve them
286
    to accepted and rejected, according to the state of the
287
    QuotaHolderSerial DB table. A pending commission in the quotaholder
288
    can exist in the QuotaHolderSerial table and be either accepted or
289
    rejected, or can not exist in this table, so it is rejected.
290

291
    """
292

    
293
    qh_pending = get_quotaholder_pending()
294
    if not qh_pending:
295
        return ([], [])
296

    
297
    qh_pending.sort()
298
    min_ = qh_pending[0]
299

    
300
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
301
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
302
    accepted = filter(lambda x: x in qh_pending, accepted)
303

    
304
    rejected = list(set(qh_pending) - set(accepted))
305

    
306
    return (accepted, rejected)
307

    
308

    
309
def get_quotaholder_pending():
310
    with get_quota_holder() as qh:
311
        pending_serials = qh.get_pending_commissions(context={},
312
                                                     clientkey='cyclades')
313
    return pending_serials