Revision 629acc65

b/snf-cyclades-app/synnefo/api/management/commands/cyclades-astakos-migrate-013.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
4 4
# without modification, are permitted provided that the following
......
41 41
from django.db import transaction
42 42
from django.conf import settings
43 43

  
44
from synnefo.quotas import get_quota_holder
45 44
from synnefo.api.util import get_existing_users
46 45
from synnefo.lib.utils import case_unique
47 46
from synnefo.db.models import Network, VirtualMachine
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
b/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-reset-usage.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
4 4
# without modification, are permitted provided that the following
......
34 34
from django.core.management.base import BaseCommand
35 35
from optparse import make_option
36 36

  
37
from synnefo.quotas import get_quota_holder
37
from synnefo.quotas import Quotaholder
38 38
from synnefo.quotas.util import get_db_holdings
39 39

  
40 40

  
......
58 58
        db_holdings = get_db_holdings(users)
59 59

  
60 60
        # Create commissions
61
        with get_quota_holder() as qh:
62
            for user, resources in db_holdings.items():
63
                if not user:
64
                    continue
65
                reset_holding = []
66
                for res, val in resources.items():
67
                    reset_holding.append((user, "cyclades." + res, "1", val, 0,
68
                                          0, 0))
69
                if not options['dry_run']:
70
                    try:
71
                        qh.reset_holding(context={},
72
                                         reset_holding=reset_holding)
73
                    except Exception as e:
74
                        self.stderr.write("Can not set up holding:%s" % e)
75
                else:
76
                    self.stdout.write("Reseting holding: %s\n" % reset_holding)
61
        qh = Quotaholder.get()
62
        for user, resources in db_holdings.items():
63
            if not user:
64
                continue
65
            reset_holding = []
66
            for res, val in resources.items():
67
                reset_holding.append((user, "cyclades." + res, "1", val, 0,
68
                                      0, 0))
69
            if not options['dry_run']:
70
                try:
71
                    qh.reset_holding(context={},
72
                                     reset_holding=reset_holding)
73
                except Exception as e:
74
                    self.stderr.write("Can not set up holding:%s" % e)
75
            else:
76
                self.stdout.write("Reseting holding: %s\n" % reset_holding)
b/snf-cyclades-app/synnefo/quotas/management/commands/cyclades-usage-verify.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
4 4
# without modification, are permitted provided that the following
......
34 34
from django.core.management.base import BaseCommand
35 35
from optparse import make_option
36 36

  
37
from synnefo.quotas.util import get_db_holdings, get_quotaholder_holdings
37
from synnefo.quotas import DEFAULT_SOURCE
38
from synnefo.quotas.util import (get_db_holdings, get_quotaholder_holdings,
39
                                 transform_quotas)
38 40
from synnefo.webproject.management.utils import pprint_table
39 41

  
40 42

  
......
61 63
        # Get info from DB
62 64
        db_holdings = get_db_holdings(users)
63 65
        users = db_holdings.keys()
64
        qh_holdings = get_quotaholder_holdings(users)
66
        qh_holdings = get_quotaholder_holdings(userid)
65 67
        qh_users = qh_holdings.keys()
66 68

  
67 69
        if len(qh_users) < len(users):
......
73 75
        unsynced = []
74 76
        for user in users:
75 77
            db = db_holdings[user]
76
            qh = qh_holdings[user]
77
            if not self.verify_resources(user, db.keys(), qh.keys()):
78
                continue
78
            qh_all = qh_holdings[user]
79
            # Assuming only one source
80
            qh = qh_all[DEFAULT_SOURCE]
81
            qh = transform_quotas(qh)
79 82

  
80
            for res in db.keys():
81
                if db[res] != qh[res]:
82
                    unsynced.append((user, res, str(db[res]), str(qh[res])))
83
            for resource, (value, value1) in qh.iteritems:
84
                db_value = db.pop(resource, None)
85
                if value != value1:
86
                    write("Commission pending for %s"
87
                          % str((user, resource)))
88
                    continue
89
                if db_value is None:
90
                    write("Resource %s exists in QH for %s but not in DB\n"
91
                          % (resource, user))
92
                elif db_value != value:
93
                    data = (user, resource, str(db_value), str(value))
94
                    unsynced.append(data)
95

  
96
            for resource, db_value in db.iteritems():
97
                write("Resource %s exists in DB for %s but not in QH\n"
98
                      % (resource, user))
83 99

  
84 100
        if unsynced:
85 101
            pprint_table(self.stderr, unsynced, headers)
86

  
87
    def verify_resources(self, user, db_resources, qh_resources):
88
        write = self.stderr.write
89
        db_res = set(db_resources)
90
        qh_res = set(qh_resources)
91
        if qh_res == db_res:
92
            return True
93
        db_extra = db_res - qh_res
94
        if db_extra:
95
            for res in db_extra:
96
                write("Resource %s exists in DB for %s but not in QH\n"
97
                      % (res, user))
98
        qh_extra = qh_res - db_res
99
        if qh_extra:
100
            for res in qh_extra:
101
                write("Resource %s exists in QH for %s but not in DB\n"
102
                      % (res, user))
103
        return False
b/snf-cyclades-app/synnefo/quotas/util.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
4 4
# without modification, are permitted provided that the following
......
34 34
from django.db.models import Sum, Count
35 35

  
36 36
from synnefo.db.models import VirtualMachine, Network
37
from synnefo.quotas import get_quota_holder, NoEntityError
37
from synnefo.quotas import Quotaholder, ASTAKOS_TOKEN
38 38

  
39 39

  
40 40
def get_db_holdings(users=None):
......
74 74
    return holdings
75 75

  
76 76

  
77
def get_quotaholder_holdings(users=[]):
78
    """Get holdings from Quotaholder.
77
def get_quotaholder_holdings(user=None):
78
    """Get quotas from Quotaholder for all Cyclades resources.
79 79

  
80
    If the entity for the user does not exist in quotaholder, no holding
81
    is returned.
80
    Returns quotas for all users, unless a single user is specified.
82 81
    """
83
    users = filter(lambda u: not u is None, users)
84
    holdings = {}
85
    with get_quota_holder() as qh:
86
        list_holdings = [(user, "1") for user in users]
87
        (qh_holdings, rejected) = qh.list_holdings(context={},
88
                                                   list_holdings=list_holdings)
89
        found_users = filter(lambda u: not u in rejected, users)
90
    for user, user_holdings in zip(found_users, qh_holdings):
91
        if not user_holdings:
92
            continue
93
        for h in user_holdings:
94
            assert(h[0] == user)
95
        user_holdings = filter(lambda x: x[1].startswith("cyclades."),
96
                               user_holdings)
97
        holdings[user] = dict(map(decode_holding, user_holdings))
98
    return holdings
82
    qh = Quotaholder.get()
83
    return qh.get_service_quotas(ASTAKOS_TOKEN, user)
99 84

  
100 85

  
101
def decode_holding(holding):
102
    entity, resource, imported, exported, returned, released = holding
103
    res = resource.replace("cyclades.", "")
104
    return (res, imported - exported + returned - released)
86
def transform_quotas(quotas):
87
    d = {}
88
    for resource, counters in quotas.iteritems():
89
        res = resource.replace("cyclades.", "")
90
        available = counters['available']
91
        limit = counters['limit']
92
        used = counters['used']
93
        used_max = limit - available
94
        d[res] = (used, used_max)
95
    return d

Also available in: Unified diff