Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.9 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
                               IPAddress)
36

    
37
from synnefo.settings import (CYCLADES_SERVICE_TOKEN as ASTAKOS_TOKEN,
38
                              ASTAKOS_AUTH_URL)
39
from astakosclient import AstakosClient
40
from astakosclient import errors
41

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

    
45

    
46
DEFAULT_SOURCE = 'system'
47
RESOURCES = [
48
    "cyclades.vm",
49
    "cyclades.total_cpu",
50
    "cyclades.cpu",
51
    "cyclades.disk",
52
    "cyclades.total_ram",
53
    "cyclades.ram",
54
    "cyclades.network.private",
55
    "cyclades.floating_ip",
56
]
57

    
58

    
59
class Quotaholder(object):
60
    _object = None
61

    
62
    @classmethod
63
    def get(cls):
64
        if cls._object is None:
65
            cls._object = AstakosClient(ASTAKOS_TOKEN,
66
                                        ASTAKOS_AUTH_URL,
67
                                        use_pool=True,
68
                                        retry=3,
69
                                        logger=log)
70
        return cls._object
71

    
72

    
73
class AstakosClientExceptionHandler(object):
74
    def __init__(self, *args, **kwargs):
75
        pass
76

    
77
    def __enter__(self):
78
        pass
79

    
80
    def __exit__(self, exc_type, value, traceback):
81
        if value is not None:  # exception
82
            if not isinstance(value, errors.AstakosClientException):
83
                return False  # reraise
84
            if exc_type is errors.QuotaLimit:
85
                msg, details = render_overlimit_exception(value)
86
                raise faults.OverLimit(msg, details=details)
87

    
88
            log.exception("Unexpected error %s" % value.message)
89
            raise faults.InternalServerError("Unexpected error")
90

    
91

    
92
def issue_commission(resource, action, name="", force=False, auto_accept=False,
93
                     action_fields=None):
94
    """Issue a new commission to the quotaholder.
95

96
    Issue a new commission to the quotaholder, and create the
97
    corresponing QuotaHolderSerial object in DB.
98

99
    """
100

    
101
    provisions = get_commission_info(resource=resource, action=action,
102
                                     action_fields=action_fields)
103

    
104
    if provisions is None:
105
        return None
106

    
107
    user = resource.userid
108
    source = DEFAULT_SOURCE
109

    
110
    qh = Quotaholder.get()
111
    if True:  # placeholder
112
        with AstakosClientExceptionHandler():
113
            serial = qh.issue_one_commission(user, source,
114
                                             provisions, name=name,
115
                                             force=force,
116
                                             auto_accept=auto_accept)
117

    
118
    if serial:
119
        serial_info = {"serial": serial}
120
        if auto_accept:
121
            serial_info["pending"] = False
122
            serial_info["accept"] = True
123
            serial_info["resolved"] = True
124
        return QuotaHolderSerial.objects.create(**serial_info)
125
    else:
126
        raise Exception("No serial")
127

    
128

    
129
def accept_serial(serial, strict=True):
130
    assert serial.pending or serial.accept, "%s can't be accepted" % serial
131
    response = resolve_commissions(accept=[serial.serial], strict=strict)
132
    return response
133

    
134

    
135
def reject_serial(serial, strict=True):
136
    assert serial.pending or not serial.accept, "%s can't be rejected" % serial
137
    response = resolve_commissions(reject=[serial.serial], strict=strict)
138
    return response
139

    
140

    
141
def resolve_commissions(accept=None, reject=None, strict=True):
142
    if accept is None:
143
        accept = []
144
    if reject is None:
145
        reject = []
146

    
147
    qh = Quotaholder.get()
148
    with AstakosClientExceptionHandler():
149
        response = qh.resolve_commissions(accept, reject)
150

    
151
    accepted = response.get("accepted", [])
152
    rejected = response.get("rejected", [])
153

    
154
    if accepted:
