Revision 629acc65 snf-cyclades-app/synnefo/quotas/__init__.py

b/snf-cyclades-app/synnefo/quotas/__init__.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
1
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2 2
#
3 3
# Redistribution and use in source and binary forms, with or without
4 4
# modification, are permitted provided that the following conditions
......
31 31
from contextlib import contextmanager
32 32

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

  
37
if CYCLADES_USE_QUOTAHOLDER:
38
    from synnefo.settings import (CYCLADES_QUOTAHOLDER_URL,
39
                                  CYCLADES_QUOTAHOLDER_TOKEN,
40
                                  CYCLADES_QUOTAHOLDER_POOLSIZE)
41
    from synnefo.lib.quotaholder import QuotaholderClient
42
else:
43
    from synnefo.settings import (VMS_USER_QUOTA, MAX_VMS_PER_USER,
44
                                  NETWORKS_USER_QUOTA, MAX_NETWORKS_PER_USER)
45

  
46
from synnefo.lib.quotaholder.api import (NoCapacityError, NoQuantityError,
47
                                         NoEntityError, CallError)
37
from synnefo.settings import (CYCLADES_ASTAKOS_SERVICE_TOKEN as ASTAKOS_TOKEN,
38
                              ASTAKOS_URL)
39
from astakosclient import AstakosClient
40
from astakosclient.errors import AstakosClientException, QuotaLimit
48 41

  
49 42
import logging
50 43
log = logging.getLogger(__name__)
51 44

  
45
DEFAULT_SOURCE = 'system'
52 46

  
53
class DummySerial(QuotaHolderSerial):
54
    accepted = True
55
    rejected = True
56
    pending = True
57
    id = None
58

  
59
    def save(*args, **kwargs):
60
        pass
61

  
62

  
63
class DummyQuotaholderClient(object):
64
    def issue_commission(self, **commission_info):
65
        provisions = commission_info["provisions"]
66
        userid = commission_info["target"]
67
        for provision in provisions:
68
            entity, resource, size = provision
69
            if resource == "cyclades.vm" and size > 0:
70
                user_vms = VirtualMachine.objects.filter(userid=userid,
71
                                                         deleted=False).count()
72
                user_vm_limit = VMS_USER_QUOTA.get(userid, MAX_VMS_PER_USER)
73
                log.debug("Users VMs %s User Limits %s", user_vms,
74
                          user_vm_limit)
75
                if user_vms + size > user_vm_limit:
76
                    raise NoQuantityError(source='cyclades',
77
                                          target=userid,
78
                                          resource=resource,
79
                                          requested=size,
80
                                          current=user_vms,
81
                                          limit=user_vm_limit)
82
            if resource == "cyclades.network.private" and size > 0:
83
                user_networks = Network.objects.filter(userid=userid,
84
                                                       deleted=False).count()
85
                user_network_limit =\
86
                    NETWORKS_USER_QUOTA.get(userid, MAX_NETWORKS_PER_USER)
87
                if user_networks + size > user_network_limit:
88
                    raise NoQuantityError(source='cyclades',
89
                                          target=userid,
90
                                          resource=resource,
91
                                          requested=size,
92
                                          current=user_networks,
93
                                          limit=user_network_limit)
94

  
95
        return None
96

  
97
    def accept_commission(self, *args, **kwargs):
98
        pass
99

  
100
    def reject_commission(self, *args, **kwargs):
101
        pass
102

  
103
    def get_pending_commissions(self, *args, **kwargs):
104
        return []
105

  
106

  
107
@contextmanager
108
def get_quota_holder():
109
    """Context manager for using a QuotaHolder."""
110
    if CYCLADES_USE_QUOTAHOLDER:
111
        quotaholder = QuotaholderClient(CYCLADES_QUOTAHOLDER_URL,
112
                                        token=CYCLADES_QUOTAHOLDER_TOKEN,
113
                                        poolsize=CYCLADES_QUOTAHOLDER_POOLSIZE)
114
    else:
115
        quotaholder = DummyQuotaholderClient()
116 47

  
117
    try:
118
        yield quotaholder
119
    finally:
120
        pass
48
class Quotaholder(object):
49
    _object = None
50

  
51
    @classmethod
52
    def get(cls):
53
        if cls._object is None:
54
            cls._object = AstakosClient(
55
                ASTAKOS_URL,
56
                use_pool=True,
57
                logger=log)
58
        return cls._object
121 59

  
122 60

  
123 61
def uses_commission(func):
......
171 109
                s.accepted = True
172 110
                s.save()
173 111

  
174
    with get_quota_holder() as qh:
175
        qh.accept_commission(context={},
176
                             clientkey='cyclades',
177
                             serials=[s.serial for s in serials])
112
    accept_serials = [s.serial for s in serials]
113
    qh_resolve_commissions(accept=accept_serials)
178 114

  
179 115

  
180 116
def reject_commission(serials, update_db=True):
......
189 125
                s.rejected = True
190 126
                s.save()
191 127

  
192
    with get_quota_holder() as qh:
193
        qh.reject_commission(context={},
194
                             clientkey='cyclades',
195
                             serials=[s.serial for s in serials])
128
    reject_serials = [s.serial for s in serials]
129
    qh_resolve_commissions(reject=reject_serials)
196 130

  
197 131

  
198
def issue_commission(**commission_info):
132
def issue_commission(user, source, provisions,
133
                     force=False, auto_accept=False):
199 134
    """Issue a new commission to the quotaholder.
200 135

  
201 136
    Issue a new commission to the quotaholder, and create the
......
203 138

  
204 139
    """
205 140

  
206
    with get_quota_holder() as qh:
207
        try:
208
            serial = qh.issue_commission(**commission_info)
209
        except (NoCapacityError, NoQuantityError) as e:
