Statistics
| Branch: | Tag: | Revision:

root / snf-astakos-app / astakos / quotaholder / callpoint.py @ 3adbfafa

History | View | Annotate | Download (27.3 kB)

1
# Copyright 2012, 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from astakos.quotaholder.exception import (
35
    QuotaholderError,
36
    CorruptedError, InvalidDataError,
37
    NoQuantityError, NoCapacityError,
38
    DuplicateError)
39

    
40
from astakos.quotaholder.utils.newname import newname
41
from astakos.quotaholder.api import QH_PRACTICALLY_INFINITE
42

    
43
from django.db.models import Q, Count
44
from django.db.models import Q
45
from .models import (Policy, Holding,
46
                     Commission, Provision, ProvisionLog,
47
                     now,
48
                     db_get_holding, db_get_policy,
49
                     db_get_commission, db_filter_provision)
50

    
51

    
52
class QuotaholderDjangoDBCallpoint(object):
53

    
54
    def get_limits(self, context=None, get_limits=()):
55
        limits = []
56
        append = limits.append
57

    
58
        for policy in get_limits:
59
            try:
60
                p = Policy.objects.get(policy=policy)
61
            except Policy.DoesNotExist:
62
                continue
63

    
64
            append((policy, p.quantity, p.capacity))
65

    
66
        return limits
67

    
68
    def set_limits(self, context=None, set_limits=()):
69

    
70
        for (policy, quantity, capacity) in set_limits:
71

    
72
            try:
73
                policy = db_get_policy(policy=policy, for_update=True)
74
            except Policy.DoesNotExist:
75
                Policy.objects.create(policy=policy,
76
                                      quantity=quantity,
77
                                      capacity=capacity,
78
                                      )
79
            else:
80
                policy.quantity = quantity
81
                policy.capacity = capacity
82
                policy.save()
83

    
84
        return ()
85

    
86
    def get_holding(self, context=None, get_holding=()):
87
        holdings = []
88
        append = holdings.append
89

    
90
        for holder, resource in get_holding:
91
            try:
92
                h = Holding.objects.get(holder=holder, resource=resource)
93
            except Holding.DoesNotExist:
94
                continue
95

    
96
            append((h.holder, h.resource, h.policy.policy,
97
                    h.imported, h.exported,
98
                    h.returned, h.released, h.flags))
99

    
100
        return holdings
101

    
102
    def set_holding(self, context=None, set_holding=()):
103
        rejected = []
104
        append = rejected.append
105

    
106
        for holder, resource, policy, flags in set_holding:
107
            try:
108
                p = Policy.objects.get(policy=policy)
109
            except Policy.DoesNotExist:
110
                append((holder, resource, policy))
111
                continue
112

    
113
            try:
114
                h = db_get_holding(holder=holder, resource=resource,
115
                                   for_update=True)
116
                h.policy = p
117
                h.flags = flags
118
                h.save()
119
            except Holding.DoesNotExist:
120
                h = Holding.objects.create(holder=holder, resource=resource,
121
                                           policy=p, flags=flags)
122

    
123
        if rejected:
124
            raise QuotaholderError(rejected)
125
        return rejected
126

    
127
    def _init_holding(self,
128
                      holder, resource, policy,
129
                      imported, exported, returned, released,
130
                      flags):
131
        try:
132
            h = db_get_holding(holder=holder, resource=resource,
133
                               for_update=True)
134
        except Holding.DoesNotExist:
135
            h = Holding(holder=holder, resource=resource)
136

    
137
        h.policy = policy
138
        h.flags = flags
139
        h.imported = imported
140
        h.importing = imported
141
        h.exported = exported
142
        h.exporting = exported
143
        h.returned = returned
144
        h.returning = returned
145
        h.released = released
146
        h.releasing = released
147
        h.save()
148

    
149
    def init_holding(self, context=None, init_holding=()):
150
        rejected = []
151
        append = rejected.append
152

    
153
        for idx, sfh in enumerate(init_holding):
154
            (holder, resource, policy,
155
             imported, exported, returned, released,
156
             flags) = sfh