155
        QuotaHolderSerial.objects.filter(serial__in=accepted).update(
156
            accept=True, pending=False, resolved=True)
157
    if rejected:
158
        QuotaHolderSerial.objects.filter(serial__in=rejected).update(
159
            accept=False, pending=False, resolved=True)
160

    
161
    if strict:
162
        failed = response["failed"]
163
        if failed:
164
            log.error("Unexpected error while resolving commissions: %s",
165
                      failed)
166

    
167
    return response
168

    
169

    
170
def resolve_pending_commissions():
171
    """Resolve quotaholder pending commissions.
172

173
    Get pending commissions from the quotaholder and resolve them
174
    to accepted and rejected, according to the state of the
175
    QuotaHolderSerial DB table. A pending commission in the quotaholder
176
    can exist in the QuotaHolderSerial table and be either accepted or
177
    rejected, or cannot exist in this table, so it is rejected.
178

179
    """
180

    
181
    qh_pending = get_quotaholder_pending()
182
    if not qh_pending:
183
        return ([], [])
184

    
185
    qh_pending.sort()
186
    min_ = qh_pending[0]
187

    
188
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
189
    accepted = serials.filter(accept=True).values_list('serial', flat=True)
190
    accepted = filter(lambda x: x in qh_pending, accepted)
191

    
192
    rejected = list(set(qh_pending) - set(accepted))
193

    
194
    return (accepted, rejected)
195

    
196

    
197
def get_quotaholder_pending():
198
    qh = Quotaholder.get()
199
    pending_serials = qh.get_pending_commissions()
200
    return pending_serials
201

    
202

    
203
def render_overlimit_exception(e):
204
    resource_name = {"vm": "Virtual Machine",
205
                     "cpu": "CPU",
206
                     "ram": "RAM",
207
                     "network.private": "Private Network",
208
                     "floating_ip": "Floating IP address"}
209
    details = json.loads(e.details)
210
    data = details['overLimit']['data']
211
    usage = data["usage"]
212
    limit = data["limit"]
213
    available = limit - usage
214
    provision = data['provision']
215
    requested = provision['quantity']
216
    resource = provision['resource']
217
    res = resource.replace("cyclades.", "", 1)
218
    try:
219
        resource = resource_name[res]
220
    except KeyError:
221
        resource = res
222

    
223
    msg = "Resource Limit Exceeded for your account."
224
    details = "Limit for resource '%s' exceeded for your account."\
225
              " Available: %s, Requested: %s"\
226
              % (resource, available, requested)
227
    return msg, details
228

    
229

    
230
@transaction.commit_on_success
231
def issue_and_accept_commission(resource, action="BUILD", action_fields=None):
232
    """Issue and accept a commission to Quotaholder.
233

234
    This function implements the Commission workflow, and must be called
235
    exactly after and in the same transaction that created/updated the
236
    resource. The workflow that implements is the following:
237
    0) Resolve previous unresolved commission if exists
238
    1) Issue commission, get a serial and correlate it with the resource
239
    2) Store the serial in DB as a serial to accept
240
    3) COMMIT!
241
    4) Accept commission to QH
242

243
    """
244
    commission_reason = ("client: api, resource: %s, action: %s"
245
                         % (resource, action))
246
    serial = handle_resource_commission(resource=resource, action=action,
247
                                        action_fields=action_fields,
248
                                        commission_name=commission_reason)
249

    
250
    # Mark the serial as one to accept and associate it with the resource
251
    serial.pending = False
252
    serial.accept = True
253
    serial.save()
254
    transaction.commit()
255

    
256
    try:
257
        # Accept the commission to quotaholder
258
        accept_serial(serial)
259
    except:
260
        # Do not crash if we can not accept commission to Quotaholder. Quotas
261
        # have already been reserved and the resource already exists in DB.
262
        # Just log the error
263
        log.exception("Failed to accept commission: %s", serial)
264

    
265
    return serial
