Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (9.2 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_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
37
                              ASTAKOS_URL)
38
from astakosclient import AstakosClient
39
from astakosclient.errors import AstakosClientException, QuotaLimit
40

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

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

    
53

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

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

    
66

    
67
def issue_commission(user, source, provisions,
68
                     force=False, auto_accept=False):
69
    """Issue a new commission to the quotaholder.
70

71
    Issue a new commission to the quotaholder, and create the
72
    corresponing QuotaHolderSerial object in DB.
73

74
    """
75

    
76
    qh = Quotaholder.get()
77
    try:
78
        serial = qh.issue_one_commission(ASTAKOS_TOKEN,
79
                                         user, source, provisions,
80
                                         force=force, auto_accept=auto_accept)
81
    except QuotaLimit as e:
82
        msg, details = render_overlimit_exception(e)
83
        raise faults.OverLimit(msg, details=details)
84
    except AstakosClientException as e:
85
        log.exception("Unexpected error")
86
        raise
87

    
88
    if serial:
89
        return QuotaHolderSerial.objects.create(serial=serial)
90
    else:
91
        raise Exception("No serial")
92

    
93

    
94
def accept_commissions(accepted, strict=True):
95
    return resolve_commissions(accept=accepted, strict=strict)
96

    
97

    
98
def reject_commissions(rejected, strict=True):
99
    return resolve_commissions(reject=rejected, strict=strict)
100

    
101

    
102
def resolve_commissions(accept=None, reject=None, strict=True):
103
    if accept is None:
104
        accept = []
105
    if reject is None:
106
        reject = []
107

    
108
    qh = Quotaholder.get()
109
    response = qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
110

    
111
    if strict:
112
        failed = response["failed"]
113
        if failed:
114
            log.error("Unexpected error while resolving commissions: %s",
115
                      failed)
116

    
117
    return response
118

    
119

    
120
def fix_pending_commissions():
121
    (accepted, rejected) = resolve_pending_commissions()
122
    resolve_commissions(accept=accepted, reject=rejected)
123

    
124

    
125
def resolve_pending_commissions():
126
    """Resolve quotaholder pending commissions.
127

128
    Get pending commissions from the quotaholder and resolve them
129
    to accepted and rejected, according to the state of the
130
    QuotaHolderSerial DB table. A pending commission in the quotaholder
131
    can exist in the QuotaHolderSerial table and be either accepted or
132
    rejected, or can not exist in this table, so it is rejected.
133

134
    """
135

    
136
    qh_pending = get_quotaholder_pending()
137
    if not qh_pending:
138
        return ([], [])
139

    
140
    qh_pending.sort()
141
    min_ = qh_pending[0]
142

    
143
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
144
    accepted = serials.filter(accept=True).values_list('serial', flat=True)
145
    accepted = filter(lambda x: x in qh_pending, accepted)
146

    
147
    rejected = list(set(qh_pending) - set(accepted))
148

    
149
    return (accepted, rejected)
150

    
151

    
152
def get_quotaholder_pending():
153
    qh = Quotaholder.get()
154
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
155
    return pending_serials
156

    
157

    
158
def render_overlimit_exception(e):
159
    resource_name = {"vm": "Virtual Machine",
160
                     "cpu": "CPU",
161
                     "ram": "RAM",
162
                     "network.private": "Private Network"}
163
    details = json.loads(e.details)
164
    data = details['overLimit']['data']
165
    usage = data["usage"]
166
    limit = data["limit"]
167
    available = limit - usage
168
    provision = data['provision']
169
    requested = provision['quantity']
170
    resource = provision['resource']
171
    res = resource.replace("cyclades.", "", 1)
172
    try:
173
        resource = resource_name[res]
174
    except KeyError:
175
        resource = res
176

    
177
    msg = "Resource Limit Exceeded for your account."
178
    details = "Limit for resource '%s' exceeded for your account."\
179
              " Available: %s, Requested: %s"\
180
              % (resource, available, requested)
181
    return msg, details
182

    
183

    
184
@transaction.commit_manually
185
def issue_and_accept_commission(resource, delete=False):
186
    """Issue and accept a commission to Quotaholder.
187

188
    This function implements the Commission workflow, and must be called
189
    exactly after and in the same transaction that created/updated the
190
    resource. The workflow that implements is the following:
191
    0) Resolve previous unresolved commission if exists
192
    1) Issue commission and get a serial
193
    2) Store the serial in DB and mark is as one to accept
194
    3) Correlate the serial with the resource
195
    4) COMMIT!
196
    5) Accept commission to QH (reject if failed until 5)
197
    6) Mark serial as resolved
198
    7) COMMIT!
199

200
    """
201
    previous_serial = resource.serial
202
    if previous_serial is not None and not previous_serial.resolved:
203
        if previous_serial.pending:
204
            msg = "Issuing commission for resource '%s' while previous serial"\
205
                  " '%s' is still pending." % (resource, previous_serial)
206
            raise Exception(msg)
207
        elif previous_serial.accept:
208
            accept_commissions(accepted=[previous_serial.serial], strict=False)
209
        else:
210
            reject_commissions(rejected=[previous_serial.serial], strict=False)
211
        previous_serial.resolved = True
212

    
213
    try:
214
        # Convert resources in the format expected by Quotaholder
215
        qh_resources = prepare_qh_resources(resource)
216
        if delete:
217
            qh_resources = reverse_quantities(qh_resources)
218

    
219
        # Issue commission and get the assigned serial
220
        serial = issue_commission(resource.userid, DEFAULT_SOURCE,
221
                                  qh_resources)
222
    except:
223
        transaction.rollback()
224
        raise
225

    
226
    try:
227
        # Mark the serial as one to accept. This step is necessary for
228
        # reconciliation
229
        serial.pending = False
230
        serial.accept = True
231
        serial.save()
232

    
233
        # Associate serial with the resource
234
        resource.serial = serial
235
        resource.save()
236

    
237
        # Commit transaction in the DB! If this commit succeeds, then the
238
        # serial is created in the DB with all the necessary information to
239
        # reconcile commission
240
        transaction.commit()
241
    except:
242
        transaction.rollback()
243
        serial.pending = False
244
        serial.accept = False
245
        serial.save()
246
        transaction.commit()
247
        raise
248

    
249
    if serial.accept:
250
        # Accept commission to Quotaholder
251
        accept_commissions(accepted=[serial.serial])
252
    else:
253
        reject_commissions(rejected=[serial.serial])
254

    
255
    # Mark the serial as resolved, indicating that no further actions are
256
    # needed for this serial
257
    serial.resolved = True
258
    serial.save()
259
    transaction.commit()
260

    
261
    return serial
262

    
263

    
264
def prepare_qh_resources(resource):
265
    if isinstance(resource, VirtualMachine):
266
        flavor = resource.flavor
267
        return {'cyclades.vm': 1,
268
                'cyclades.cpu': flavor.cpu,
269
                'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk in GB
270
                # 'public_ip': 1,
271
                #'disk_template': flavor.disk_template,
272
                'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
273
    elif isinstance(resource, Network):
274
        return {"cyclades.network.private": 1}
275
    else:
276
        raise ValueError("Unknown Resource '%s'" % resource)
277

    
278

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