157

    
158
            try:
159
                p = Policy.objects.get(policy=policy)
160
            except Policy.DoesNotExist:
161
                append(idx)
162
                continue
163

    
164
            self._init_holding(holder, resource, p,
165
                               imported, exported,
166
                               returned, released,
167
                               flags)
168
        if rejected:
169
            raise QuotaholderError(rejected)
170
        return rejected
171

    
172
    def reset_holding(self, context=None, reset_holding=()):
173
        rejected = []
174
        append = rejected.append
175

    
176
        for idx, tpl in enumerate(reset_holding):
177
            (holder, resource,
178
             imported, exported, returned, released) = tpl
179

    
180
            try:
181
                h = db_get_holding(holder=holder, resource=resource,
182
                                   for_update=True)
183
                h.imported = imported
184
                h.importing = imported
185
                h.exported = exported
186
                h.exporting = exported
187
                h.returned = returned
188
                h.returning = returned
189
                h.released = released
190
                h.releasing = released
191
                h.save()
192
            except Holding.DoesNotExist:
193
                append(idx)
194
                continue
195

    
196
        if rejected:
197
            raise QuotaholderError(rejected)
198
        return rejected
199

    
200
    def _check_pending(self, holder, resource):
201
        cs = Commission.objects.filter(holder=holder)
202
        cs = [c for c in cs if c.provisions.filter(resource=resource)]
203
        as_target = [c.serial for c in cs]
204

    
205
        ps = Provision.objects.filter(holder=holder, resource=resource)
206
        as_source = [p.serial.serial for p in ps]
207

    
208
        return as_target + as_source
209

    
210
    def _actual_quantity(self, holding):
211
        hp = holding.policy
212
        return hp.quantity + (holding.imported + holding.returned -
213
                              holding.exported - holding.released)
214

    
215
    def release_holding(self, context=None, release_holding=()):
216
        rejected = []
217
        append = rejected.append
218

    
219
        for idx, (holder, resource) in enumerate(release_holding):
220
            try:
221
                h = db_get_holding(holder=holder, resource=resource,
222
                                   for_update=True)
223
            except Holding.DoesNotExist:
224
                append(idx)
225
                continue
226

    
227
            if self._check_pending(holder, resource):
228
                append(idx)
229
                continue
230

    
231
            q = self._actual_quantity(h)
232
            if q > 0:
233
                append(idx)
234
                continue
235

    
236
            h.delete()
237

    
238
        if rejected:
239
            raise QuotaholderError(rejected)
240
        return rejected
241

    
242
    def list_resources(self, context=None, holder=None):
243
        holdings = Holding.objects.filter(holder=holder)
244
        resources = [h.resource for h in holdings]
245
        return resources
246

    
247
    def list_holdings(self, context=None, list_holdings=()):
248
        rejected = []
249
        reject = rejected.append
250
        holdings_list = []
251
        append = holdings_list.append
252

    
253
        for holder in list_holdings:
254
            holdings = list(Holding.objects.filter(holder=holder))
255
            if not holdings:
256
                reject(holder)
257
                continue
258

    
259
            append([(holder, h.resource,
260
                     h.imported, h.exported, h.returned, h.released)
261
                    for h in holdings])
262

    
263
        return holdings_list, rejected
264

    
265
    def get_quota(self, context=None, get_quota=()):
266
        quotas = []
267
        append = quotas.append
268

    
269
        holders = set(holder for holder, r in get_quota)
270
        hs = Holding.objects.select_related().filter(holder__in=holders)
271
        holdings = {}
272
        for h in hs:
273
            holdings[(h.holder, h.resource)] = h
274

    
275
        for holder, resource in get_quota:
276
            try:
277
                h = holdings[(holder, resource)]
278
            except:
279
                continue
280

    
281
            p = h.policy
282

    
283
            append((h.holder, h.resource, p.quantity, p.capacity,
284
                    h.imported, h.exported,
285
                    h.returned, h.released,
286
                    h.flags))
