Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.7 kB)

1
# Copyright 2012, 2013 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 django.utils import simplejson as json
32

    
33
from snf_django.lib.api import faults
34
from synnefo.db.models import QuotaHolderSerial
35

    
36
from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
37
                              ASTAKOS_URL)
38
from astakosclient import AstakosClient
39
from astakosclient.errors import AstakosClientException, QuotaLimit
40

    
41
import logging
42
log = logging.getLogger(__name__)
43

    
44
DEFAULT_SOURCE = 'system'
45
RESOURCES = [
46
    "cyclades.vm",
47
    "cyclades.cpu",
48
    "cyclades.disk",
49
    "cyclades.ram",
50
    "cyclades.network.private"
51
]
52

    
53

    
54
class Quotaholder(object):
55
    _object = None
56

    
57
    @classmethod
58
    def get(cls):
59
        if cls._object is None:
60
            cls._object = AstakosClient(
61
                ASTAKOS_URL,
62
                use_pool=True,
63
                logger=log)
64
        return cls._object
65

    
66

    
67
def uses_commission(func):
68
    """Decorator for wrapping functions that needs commission.
69

70
    All decorated functions must take as first argument the `serials` list in
71
    order to extend them with the needed serial numbers, as return by the
72
    Quotaholder
73

74
    On successful competition of the decorated function, all serials are
75
    accepted to the quotaholder, otherwise they are rejected.
76

77
    """
78

    
79
    @wraps(func)
80
    def wrapper(*args, **kwargs):
81
        try:
82
            serials = []
83
            ret = func(serials, *args, **kwargs)
84
        except:
85
            log.exception("Unexpected error")
86
            try:
87
                if serials:
88
                    reject_commission(serials=serials)
89
            except:
90
                log.exception("Exception while rejecting serials %s", serials)
91
                raise
92
            raise
93

    
94
        # func has completed successfully. accept serials
95
        try:
96
            if serials:
97
                accept_commission(serials)
98
            return ret
99
        except:
100
            log.exception("Exception while accepting serials %s", serials)
101
            raise
102
    return wrapper
103

    
104

    
105
## FIXME: Wrap the following two functions inside transaction ?
106
def accept_commission(serials, update_db=True):
107
    """Accept a list of pending commissions.
108

109
    @param serials: List of QuotaHolderSerial objects
110

111
    """
112
    if update_db:
113
        for s in serials:
114
            if s.pending:
115
                s.accepted = True
116
                s.save()
117

    
118
    accept_serials = [s.serial for s in serials]
119
    qh_resolve_commissions(accept=accept_serials)
120

    
121

    
122
def reject_commission(serials, update_db=True):
123
    """Reject a list of pending commissions.
124

125
    @param serials: List of QuotaHolderSerial objects
126

127
    """
128
    if update_db:
129
        for s in serials:
130
            if s.pending:
131
                s.rejected = True
132
                s.save()
133

    
134
    reject_serials = [s.serial for s in serials]
135
    qh_resolve_commissions(reject=reject_serials)
136

    
137

    
138
def issue_commission(user, source, provisions,
139
                     force=False, auto_accept=False):
140
    """Issue a new commission to the quotaholder.
141

142
    Issue a new commission to the quotaholder, and create the
143
    corresponing QuotaHolderSerial object in DB.
144

145
    """
146

    
147
    qh = Quotaholder.get()
148
    try:
149
        serial = qh.issue_one_commission(ASTAKOS_TOKEN,
150
                                         user, source, provisions,
151
                                         force=force, auto_accept=auto_accept)
152
    except QuotaLimit as e:
153
        msg, details = render_overlimit_exception(e)
154
        raise faults.OverLimit(msg, details=details)
155
    except AstakosClientException as e:
156
        log.exception("Unexpected error")
157
        raise
158

    
159
    if serial:
160
        return QuotaHolderSerial.objects.create(serial=serial)
161
    else:
