Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (12.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 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
131
    response = resolve_commissions(accept=[serial.serial], strict=strict)
132
    serial.pending = False
133
    serial.accept = True
134
    serial.resolved = True
135
    serial.save()
136
    return response
137

    
138

    
139
def reject_serial(serial, strict=True):
140
    assert serial.pending or not serial.accept
141
    response = resolve_commissions(reject=[serial.serial], strict=strict)
142
    serial.pending = False
143
    serial.accept = False
144
    serial.resolved = True
145
    serial.save()
146
    return response
147

    
148

    
149
def accept_commissions(accepted, strict=True):
150
    return resolve_commissions(accept=accepted, strict=strict)
151

    
152

    
153
def reject_commissions(rejected, strict=True):
154
    return resolve_commissions(reject=rejected, strict=strict)
155

    
156

    
157
def resolve_commissions(accept=None, reject=None, strict=True):
158
    if accept is None:
159
        accept = []
160
    if reject is None:
161
        reject = []
162

    
163
    qh = Quotaholder.get()
164
    with AstakosClientExceptionHandler():
165
        response = qh.resolve_commissions(accept, reject)
166

    
167
    if strict:
168
        failed = response["failed"]
169
        if failed:
170
            log.error("Unexpected error while resolving commissions: %s",
171
                      failed)
172

    
173
    return response
174

    
175

    
176
def fix_pending_commissions():
177
    (accepted, rejected) = resolve_pending_commissions()
178
    resolve_commissions(accept=accepted, reject=rejected)
179

    
180

    
181
def resolve_pending_commissions():
182
    """Resolve quotaholder pending commissions.
183

184
    Get pending commissions from the quotaholder and resolve them
185
    to accepted and rejected, according to the state of the
186
    QuotaHolderSerial DB table. A pending commission in the quotaholder
187
    can exist in the QuotaHolderSerial table and be either accepted or
188
    rejected, or cannot exist in this table, so it is rejected.
189

190
    """
191

    
192
    qh_pending = get_quotaholder_pending()
193
    if not qh_pending:
194
        return ([], [])
195

    
196
    qh_pending.sort()
197
    min_ = qh_pending[0]
198

    
199
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
200
    accepted = serials.filter(accept=True).values_list('serial', flat=True)
201
    accepted = filter(lambda x: x in qh_pending, accepted)
202

    
203
    rejected = list(set(qh_pending) - set(accepted))
204

    
205
    return (accepted, rejected)
206

    
207

    
208
def get_quotaholder_pending():
209
    qh = Quotaholder.get()
210
    pending_serials = qh.get_pending_commissions()
211
    return pending_serials
212

    
213

    
214
def render_overlimit_exception(e):
215
    resource_name = {"vm": "Virtual Machine",
216
                     "cpu": "CPU",
217
                     "ram": "RAM",
218
                     "network.private": "Private Network",
219
                     "floating_ip": "Floating IP address"}
220
    details = json.loads(e.details)
221
    data = details['overLimit']['data']
222
    usage = data["usage"]
223
    limit = data["limit"]
224
    available = limit - usage
225
    provision = data['provision']
226
    requested = provision['quantity']
227
    resource = provision['resource']
228
    res = resource.replace("cyclades.", "", 1)
229
    try:
230
        resource = resource_name[res]
231
    except KeyError:
232
        resource = res
233

    
234
    msg = "Resource Limit Exceeded for your account."
235
    details = "Limit for resource '%s' exceeded for your account."\
236
              " Available: %s, Requested: %s"\
237
              % (resource, available, requested)
238
    return msg, details
239

    
240

    
241
@transaction.commit_on_success
242
def issue_and_accept_commission(resource, action="BUILD", action_fields=None):
243
    """Issue and accept a commission to Quotaholder.
244

245
    This function implements the Commission workflow, and must be called
246
    exactly after and in the same transaction that created/updated the
247
    resource. The workflow that implements is the following:
248
    0) Resolve previous unresolved commission if exists
249
    1) Issue commission, get a serial and correlate it with the resource
250
    2) Store the serial in DB as a serial to accept
251
    3) COMMIT!
252
    4) Accept commission to QH
253

254
    """
255
    commission_reason = ("client: api, resource: %s, action: %s"
256
                         % (resource, action))
257
    serial = handle_resource_commission(resource=resource, action=action,
258
                                        action_fields=action_fields,
259
                                        commission_name=commission_reason)
260

    
261
    # Mark the serial as one to accept and associate it with the resource
262
    serial.pending = False
263
    serial.accept = True
264
    serial.save()
265
    transaction.commit()
266

    
267
    try:
268
        # Accept the commission to quotaholder
269
        accept_serial(serial)
270
    except:
271
        # Do not crash if we can not accept commission to Quotaholder. Quotas
272
        # have already been reserved and the resource already exists in DB.
273
        # Just log the error
274
        log.exception("Failed to accept commission: %s", serial)
275

    
276
    return serial
277

    
278

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

    
335

    
336
def reverse_quantities(resources):
337
    return dict((r, -s) for r, s in resources.items())
338

    
339

    
340
def handle_resource_commission(resource, action, commission_name,
341
                               force=False, auto_accept=False,
342
                               action_fields=None):
343
    """Handle a issuing of a commission for a resource.
344

345
    Create a new commission for a resource based on the action that
346
    is performed. If the resource has a previous pending commission,
347
    resolved it before issuing the new one.
348

349
    """
350
    # Try to resolve previous serial
351
    resolve_commission(resource.serial, force=force)
352

    
353
    serial = issue_commission(resource, action, name=commission_name,
354
                              force=force, auto_accept=auto_accept,
355
                              action_fields=action_fields)
356
    resource.serial = serial
357
    resource.save()
358
    return serial
359

    
360

    
361
class ResolveError(Exception):
362
    pass
363

    
364

    
365
def resolve_commission(serial, force=False):
366
    if serial is None or serial.resolved:
367
        return
368
    if serial.pending and not force:
369
        m = "Could not resolve commission: serial %s is undecided" % serial
370
        raise ResolveError(m)
371
    log.warning("Resolving pending commission: %s", serial)
372
    if not serial.pending and serial.accept:
373
        accept_serial(serial)
374
    else:
375
        reject_serial(serial)