287

    
288
        return quotas
289

    
290
    def set_quota(self, context=None, set_quota=()):
291
        rejected = []
292
        append = rejected.append
293

    
294
        q_holdings = Q()
295
        holders = []
296
        for (holder, resource, _, _, _) in set_quota:
297
            holders.append(holder)
298

    
299
        hs = Holding.objects.filter(holder__in=holders).select_for_update()
300
        holdings = {}
301
        for h in hs:
302
            holdings[(h.holder, h.resource)] = h
303

    
304
        old_policies = []
305

    
306
        for (holder, resource,
307
             quantity, capacity,
308
             flags) in set_quota:
309

    
310
            policy = newname('policy_')
311
            newp = Policy(policy=policy,
312
                          quantity=quantity,
313
                          capacity=capacity,
314
                          )
315

    
316
            try:
317
                h = holdings[(holder, resource)]
318
                old_policies.append(h.policy_id)
319
                h.policy = newp
320
                h.flags = flags
321
            except KeyError:
322
                h = Holding(holder=holder, resource=resource,
323
                            policy=newp, flags=flags)
324

    
325
            # the order is intentionally reversed so that it
326
            # would break if we are not within a transaction.
327
            # Has helped before.
328
            h.save()
329
            newp.save()
330
            holdings[(holder, resource)] = h
331

    
332
        objs = Policy.objects.annotate(refs=Count('holding'))
333
        objs.filter(policy__in=old_policies, refs=0).delete()
334

    
335
        if rejected:
336
            raise QuotaholderError(rejected)
337
        return rejected
338

    
339
    def add_quota(self,
340
                  context=None,
341
                  sub_quota=(), add_quota=()):
342
        rejected = []
343
        append = rejected.append
344

    
345
        sources = sub_quota + add_quota
346
        q_holdings = Q()
347
        holders = []
348
        for (holder, resource, _, _) in sources:
349
            holders.append(holder)
350

    
351
        hs = Holding.objects.filter(holder__in=holders).select_for_update()
352
        holdings = {}
353
        for h in hs:
354
            holdings[(h.holder, h.resource)] = h
355

    
356
        pids = [h.policy_id for h in hs]
357
        policies = Policy.objects.in_bulk(pids)
358

    
359
        old_policies = []
360

    
361
        for removing, source in [(True, sub_quota), (False, add_quota)]:
362
            for (holder, resource,
363
                 quantity, capacity,
364
                 ) in source:
365

    
366
                try:
367
                    h = holdings[(holder, resource)]
368
                    old_policies.append(h.policy_id)
369
                    try:
370
                        p = policies[h.policy_id]
371
                    except KeyError:
372
                        raise AssertionError("no policy %s" % h.policy_id)
373
                except KeyError:
374
                    if removing:
375
                        append((holder, resource))
376
                        continue
377

    
378
                    h = Holding(holder=holder, resource=resource, flags=0)
379
                    p = None
380

    
381
                policy = newname('policy_')
382
                newp = Policy(policy=policy)
383

    
384
                newp.quantity = _add(p.quantity if p else 0, quantity,
385
                                     invert=removing)
386
                newp.capacity = _add(p.capacity if p else 0, capacity,
387
                                     invert=removing)
388

    
389
                if _isneg(newp.capacity):
390
                    append((holder, resource))
391
                    continue
392

    
393
                h.policy = newp
394

    
395
                # the order is intentionally reversed so that it
396
                # would break if we are not within a transaction.
397
                # Has helped before.
398
                h.save()
399
                newp.save()
400
                policies[policy] = newp
401
                holdings[(holder, resource)] = h
402

    
403
        objs = Policy.objects.annotate(refs=Count('holding'))
404
        objs.filter(policy__in=old_policies, refs=0).delete()
405

    
406
        if rejected:
407
            raise QuotaholderError(rejected)
408

    
409
        return rejected
410

    
411
    def issue_commission(self,
412
                         context=None,
413
                         clientkey=None,
414
                         target=None,
415
                         name=None,
416
                         provisions=()):
