Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.3 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
    1) Issue commission and get a serial
184
    2) Store the serial in DB and mark is as one to accept
185
    3) Correlate the serial with the resource
186
    4) COMMIT!
187
    5) Accept commission to QH (reject if failed until 5)
188
    6) Mark serial as resolved
189
    7) COMMIT!
190

191
    """
192
    try:
193
        # Convert resources in the format expected by Quotaholder
194
        qh_resources = prepare_qh_resources(resource)
195
        if delete:
196
            qh_resources = reverse_quantities(qh_resources)
197

    
198
        # Issue commission and get the assigned serial
199
        serial = issue_commission(resource.userid, DEFAULT_SOURCE,
200
                                  qh_resources)
201
    except:
202
        transaction.rollback()
203
        raise
204

    
205
    try:
206
        # Mark the serial as one to accept. This step is necessary for
207
        # reconciliation
208
        serial.pending = False
209
        serial.accept = True
210
        serial.save()
211

    
212
        # Associate serial with the resource
213
        resource.serial = serial
214
        resource.save()
215

    
216
        # Commit transaction in the DB! If this commit succeeds, then the
217
        # serial is created in the DB with all the necessary information to
218
        # reconcile commission
219
        transaction.commit()
220
    except:
221
        transaction.rollback()
222
        serial.pending = False
223
        serial.accept = False
224
        serial.save()
225
        transaction.commit()
226
        raise
227

    
228
    if serial.accept:
229
        # Accept commission to Quotaholder
230
        accept_commissions(accepted=[serial.serial])
231
    else:
232
        reject_commissions(rejected=[serial.serial])
233

    
234
    # Mark the serial as resolved, indicating that no further actions are
235
    # needed for this serial
236
    serial.resolved = True
237
    serial.save()
238
    transaction.commit()
239

    
240
    return serial
241

    
242

    
243
def prepare_qh_resources(resource):
244
    if isinstance(resource, VirtualMachine):
245
        flavor = resource.flavor
246
        return {'cyclades.vm': 1,
247
                'cyclades.cpu': flavor.cpu,
248
                'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk in GB
249
                # 'public_ip': 1,
250
                #'disk_template': flavor.disk_template,
251
                'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
252
    elif isinstance(resource, Network):
253
        return {"cyclades.network.private": 1}
254
    else:
255
        raise ValueError("Unknown Resource '%s'" % resource)
256

    
257

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