162
        raise Exception("No serial")
163

    
164

    
165
# Wrapper functions for issuing commissions for each resource type.  Each
166
# functions creates the `commission_info` dictionary as expected by the
167
# `issue_commision` function. Commissions for deleting a resource, are the same
168
# as for creating the same resource, but with negative resource sizes.
169

    
170

    
171
def issue_vm_commission(user, flavor, delete=False):
172
    resources = get_server_resources(flavor)
173
    if delete:
174
        resources = reverse_quantities(resources)
175
    return issue_commission(user, DEFAULT_SOURCE, resources)
176

    
177

    
178
def get_server_resources(flavor):
179
    return {'cyclades.vm': 1,
180
            'cyclades.cpu': flavor.cpu,
181
            'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
182
            # 'public_ip': 1,
183
            #'disk_template': flavor.disk_template,
184
            'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
185

    
186

    
187
def issue_network_commission(user, delete=False):
188
    resources = get_network_resources()
189
    if delete:
190
        resources = reverse_quantities(resources)
191
    return issue_commission(user, DEFAULT_SOURCE, resources)
192

    
193

    
194
def get_network_resources():
195
    return {"cyclades.network.private": 1}
196

    
197

    
198
def reverse_quantities(resources):
199
    return dict((r, -s) for r, s in resources.items())
200

    
201

    
202
##
203
## Reconcile pending commissions
204
##
205

    
206

    
207
def accept_commissions(accepted):
208
    qh_resolve_commissions(accept=accepted)
209

    
210

    
211
def reject_commissions(rejected):
212
    qh_resolve_commissions(reject=rejected)
213

    
214

    
215
def fix_pending_commissions():
216
    (accepted, rejected) = resolve_pending_commissions()
217
    qh_resolve_commissions(accepted, rejected)
218

    
219

    
220
def qh_resolve_commissions(accept=None, reject=None):
221
    if accept is None:
222
        accept = []
223
    if reject is None:
224
        reject = []
225

    
226
    qh = Quotaholder.get()
227
    qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
228

    
229

    
230
def resolve_pending_commissions():
231
    """Resolve quotaholder pending commissions.
232

233
    Get pending commissions from the quotaholder and resolve them
234
    to accepted and rejected, according to the state of the
235
    QuotaHolderSerial DB table. A pending commission in the quotaholder
236
    can exist in the QuotaHolderSerial table and be either accepted or
237
    rejected, or can not exist in this table, so it is rejected.
238

239
    """
240

    
241
    qh_pending = get_quotaholder_pending()
242
    if not qh_pending:
243
        return ([], [])
244

    
245
    qh_pending.sort()
246
    min_ = qh_pending[0]
247

    
248
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
249
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
250
    accepted = filter(lambda x: x in qh_pending, accepted)
251

    
252
    rejected = list(set(qh_pending) - set(accepted))
253

    
254
    return (accepted, rejected)
255

    
256

    
257
def get_quotaholder_pending():
258
    qh = Quotaholder.get()
259
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
260
    return pending_serials
261

    
262

    
263
def render_overlimit_exception(e):
264
    resource_name = {"vm": "Virtual Machine",
265
                     "cpu": "CPU",
266
                     "ram": "RAM",
267
                     "network.private": "Private Network"}
268
    details = json.loads(e.details)
269
    data = details['overLimit']['data']
270
    usage = data["usage"]
271
    limit = data["limit"]
272
    available = limit - usage
273
    provision = data['provision']
274
    requested = provision['quantity']
275
    resource = provision['resource']
276
    res = resource.replace("cyclades.", "", 1)
277
    try:
278
        resource = resource_name[res]
279
    except KeyError:
280
        resource = res
281

    
282
    msg = "Resource Limit Exceeded for your account."
283
    details = "Limit for resource '%s' exceeded for your account."\
284
              " Available: %s, Requested: %s"\
285
              % (resource, available, requested)
286
    return msg, details