Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.5 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 contextlib import contextmanager
32

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

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

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

    
45
DEFAULT_SOURCE = 'system'
46

    
47

    
48
class Quotaholder(object):
49
    _object = None
50

    
51
    @classmethod
52
    def get(cls):
53
        if cls._object is None:
54
            cls._object = AstakosClient(
55
                ASTAKOS_URL,
56
                use_pool=True,
57
                logger=log)
58
        return cls._object
59

    
60

    
61
def uses_commission(func):
62
    """Decorator for wrapping functions that needs commission.
63

64
    All decorated functions must take as first argument the `serials` list in
65
    order to extend them with the needed serial numbers, as return by the
66
    Quotaholder
67

68
    On successful competition of the decorated function, all serials are
69
    accepted to the quotaholder, otherwise they are rejected.
70

71
    """
72

    
73
    @wraps(func)
74
    def wrapper(*args, **kwargs):
75
        try:
76
            serials = []
77
            ret = func(serials, *args, **kwargs)
78
        except:
79
            log.exception("Unexpected error")
80
            try:
81
                if serials:
82
                    reject_commission(serials=serials)
83
            except:
84
                log.exception("Exception while rejecting serials %s", serials)
85
                raise
86
            raise
87

    
88
        # func has completed successfully. accept serials
89
        try:
90
            if serials:
91
                accept_commission(serials)
92
            return ret
93
        except:
94
            log.exception("Exception while accepting serials %s", serials)
95
            raise
96
    return wrapper
97

    
98

    
99
## FIXME: Wrap the following two functions inside transaction ?
100
def accept_commission(serials, update_db=True):
101
    """Accept a list of pending commissions.
102

103
    @param serials: List of QuotaHolderSerial objects
104

105
    """
106
    if update_db:
107
        for s in serials:
108
            if s.pending:
109
                s.accepted = True
110
                s.save()
111

    
112
    accept_serials = [s.serial for s in serials]
113
    qh_resolve_commissions(accept=accept_serials)
114

    
115

    
116
def reject_commission(serials, update_db=True):
117
    """Reject a list of pending commissions.
118

119
    @param serials: List of QuotaHolderSerial objects
120

121
    """
122
    if update_db:
123
        for s in serials:
124
            if s.pending:
125
                s.rejected = True
126
                s.save()
127

    
128
    reject_serials = [s.serial for s in serials]
129
    qh_resolve_commissions(reject=reject_serials)
130

    
131

    
132
def issue_commission(user, source, provisions,
133
                     force=False, auto_accept=False):
134
    """Issue a new commission to the quotaholder.
135

136
    Issue a new commission to the quotaholder, and create the
137
    corresponing QuotaHolderSerial object in DB.
138

139
    """
140

    
141
    qh = Quotaholder.get()
142
    try:
143
        serial = qh.issue_one_commission(ASTAKOS_TOKEN,
144
                                         user, source, provisions,
145
                                         force, auto_accept)
146
    except QuotaLimit as e:
147
        msg, details = render_overlimit_exception(e)
148
        raise faults.OverLimit(msg, details=details)
149
    except AstakosClientException as e:
150
        log.exception("Unexpected error")
151
        raise
152

    
153
    if serial:
154
        return QuotaHolderSerial.objects.create(serial=serial)
155
    else:
156
        raise Exception("No serial")
157

    
158

    
159
# Wrapper functions for issuing commissions for each resource type.  Each
160
# functions creates the `commission_info` dictionary as expected by the
161
# `issue_commision` function. Commissions for deleting a resource, are the same
162
# as for creating the same resource, but with negative resource sizes.
163

    
164

    
165
def issue_vm_commission(user, flavor, delete=False):
166
    resources = prepare(get_server_resources(flavor), delete)
167
    return issue_commission(user, DEFAULT_SOURCE, resources)
168

    
169

    
170
def get_server_resources(flavor):
171
    return {'vm': 1,
172
            'cpu': flavor.cpu,
173
            'disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
174
            # 'public_ip': 1,
175
            #'disk_template': flavor.disk_template,
176
            'ram': 1048576 * flavor.ram}  # flavor.ram is in MB
177

    
178

    
179
def issue_network_commission(user, delete=False):
180
    resources = prepare(get_network_resources(), delete)
181
    return issue_commission(user, DEFAULT_SOURCE, resources)
182

    
183

    
184
def get_network_resources():
185
    return {"network.private": 1}
186

    
187

    
188
def prepare(resources_dict, delete):
189
    if delete:
190
        return dict((r, -s) for r, s in resources_dict.items())
191
    return resources_dict
192

    
193

    
194
##
195
## Reconcile pending commissions
196
##
197

    
198

    
199
def accept_commissions(accepted):
200
    qh_resolve_commissions(accept=accepted)
201

    
202

    
203
def reject_commissions(rejected):
204
    qh_resolve_commissions(reject=rejected)
205

    
206

    
207
def fix_pending_commissions():
208
    (accepted, rejected) = resolve_pending_commissions()
209
    qh_resolve_commissions(accepted, rejected)
210

    
211

    
212
def qh_resolve_commissions(accept=None, reject=None):
213
    if accept is None:
214
        accept = []
215
    if reject is None:
216
        reject = []
217

    
218
    qh = Quotaholder.get()
219
    qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
220

    
221

    
222
def resolve_pending_commissions():
223
    """Resolve quotaholder pending commissions.
224

225
    Get pending commissions from the quotaholder and resolve them
226
    to accepted and rejected, according to the state of the
227
    QuotaHolderSerial DB table. A pending commission in the quotaholder
228
    can exist in the QuotaHolderSerial table and be either accepted or
229
    rejected, or can not exist in this table, so it is rejected.
230

231
    """
232

    
233
    qh_pending = get_quotaholder_pending()
234
    if not qh_pending:
235
        return ([], [])
236

    
237
    qh_pending.sort()
238
    min_ = qh_pending[0]
239

    
240
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
241
    accepted = serials.filter(accepted=True).values_list('serial', flat=True)
242
    accepted = filter(lambda x: x in qh_pending, accepted)
243

    
244
    rejected = list(set(qh_pending) - set(accepted))
245

    
246
    return (accepted, rejected)
247

    
248

    
249
def get_quotaholder_pending():
250
    qh = Quotaholder.get()
251
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
252
    return pending_serials
253

    
254

    
255
def render_overlimit_exception(e):
256
    resource_name = {"vm": "Virtual Machine",
257
                     "cpu": "CPU",
258
                     "ram": "RAM",
259
                     "network.private": "Private Network"}
260
    details = e.details
261
    data = details['overLimit']['data']
262
    available = data['available']
263
    provision = data['provision']
264
    requested = provision['quantity']
265
    resource = provision['resource']
266
    res = resource.replace("cyclades.", "", 1)
267
    try:
268
        resource = resource_name[res]
269
    except KeyError:
270
        resource = res
271

    
272
    msg = "Resource Limit Exceeded for your account."
273
    details = "Limit for resource '%s' exceeded for your account."\
274
              " Available: %s, Requested: %s"\
275
              % (resource, available, requested)
276
    return msg, details