Statistics
| Branch: | Tag: | Revision:

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

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
                retry=3,
65
                logger=log)
66
        return cls._object
67

    
68

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

    
80

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

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

89
    """
90

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

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

    
105

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

    
109

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

    
113

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

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

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

    
130
    return response
131

    
132

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

    
137

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

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

147
    """
148

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

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

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

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

    
162
    return (accepted, rejected)
163

    
164

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

    
170

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

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

    
196

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

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

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

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

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

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

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

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

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

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

    
274
    return serial
275

    
276

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

    
291

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