210
            msg, details = render_quotaholder_exception(e)
211
            raise faults.OverLimit(msg, details=details)
212
        except CallError as e:
213
            log.exception("Unexpected error")
214
            raise
141
    qh = Quotaholder.get()
142
    try:
143
        serial = qh.issue_one_commission(ASTAKOS_TOKEN,
144
                                         user, source, provisions,
145
                                         force, auto_accept)
146
    except QuotaLimit as e:
147
        msg, details = render_overlimit_exception(e)
148
        raise faults.OverLimit(msg, details=details)
149
    except AstakosClientException as e:
150
        log.exception("Unexpected error")
151
        raise
215 152

  
216 153
    if serial:
217 154
        return QuotaHolderSerial.objects.create(serial=serial)
218
    elif not CYCLADES_USE_QUOTAHOLDER:
219
        return DummySerial()
220 155
    else:
221 156
        raise Exception("No serial")
222 157

  
......
228 163

  
229 164

  
230 165
def issue_vm_commission(user, flavor, delete=False):
231
    resources = get_server_resources(flavor)
232
    commission_info = create_commission(user, resources, delete)
233

  
234
    return issue_commission(**commission_info)
166
    resources = prepare(get_server_resources(flavor), delete)
167
    return issue_commission(user, DEFAULT_SOURCE, resources)
235 168

  
236 169

  
237 170
def get_server_resources(flavor):
......
244 177

  
245 178

  
246 179
def issue_network_commission(user, delete=False):
247
    resources = get_network_resources()
248
    commission_info = create_commission(user, resources, delete)
249

  
250
    return issue_commission(**commission_info)
180
    resources = prepare(get_network_resources(), delete)
181
    return issue_commission(user, DEFAULT_SOURCE, resources)
251 182

  
252 183

  
253 184
def get_network_resources():
254 185
    return {"network.private": 1}
255 186

  
256 187

  
257
def invert_resources(resources_dict):
258
    return dict((r, -s) for r, s in resources_dict.items())
259

  
260

  
261
def create_commission(user, resources, delete=False):
188
def prepare(resources_dict, delete):
262 189
    if delete:
263
        resources = invert_resources(resources)
264
    provisions = [('cyclades', 'cyclades.' + r, s)
265
                  for r, s in resources.items()]
266
    return {"context": {},
267
            "target": user,
268
            "key": "1",
269
            "clientkey": "cyclades",
270
            #"owner":      "",
271
            #"ownerkey":   "1",
272
            "name": "",
273
            "provisions": provisions}
190
        return dict((r, -s) for r, s in resources_dict.items())
191
    return resources_dict
192

  
274 193

  
275 194
##
276 195
## Reconcile pending commissions
......
278 197

  
279 198

  
280 199
def accept_commissions(accepted):
281
    with get_quota_holder() as qh:
282
        qh.accept_commission(context={},
283
                             clientkey='cyclades',
284
                             serials=accepted)
200
    qh_resolve_commissions(accept=accepted)
285 201

  
286 202

  
287 203
def reject_commissions(rejected):
288
    with get_quota_holder() as qh:
289
            qh.reject_commission(context={},
290
                                 clientkey='cyclades',
291
                                 serials=rejected)
204
    qh_resolve_commissions(reject=rejected)
292 205

  
293 206

  
294 207
def fix_pending_commissions():
295 208
    (accepted, rejected) = resolve_pending_commissions()
209
    qh_resolve_commissions(accepted, rejected)
210

  
211

  
212
def qh_resolve_commissions(accept=None, reject=None):
213
    if accept is None:
214
        accept = []
215
    if reject is None:
216
        reject = []
296 217

  
297
    with get_quota_holder() as qh:
298
        if accepted:
299
            qh.accept_commission(context={},
300
                                 clientkey='cyclades',
301
                                 serials=accepted)
302
        if rejected:
303
            qh.reject_commission(context={},
304
                                 clientkey='cyclades',
305
                                 serials=rejected)
218
    qh = Quotaholder.get()
219
    qh.resolve_commissions(ASTAKOS_TOKEN, accept, reject)
306 220

  
307 221

  
308 222
def resolve_pending_commissions():
......
333 247

  
334 248

  
335 249
def get_quotaholder_pending():
336
    with get_quota_holder() as qh:
337
        pending_serials = qh.get_pending_commissions(context={},
338
                                                     clientkey='cyclades')
250
    qh = Quotaholder.get()
251
    pending_serials = qh.get_pending_commissions(ASTAKOS_TOKEN)
339 252
    return pending_serials
340 253

  
341 254

  
342
def render_quotaholder_exception(e):
255
def render_overlimit_exception(e):
343 256
    resource_name = {"vm": "Virtual Machine",
344 257
                     "cpu": "CPU",
345 258
                     "ram": "RAM",
346 259
                     "network.private": "Private Network"}
347
    res = e.resource.replace("cyclades.", "", 1)
260
    details = e.details
261
    data = details['overLimit']['data']
262
    available = data['available']
263
    provision = data['provision']
264
    requested = provision['quantity']
265
    resource = provision['resource']
266
    res = resource.replace("cyclades.", "", 1)
348 267
    try:
349 268
        resource = resource_name[res]
350 269
    except KeyError:
351 270
        resource = res
352 271

  
353
    requested = e.requested
354
    current = e.current
355
    limit = e.limit
356 272
    msg = "Resource Limit Exceeded for your account."
357 273
    details = "Limit for resource '%s' exceeded for your account."\
358
              " Current value: %s, Limit: %s, Requested: %s"\
359
              % (resource, current, limit, requested)
274
              " Available: %s, Requested: %s"\
275
              % (resource, available, requested)
360 276
    return msg, details

Also available in: Unified diff