417

    
418
        create = Commission.objects.create
419
        commission = create(holder=target, clientkey=clientkey, name=name)
420
        serial = commission.serial
421

    
422
        checked = []
423
        for holder, resource, quantity in provisions:
424

    
425
            if holder == target:
426
                m = "Cannot issue commission from an holder to itself (%s)" % (
427
                    holder,)
428
                raise InvalidDataError(m)
429

    
430
            ent_res = holder, resource
431
            if ent_res in checked:
432
                m = "Duplicate provision for %s.%s" % ent_res
433
                raise DuplicateError(m)
434
            checked.append(ent_res)
435

    
436
            release = 0
437
            if quantity < 0:
438
                release = 1
439

    
440
            # Source limits checks
441
            try:
442
                h = db_get_holding(holder=holder, resource=resource,
443
                                   for_update=True)
444
            except Holding.DoesNotExist:
445
                m = ("There is no quantity "
446
                     "to allocate from in %s.%s" % (holder, resource))
447
                raise NoQuantityError(m,
448
                                      source=holder, target=target,
449
                                      resource=resource, requested=quantity,
450
                                      current=0, limit=0)
451

    
452
            hp = h.policy
453

    
454
            if not release:
455
                limit = hp.quantity + h.imported - h.releasing
456
                unavailable = h.exporting - h.returned
457
                available = limit - unavailable
458

    
459
                if quantity > available:
460
                    m = ("There is not enough quantity "
461
                         "to allocate from in %s.%s" % (holder, resource))
462
                    raise NoQuantityError(m,
463
                                          source=holder,
464
                                          target=target,
465
                                          resource=resource,
466
                                          requested=quantity,
467
                                          current=unavailable,
468
                                          limit=limit)
469
            else:
470
                current = (+ h.importing + h.returning
471
                           - h.exported - h.returned)
472
                limit = hp.capacity
473
                if current - quantity > limit:
474
                    m = ("There is not enough capacity "
475
                         "to release to in %s.%s" % (holder, resource))
476
                    raise NoQuantityError(m,
477
                                          source=holder,
478
                                          target=target,
479
                                          resource=resource,
480
                                          requested=quantity,
481
                                          current=current,
482
                                          limit=limit)
483

    
484
            # Target limits checks
485
            try:
486
                th = db_get_holding(holder=target, resource=resource,
487
                                    for_update=True)
488
            except Holding.DoesNotExist:
489
                m = ("There is no capacity "
490
                     "to allocate into in %s.%s" % (target, resource))
491
                raise NoCapacityError(m,
492
                                      source=holder,
493
                                      target=target,
494
                                      resource=resource,
495
                                      requested=quantity,
496
                                      current=0,
497
                                      limit=0)
498

    
499
            tp = th.policy
500

    
501
            if not release:
502
                limit = tp.quantity + tp.capacity
503
                current = (+ th.importing + th.returning + tp.quantity
504
                           - th.exported - th.released)
505

    
506
                if current + quantity > limit:
507
                    m = ("There is not enough capacity "
508
                         "to allocate into in %s.%s" % (target, resource))
509
                    raise NoCapacityError(m,
510
                                          source=holder,
511
                                          target=target,
512
                                          resource=resource,
513
                                          requested=quantity,
514
                                          current=current,
515
                                          limit=limit)
516
            else:
517
                limit = tp.quantity + th.imported - th.releasing
518
                unavailable = th.exporting - th.returned
519
                available = limit - unavailable
520

    
521
                if available + quantity < 0:
522
                    m = ("There is not enough quantity "
523
                         "to release from in %s.%s" % (target, resource))
524
                    raise NoCapacityError(m,
525
                                          source=holder,
526
                                          target=target,
527
                                          resource=resource,
528
                                          requested=quantity,
529
                                          current=unavailable,
530
                                          limit=limit)
531

    
532
            Provision.objects.create(serial=commission,
533
                                     holder=holder,
534
                                     resource=resource,
535
                                     quantity=quantity)
536
            if release:
537
                h.returning -= quantity
