Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / quotas / __init__.py @ 18c4414d

History | View | Annotate | Download (9.6 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 django.utils import simplejson as json
31
from django.db import transaction
32

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

    
36
from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN,
37
                              ASTAKOS_BASE_URL)
38
from astakosclient import AstakosClient
39
from astakosclient.errors import AstakosClientException, QuotaLimit
40
from functools import wraps
41

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

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

    
54

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

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

    
67

    
68
def handle_astakosclient_error(func):
69
    """Decorator for converting astakosclient errors to 500."""
70
    @wraps(func)
71
    def wrapper(*args, **kwargs):
72
        try:
73
            return func(*args, **kwargs)
74
        except AstakosClientException:
75
            log.exception("Unexpected error")
76
            raise faults.InternalServerError("Unexpected error")
77
    return wrapper
78

    
79

    
80
@handle_astakosclient_error
81
def issue_commission(user, source, provisions,
82
                     force=False, auto_accept=False):
83
    """Issue a new commission to the quotaholder.
84

85
    Issue a new commission to the quotaholder, and create the
86
    corresponing QuotaHolderSerial object in DB.
87

88
    """
89

    
90
    qh = Quotaholder.get()
91
    try:
92
        serial = qh.issue_one_commission(ASTAKOS_TOKEN,
93
                                         user, source, provisions,
94
                                         force=force, auto_accept=auto_accept)
95
    except QuotaLimit as e:
96
        msg, details = render_overlimit_exception(e)
97
        raise faults.OverLimit(msg, details=details)
98

    
99
    if serial:
100
        return QuotaHolderSerial.objects.create(serial=serial)
101
    else:
102
        raise Exception("No serial")
103

    
104

    
105
def accept_commissions(accepted, strict=True):
106
    return resolve_commissions(accept=accepted, strict=strict)
107

    
108

    
109
def reject_commissions(rejected, strict=True):
110
    return resolve_commissions(reject=rejected, strict=strict)
111

    
112

    
113
@handle_astakosclient_error
114
def resolve_commissions(accept=None, reject=None, strict=True):
115
    if accept is None:
116
        accept = []
117
    if reject is None:
118
        reject = []
119

    
120
    qh = Quotaholder.get()
121
    response = qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
122

    
123
    if strict:
124
        failed = response["failed"]
125
        if failed:
126
            log.error("Unexpected error while resolving commissions: %s",
127
                      failed)
128

    
129
    return response
130

    
131

    
132
def fix_pending_commissions():
133
    (accepted, rejected) = resolve_pending_commissions()
134
    resolve_commissions(accept=accepted, reject=rejected)
135

    
136

    
137
def resolve_pending_commissions():
138
    """Resolve quotaholder pending commissions.
139

140
    Get pending commissions from the quotaholder and resolve them
141
    to accepted and rejected, according to the state of the
142
    QuotaHolderSerial DB table. A pending commission in the quotaholder
143
    can exist in the QuotaHolderSerial table and be either accepted or
144
    rejected, or can not exist in this table, so it is rejected.
145

146
    """
147

    
148
    qh_pending = get_quotaholder_pending()
149
    if not qh_pending:
150
        return ([], [])
151

    
152
    qh_pending.sort()
153
    min_ = qh_pending[0]
154

    
155
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
156
    accepted = serials.filter(accept=True).values_list('serial', flat=True)
157
    accepted = filter(lambda x: x in qh_pending, accepted)
158

    
159
    rejected = list(set(qh_pending) - set(accepted))
160

    
161
    return (accepted, rejected)
162

    
163

    
164
def get_quotaholder_pending():
165
    qh = Quotaholder.get()
166
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
167
    return pending_serials
168

    
169

    
170
def render_overlimit_exception(e):
171
    resource_name = {"vm": "Virtual Machine",
172
                     "cpu": "CPU",
173
                     "ram": "RAM",
174
                     "network.private": "Private Network"}
175
    details = json.loads(e.details)
176
    data = details['overLimit']['data']
