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

b/snf-cyclades-app/synnefo/quotas/__init__.py
27 27
# those of the authors and should not be interpreted as representing official
28 28
# policies, either expressed or implied, of GRNET S.A.
29 29

  
30
from functools import wraps
31 30
from django.utils import simplejson as json
31
from django.db import transaction
32 32

  
33 33
from snf_django.lib.api import faults
34
from synnefo.db.models import QuotaHolderSerial
34
from synnefo.db.models import QuotaHolderSerial, VirtualMachine, Network
35 35

  
36 36
from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
37 37
                              ASTAKOS_URL)
......
64 64
        return cls._object
65 65

  
66 66

  
67
def uses_commission(func):
68
    """Decorator for wrapping functions that needs commission.
69

  
70
    All decorated functions must take as first argument the `serials` list in
71
    order to extend them with the needed serial numbers, as return by the
72
    Quotaholder
73

  
74
    On successful competition of the decorated function, all serials are
75
    accepted to the quotaholder, otherwise they are rejected.
76

  
77
    """
78

  
79
    @wraps(func)
80
    def wrapper(*args, **kwargs):
81
        try:
82
            serials = []
83
            ret = func(serials, *args, **kwargs)
84
        except:
85
            log.exception("Unexpected error")
86
            try:
87
                if serials:
88
                    reject_commission(serials=serials)
89
            except:
90
                log.exception("Exception while rejecting serials %s", serials)
91
                raise
92
            raise
93

  
94
        # func has completed successfully. accept serials
95
        try:
96
            if serials:
97
                accept_commission(serials)
98
            return ret
99
        except:
100
            log.exception("Exception while accepting serials %s", serials)
101
            raise
102
    return wrapper
103

  
104

  
105
## FIXME: Wrap the following two functions inside transaction ?
106
def accept_commission(serials, update_db=True):
107
    """Accept a list of pending commissions.
108

  
109
    @param serials: List of QuotaHolderSerial objects
110

  
111
    """
112
    if update_db:
113
        for s in serials:
114
            if s.pending:
115
                s.accepted = True
116
                s.save()
117

  
118
    accept_serials = [s.serial for s in serials]
119
    qh_resolve_commissions(accept=accept_serials)
120

  
121

  
122
def reject_commission(serials, update_db=True):
123
    """Reject a list of pending commissions.
124

  
125
    @param serials: List of QuotaHolderSerial objects
126

  
127
    """
128
    if update_db:
129
        for s in serials:
130
            if s.pending:
131
                s.rejected = True
132
                s.save()
133

  
134
    reject_serials = [s.serial for s in serials]
135
    qh_resolve_commissions(reject=reject_serials)
136

  
137

  
138 67
def issue_commission(user, source, provisions,
139 68
                     force=False, auto_accept=False):
140 69
    """Issue a new commission to the quotaholder.
......
162 91
        raise Exception("No serial")
163 92

  
164 93

  
165
# Wrapper functions for issuing commissions for each resource type.  Each
166
# functions creates the `commission_info` dictionary as expected by the
167
# `issue_commision` function. Commissions for deleting a resource, are the same
168
# as for creating the same resource, but with negative resource sizes.
169

  
170

  
171
def issue_vm_commission(user, flavor, delete=False):
172
    resources = get_server_resources(flavor)
173
    if delete:
174
        resources = reverse_quantities(resources)
175
    return issue_commission(user, DEFAULT_SOURCE, resources)
176

  
177

  
178
def get_server_resources(flavor):
179
    return {'cyclades.vm': 1,
180
            'cyclades.cpu': flavor.cpu,
181
            'cyclades.disk': 1073741824 * flavor.disk,  # flavor.disk is in GB
182
            # 'public_ip': 1,
183
            #'disk_template': flavor.disk_template,
184
            'cyclades.ram': 1048576 * flavor.ram}  # flavor.ram is in MB
185

  
186

  
187
def issue_network_commission(user, delete=False):
188
    resources = get_network_resources()
189
    if delete:
190
        resources = reverse_quantities(resources)
191
    return issue_commission(user, DEFAULT_SOURCE, resources)
192

  
193

  
194
def get_network_resources():
195
    return {"cyclades.network.private": 1}
196

  
197

  
198
def reverse_quantities(resources):
199
    return dict((r, -s) for r, s in resources.items())
200

  
201

  
202
##
203
## Reconcile pending commissions
204
##
205

  
206

  
207 94
def accept_commissions(accepted):
208 95
    qh_resolve_commissions(accept=accepted)
209 96

  
......
212 99
    qh_resolve_commissions(reject=rejected)
213 100

  
214 101

  
215
def fix_pending_commissions():
216
    (accepted, rejected) = resolve_pending_commissions()
217
    qh_resolve_commissions(accepted, rejected)
218

  
219

  
220 102
def qh_resolve_commissions(accept=None, reject=None):
221 103
    if accept is None:
222 104
        accept = []
......
227 109
    qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
228 110

  
229 111

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

  
116

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

  
......
246 133
    min_ = qh_pending[0]
247 134

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

  
252 139
    rejected = list(set(qh_pending) - set(accepted))
......
284 171
              " Available: %s, Requested: %s"\
285 172
              % (resource, available, requested)
286 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())

Also available in: Unified diff