Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.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

    
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):
95
    qh_resolve_commissions(accept=accepted)
96

    
97

    
98
def reject_commissions(rejected):
99
    qh_resolve_commissions(reject=rejected)
100

    
101

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

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

    
111

    
112
def fix_pending_commissions():
113
    (accepted, rejected) = resolve_pending_commissions()
114
    qh_resolve_commissions(accepted, rejected)
115

    
116

    
117
def resolve_pending_commissions():
118
    """Resolve quotaholder pending commissions.
119

120
    Get pending commissions from the quotaholder and resolve them
121
    to accepted and rejected, according to the state of the
122
    QuotaHolderSerial DB table. A pending commission in the quotaholder
123
    can exist in the QuotaHolderSerial table and be either accepted or
124
    rejected, or can not exist in this table, so it is rejected.
125

126
    """
127

    
128
    qh_pending = get_quotaholder_pending()
129
    if not qh_pending:
130
        return ([], [])
131

    
132
    qh_pending.sort()
133
    min_ = qh_pending[0]
134

    
135
    serials = QuotaHolderSerial.objects.filter(serial__gte=min_, pending=False)
136
    accepted = serials.filter(accept=True).values_list('serial', flat=True)
137
    accepted = filter(lambda x: x in qh_pending, accepted)
138

    
139
    rejected = list(set(qh_pending) - set(accepted))
140

    
141
    return (accepted, rejected)
142

    
143

    
144
def get_quotaholder_pending():
145
    qh = Quotaholder.get()
146
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
147
    return pending_serials
148

    
149

    
150
def render_overlimit_exception(e):
151
    resource_name = {"vm": "Virtual Machine",
152
                     "cpu": "CPU",
153
                     "ram": "RAM",
154
                     "network.private": "Private Network"}
155
    details = json.loads(e.details)
156
    data = details['overLimit']['data']
157
    usage = data["usage"]
158
    limit = data["limit"]
159
    available = limit - usage
160
    provision = data['provision']
161
    requested = provision['quantity']
162
    resource = provision['resource']
163
    res = resource.replace("cyclades.", "", 1)
164
    try:
165
        resource = resource_name[res]
166
    except KeyError:
167
        resource = res
168

    
169
    msg = "Resource Limit Exceeded for your account."
170
    details = "Limit for resource '%s' exceeded for your account."\
171
              " Available: %s, Requested: %s"\
172
              % (resource, available, requested)
173
    return msg, details
174

    
175

    
176
@transaction.commit_manually
177
def issue_and_accept_commission(resource, delete=False):
178
    """Issue and accept a commission to Quotaholder.
179

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

192
    """
193
    previous_serial = resource.serial
194
    if previous_serial is not None and not previous_serial.resolved:
195
        if previous_serial.pending:
196
            msg = "Issuing commission for resource '%s' while previous serial"\
197
                  " '%s' is still pending." % (resource, previous_serial)
198
            raise Exception(msg)
199
        elif previous_serial.accept:
200
            accept_commissions(accepted=[previous_serial.serial])
201
        else:
202
            reject_commissions(rejected=[previous_serial.serial])
203
        previous_serial.resolved = True
204

    
205
    try:
206
        # Convert resources in the format expected by Quotaholder
207
        qh_resources = prepare_qh_resources(resource)
208
        if delete:
209
            qh_resources = reverse_quantities(qh_resources)
210

    
211
        # Issue commission and get the assigned serial
212
        serial = issue_commission(resource.userid, DEFAULT_SOURCE,
213
                                  qh_resources)
214
    except:
215
        transaction.rollback()
216
        raise
217

    
218
    try:
219
        # Mark the serial as one to accept. This step is necessary for
220
        # reconciliation
221
        serial.pending = False
222
        serial.accept = True
223
        serial.save()
224

    
225
        # Associate serial with the resource
226
        resource.serial = serial
227
        resource.save()
228

    
229
        # Commit transaction in the DB! If this commit succeeds, then the
230
        # serial is created in the DB with all the necessary information to
231
        # reconcile commission
232
        transaction.commit()
233
    except:
234
        transaction.rollback()
235
        serial.pending = False
236
        serial.accept = False
237
        serial.save()
238
        transaction.commit()
239
        raise
240

    
241
    if serial.accept:
242
        # Accept commission to Quotaholder
243
        accept_commissions(accepted=[serial.serial])
244
    else:
245
        reject_commissions(rejected=[serial.serial])
246

    
247
    # Mark the serial as resolved, indicating that no further actions are
248
    # needed for this serial
249
    serial.resolved = True
250
    serial.save()
251
    transaction.commit()
252

    
253
    return serial
254

    
255

    
256
def prepare_qh_resources(resource):
257
    if isinstance(resource, VirtualMachine):
258
        flavor = resource.flavor
259
        return {'cyclades.vm': 1,
260
                'cyclades.cpu': flavor.cpu,
261
                'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk in GB
262
                # 'public_ip': 1,
263
                #'disk_template': flavor.disk_template,
264
                'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
265
    elif isinstance(resource, Network):
266
        return {"cyclades.network.private": 1}
267
    else:
268
        raise ValueError("Unknown Resource '%s'" % resource)
269

    
270

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