177
    usage = data["usage"]
178
    limit = data["limit"]
179
    available = limit - usage
180
    provision = data['provision']
181
    requested = provision['quantity']
182
    resource = provision['resource']
183
    res = resource.replace("cyclades.", "", 1)
184
    try:
185
        resource = resource_name[res]
186
    except KeyError:
187
        resource = res
188

    
189
    msg = "Resource Limit Exceeded for your account."
190
    details = "Limit for resource '%s' exceeded for your account."\
191
              " Available: %s, Requested: %s"\
192
              % (resource, available, requested)
193
    return msg, details
194

    
195

    
196
@transaction.commit_manually
197
def issue_and_accept_commission(resource, delete=False):
198
    """Issue and accept a commission to Quotaholder.
199

200
    This function implements the Commission workflow, and must be called
201
    exactly after and in the same transaction that created/updated the
202
    resource. The workflow that implements is the following:
203
    0) Resolve previous unresolved commission if exists
204
    1) Issue commission and get a serial
205
    2) Store the serial in DB and mark is as one to accept
206
    3) Correlate the serial with the resource
207
    4) COMMIT!
208
    5) Accept commission to QH (reject if failed until 5)
209
    6) Mark serial as resolved
210
    7) COMMIT!
211

212
    """
213
    previous_serial = resource.serial
214
    if previous_serial is not None and not previous_serial.resolved:
215
        if previous_serial.pending:
216
            msg = "Issuing commission for resource '%s' while previous serial"\
217
                  " '%s' is still pending." % (resource, previous_serial)
218
            raise Exception(msg)
219
        elif previous_serial.accept:
220
            accept_commissions(accepted=[previous_serial.serial], strict=False)
221
        else:
222
            reject_commissions(rejected=[previous_serial.serial], strict=False)
223
        previous_serial.resolved = True
224

    
225
    try:
226
        # Convert resources in the format expected by Quotaholder
227
        qh_resources = prepare_qh_resources(resource)
228
        if delete:
229
            qh_resources = reverse_quantities(qh_resources)
230

    
231
        # Issue commission and get the assigned serial
232
        serial = issue_commission(resource.userid, DEFAULT_SOURCE,
233
                                  qh_resources)
234
    except:
235
        transaction.rollback()
236
        raise
237

    
238
    try:
239
        # Mark the serial as one to accept. This step is necessary for
240
        # reconciliation
241
        serial.pending = False
242
        serial.accept = True
243
        serial.save()
244

    
245
        # Associate serial with the resource
246
        resource.serial = serial
247
        resource.save()
248

    
249
        # Commit transaction in the DB! If this commit succeeds, then the
250
        # serial is created in the DB with all the necessary information to
251
        # reconcile commission
252
        transaction.commit()
253
    except:
254
        transaction.rollback()
255
        serial.pending = False
256
        serial.accept = False
257
        serial.save()
258
        transaction.commit()
259
        raise
260

    
261
    if serial.accept:
262
        # Accept commission to Quotaholder
263
        accept_commissions(accepted=[serial.serial])
264
    else:
265
        reject_commissions(rejected=[serial.serial])
266

    
267
    # Mark the serial as resolved, indicating that no further actions are
268
    # needed for this serial
269
    serial.resolved = True
270
    serial.save()
271
    transaction.commit()
272

    
273
    return serial
274

    
275

    
276
def prepare_qh_resources(resource):
277
    if isinstance(resource, VirtualMachine):
278
        flavor = resource.flavor
279
        return {'cyclades.vm': 1,
280
                'cyclades.cpu': flavor.cpu,
281
                'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk in GB
282
                # 'public_ip': 1,
283
                #'disk_template': flavor.disk_template,
284
                'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
285
    elif isinstance(resource, Network):
286
        return {"cyclades.network.private": 1}
287
    else:
288
        raise ValueError("Unknown Resource '%s'" % resource)
289

    
290

    
291
def reverse_quantities(resources):
292
    return dict((r, -s) for r, s in resources.items())