538
                th.releasing -= quantity
539
            else:
540
                h.exporting += quantity
541
                th.importing += quantity
542

    
543
            h.save()
544
            th.save()
545

    
546
        return serial
547

    
548
    def _log_provision(self,
549
                       commission, s_holding, t_holding,
550
                       provision, log_time, reason):
551

    
552
        s_holder = s_holding.holder
553
        s_policy = s_holding.policy
554
        t_holder = t_holding.holder
555
        t_policy = t_holding.policy
556

    
557
        kwargs = {
558
            'serial':              commission.serial,
559
            'name':                commission.name,
560
            'source':              s_holder,
561
            'target':              t_holder,
562
            'resource':            provision.resource,
563
            'source_quantity':     s_policy.quantity,
564
            'source_capacity':     s_policy.capacity,
565
            'source_imported':     s_holding.imported,
566
            'source_exported':     s_holding.exported,
567
            'source_returned':     s_holding.returned,
568
            'source_released':     s_holding.released,
569
            'target_quantity':     t_policy.quantity,
570
            'target_capacity':     t_policy.capacity,
571
            'target_imported':     t_holding.imported,
572
            'target_exported':     t_holding.exported,
573
            'target_returned':     t_holding.returned,
574
            'target_released':     t_holding.released,
575
            'delta_quantity':      provision.quantity,
576
            'issue_time':          commission.issue_time,
577
            'log_time':            log_time,
578
            'reason':              reason,
579
        }
580

    
581
        ProvisionLog.objects.create(**kwargs)
582

    
583
    def accept_commission(self,
584
                          context=None, clientkey=None,
585
                          serials=(), reason=''):
586
        log_time = now()
587

    
588
        for serial in serials:
589
            try:
590
                c = db_get_commission(clientkey=clientkey, serial=serial,
591
                                      for_update=True)
592
            except Commission.DoesNotExist:
593
                return
594

    
595
            t = c.holder
596

    
597
            provisions = db_filter_provision(serial=serial, for_update=True)
598
            for pv in provisions:
599
                try:
600
                    h = db_get_holding(holder=pv.holder,
601
                                       resource=pv.resource, for_update=True)
602
                    th = db_get_holding(holder=t, resource=pv.resource,
603
                                        for_update=True)
604
                except Holding.DoesNotExist:
605
                    m = "Corrupted provision"
606
                    raise CorruptedError(m)
607

    
608
                quantity = pv.quantity
609
                release = 0
610
                if quantity < 0:
611
                    release = 1
612

    
613
                if release:
614
                    h.returned -= quantity
615
                    th.released -= quantity
616
                else:
617
                    h.exported += quantity
618
                    th.imported += quantity
619

    
620
                reason = 'ACCEPT:' + reason[-121:]
621
                self._log_provision(c, h, th, pv, log_time, reason)
622
                h.save()
623
                th.save()
624
                pv.delete()
625
            c.delete()
626

    
627
        return
628

    
629
    def reject_commission(self,
630
                          context=None, clientkey=None,
631
                          serials=(), reason=''):
632
        log_time = now()
633

    
634
        for serial in serials:
635
            try:
636
                c = db_get_commission(clientkey=clientkey, serial=serial,
637
                                      for_update=True)
638
            except Commission.DoesNotExist:
639
                return
640

    
641
            t = c.holder
642

    
643
            provisions = db_filter_provision(serial=serial, for_update=True)
644
            for pv in provisions:
645
                try:
646
                    h = db_get_holding(holder=pv.holder,
647
                                       resource=pv.resource, for_update=True)
648
                    th = db_get_holding(holder=t, resource=pv.resource,
649
                                        for_update=True)
650
                except Holding.DoesNotExist:
651
                    m = "Corrupted provision"
652
                    raise CorruptedError(m)
653

    
654
                quantity = pv.quantity
655
                release = 0
656
                if quantity < 0:
657
                    release = 1
658

    
659
                if release:
660
                    h.returning += quantity
661
                    th.releasing += quantity
662
                else:
663
                    h.exporting -= quantity