266

    
267

    
268
def get_commission_info(resource, action, action_fields=None):
269
    if isinstance(resource, VirtualMachine):
270
        flavor = resource.flavor
271
        resources = {"cyclades.vm": 1,
272
                     "cyclades.total_cpu": flavor.cpu,
273
                     "cyclades.disk": 1073741824 * flavor.disk,
274
                     "cyclades.total_ram": 1048576 * flavor.ram}
275
        online_resources = {"cyclades.cpu": flavor.cpu,
276
                            "cyclades.ram": 1048576 * flavor.ram}
277
        if action == "BUILD":
278
            resources.update(online_resources)
279
            return resources
280
        if action == "START":
281
            if resource.operstate == "STOPPED":
282
                return online_resources
283
            else:
284
                return None
285
        elif action == "STOP":
286
            if resource.operstate in ["STARTED", "BUILD", "ERROR"]:
287
                return reverse_quantities(online_resources)
288
            else:
289
                return None
290
        elif action == "REBOOT":
291
            if resource.operstate == "STOPPED":
292
                return online_resources
293
            else:
294
                return None
295
        elif action == "DESTROY":
296
            if resource.operstate in ["STARTED", "BUILD", "ERROR"]:
297
                resources.update(online_resources)
298
            return reverse_quantities(resources)
299
        elif action == "RESIZE" and action_fields:
300
            beparams = action_fields.get("beparams")
301
            cpu = beparams.get("vcpus", flavor.cpu)
302
            ram = beparams.get("maxmem", flavor.ram)
303
            return {"cyclades.total_cpu": cpu - flavor.cpu,
304
                    "cyclades.total_ram": 1048576 * (ram - flavor.ram)}
305
        else:
306
            #["CONNECT", "DISCONNECT", "SET_FIREWALL_PROFILE"]:
307
            return None
308
    elif isinstance(resource, Network):
309
        resources = {"cyclades.network.private": 1}
310
        if action == "BUILD":
311
            return resources
312
        elif action == "DESTROY":
313
            return reverse_quantities(resources)
314
    elif isinstance(resource, IPAddress):
315
        if resource.floating_ip:
316
            resources = {"cyclades.floating_ip": 1}
317
            if action == "BUILD":
318
                return resources
319
            elif action == "DESTROY":
320
                return reverse_quantities(resources)
321
        else:
322
            return None
323

    
324

    
325
def reverse_quantities(resources):
326
    return dict((r, -s) for r, s in resources.items())
327

    
328

    
329
def handle_resource_commission(resource, action, commission_name,
330
                               force=False, auto_accept=False,
331
                               action_fields=None):
332
    """Handle a issuing of a commission for a resource.
333

334
    Create a new commission for a resource based on the action that
335
    is performed. If the resource has a previous pending commission,
336
    resolved it before issuing the new one.
337

338
    """
339
    # Try to resolve previous serial:
340
    # If action is DESTROY, we must always reject the previous commission,
341
    # since multiple DESTROY actions are allowed in the same resource (e.g. VM)
342
    # The one who succeeds will be finally accepted, and all other will be
343
    # rejected
344
    force = force or (action == "DESTROY")
345
    resolve_commission(resource.serial, force=force)
346

    
347
    serial = issue_commission(resource, action, name=commission_name,
348
                              force=force, auto_accept=auto_accept,
349
                              action_fields=action_fields)
350
    resource.serial = serial
351
    resource.save()
352
    return serial
353

    
354

    
355
class ResolveError(Exception):
356
    pass
357

    
358

    
359
def resolve_commission(serial, force=False):
360
    if serial is None or serial.resolved:
361
        return
362
    if serial.pending and not force:
363
        m = "Could not resolve commission: serial %s is undecided" % serial
364
        raise ResolveError(m)
365
    log.warning("Resolving pending commission: %s", serial)
366
    if not serial.pending and serial.accept:
367
        accept_serial(serial)
368
    else:
369
        reject_serial(serial)