664
                    th.importing -= quantity
665

    
666
                reason = 'REJECT:' + reason[-121:]
667
                self._log_provision(c, h, th, pv, log_time, reason)
668
                h.save()
669
                th.save()
670
                pv.delete()
671
            c.delete()
672

    
673
        return
674

    
675
    def get_pending_commissions(self, context=None, clientkey=None):
676
        pending = Commission.objects.filter(clientkey=clientkey)
677
        pending_list = pending.values_list('serial', flat=True)
678
        return pending_list
679

    
680
    def resolve_pending_commissions(self,
681
                                    context=None, clientkey=None,
682
                                    max_serial=None, accept_set=()):
683
        accept_set = set(accept_set)
684
        pending = self.get_pending_commissions(context=context,
685
                                               clientkey=clientkey)
686
        pending = sorted(pending)
687

    
688
        accept = self.accept_commission
689
        reject = self.reject_commission
690

    
691
        for serial in pending:
692
            if serial > max_serial:
693
                break
694

    
695
            if serial in accept_set:
696
                accept(context=context, clientkey=clientkey, serials=[serial])
697
            else:
698
                reject(context=context, clientkey=clientkey, serials=[serial])
699

    
700
        return
701

    
702
    def get_timeline(self, context=None, after="", before="Z", get_timeline=()):
703
        holder_set = set()
704
        e_add = holder_set.add
705
        resource_set = set()
706
        r_add = resource_set.add
707

    
708
        for holder, resource in get_timeline:
709
            if holder not in holder_set:
710
                e_add(holder)
711

    
712
            r_add((holder, resource))
713

    
714
        chunk_size = 65536
715
        nr = 0
716
        timeline = []
717
        append = timeline.append
718
        filterlogs = ProvisionLog.objects.filter
719
        if holder_set:
720
            q_holder = Q(source__in=holder_set) | Q(target__in=holder_set)
721
        else:
722
            q_holder = Q()
723

    
724
        while 1:
725
            logs = filterlogs(q_holder,
726
                              issue_time__gt=after,
727
                              issue_time__lte=before,
728
                              reason__startswith='ACCEPT:')
729

    
730
            logs = logs.order_by('issue_time')
731
            #logs = logs.values()
732
            logs = logs[:chunk_size]
733
            nr += len(logs)
734
            if not logs:
735
                break
736
            for g in logs:
737
                if ((g.source, g.resource) not in resource_set
738
                    or (g.target, g.resource) not in resource_set):
739
                    continue
740

    
741
                o = {
742
                    'serial':                   g.serial,
743
                    'source':                   g.source,
744
                    'target':                   g.target,
745
                    'resource':                 g.resource,
746
                    'name':                     g.name,
747
                    'quantity':                 g.delta_quantity,
748
                    'source_allocated':         g.source_allocated(),
749
                    'source_allocated_through': g.source_allocated_through(),
750
                    'source_inbound':           g.source_inbound(),
751
                    'source_inbound_through':   g.source_inbound_through(),
752
                    'source_outbound':          g.source_outbound(),
753
                    'source_outbound_through':  g.source_outbound_through(),
754
                    'target_allocated':         g.target_allocated(),
755
                    'target_allocated_through': g.target_allocated_through(),
756
                    'target_inbound':           g.target_inbound(),
757
                    'target_inbound_through':   g.target_inbound_through(),
758
                    'target_outbound':          g.target_outbound(),
759
                    'target_outbound_through':  g.target_outbound_through(),
760
                    'issue_time':               g.issue_time,
761
                    'log_time':                 g.log_time,
762
                    'reason':                   g.reason,
763
                }
764

    
765
                append(o)
766

    
767
            after = g.issue_time
768
            if after >= before:
769
                break
770

    
771
        return timeline
772

    
773

    
774
def _add(x, y, invert=False):
775
    return x + y if not invert else x - y
776

    
777

    
778
def _isneg(x):
779
    return x < 0
780

    
781

    
782
API_Callpoint = QuotaholderDjangoDBCallpoint