imip-agent

imiptools/handlers/scheduling/quota.py

1040:8f2b373a311b
2016-02-08 Paul Boddie Introduced compound locking so that information can be in a consistent state for scheduling functions and confirmation functions within the same transaction, unchanged by concurrent transactions.
     1 #!/usr/bin/env python     2      3 """     4 Quota-related scheduling functionality.     5      6 Copyright (C) 2016 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from imiptools.dates import format_duration, get_duration    23 from imiptools.data import get_uri    24 from imiptools.period import Endless    25 from datetime import timedelta    26     27 # Quota maintenance.    28     29 def check_quota(handler, args):    30     31     """    32     Check the current object of the given 'handler' against the applicable    33     quota.    34     """    35     36     quota, group = _get_quota_and_group(handler, args)    37     38     # Obtain the journal entries and check the balance.    39     40     journal = handler.get_journal()    41     entries = journal.get_entries(quota, group)    42     limits = journal.get_limits(quota)    43     44     # Obtain a limit for the group or any general limit.    45     # Decline invitations if no limit has been set.    46     47     limit = limits.get(group) or limits.get("*")    48     if not limit:    49         return "DECLINED"    50     51     # Decline events whose durations exceed the balance.    52     53     total = _get_duration(handler)    54     55     if total == Endless():    56         return "DECLINED"    57     58     balance = get_duration(limit) - _get_usage(entries)    59     60     if total > balance:    61         return "DECLINED"    62     else:    63         return "ACCEPTED"    64     65 def add_to_quota(handler, args):    66     67     """    68     Record details of the current object of the given 'handler' in the    69     applicable quota.    70     """    71     72     quota, group = _get_quota_and_group(handler, args)    73     74     total = _get_duration(handler)    75     76     # Obtain the journal entries and limits.    77     78     journal = handler.get_journal()    79     entries = journal.get_entries(quota, group)    80     81     if _add_to_entries(entries, handler.obj.get_uid(), handler.obj.get_recurrenceid(), format_duration(total)):    82         journal.set_entries(quota, group, entries)    83     84 def remove_from_quota(handler, args):    85     86     """    87     Remove details of the current object of the given 'handler' from the    88     applicable quota.    89     """    90     91     quota, group = _get_quota_and_group(handler, args)    92     93     total = _get_duration(handler)    94     95     # Obtain the journal entries and limits.    96     97     journal = handler.get_journal()    98     entries = journal.get_entries(quota, group)    99    100     if _remove_from_entries(entries, handler.obj.get_uid(), handler.obj.get_recurrenceid(), format_duration(total)):   101         journal.set_entries(quota, group, entries)   102    103 def _get_quota_and_group(handler, args):   104    105     """   106     Combine information about the current object from the 'handler' with the   107     given 'args' to return a tuple containing the quota group and the user   108     identity or group involved.   109     """   110    111     quota = args and args[0] or handler.user   112    113     # Obtain the identity to whom the quota will apply.   114    115     organiser = get_uri(handler.obj.get_value("ORGANIZER"))   116    117     # Obtain any user group to which the quota will apply instead.   118    119     journal = handler.get_journal()   120     groups = journal.get_groups(quota)   121    122     return quota, groups.get(organiser) or organiser   123    124 def _get_duration(handler):   125    126     "Return the duration of the current object provided by the 'handler'."   127    128     # Count only explicit periods.   129     # NOTE: Should reject indefinitely recurring events.   130    131     total = timedelta(0)   132    133     for period in handler.get_periods(handler.obj, explicit_only=True):   134         duration = period.get_duration()   135    136         # Decline events whose period durations are endless.   137    138         if duration == Endless():   139             return duration   140         else:   141             total += duration   142    143     return total   144    145 def _get_usage(entries):   146    147     "Return the usage total according to the given 'entries'."   148    149     total = timedelta(0)   150    151     for found_uid, found_recurrenceid, found_duration in entries:   152         retraction = found_duration.startswith("-")   153         multiplier = retraction and -1 or 1   154         total += multiplier * get_duration(found_duration[retraction and 1 or 0:])   155    156     return total   157    158 def _add_to_entries(entries, uid, recurrenceid, duration):   159    160     """   161     Add to 'entries' an entry for the event having the given 'uid' and   162     'recurrenceid' with the given 'duration'.   163     """   164    165     confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration)   166    167     # Where a previous entry still applies, retract it if different.   168    169     if confirmed:   170         found_uid, found_recurrenceid, found_duration = confirmed   171         if found_duration != duration:   172             entries.append((found_uid, found_recurrenceid, "-%s" % found_duration))   173         else:   174             return False   175    176     # Without an applicable previous entry, add a new entry.   177    178     entries.append((uid, recurrenceid, duration))   179     return True   180    181 def _remove_from_entries(entries, uid, recurrenceid, duration):   182    183     """   184     Remove from the given 'entries' any entry for the event having the given   185     'uid' and 'recurrenceid' with the given 'duration'.   186     """   187    188     confirmed = _find_applicable_entry(entries, uid, recurrenceid, duration)   189    190     # Where a previous entry still applies, retract it.   191    192     if confirmed:   193         found_uid, found_recurrenceid, found_duration = confirmed   194         entries.append((found_uid, found_recurrenceid, "-%s" % found_duration))   195         return found_duration == duration   196    197     return False   198    199 def _find_applicable_entry(entries, uid, recurrenceid, duration):   200    201     """   202     Within 'entries', find any applicable previous entry for this event,   203     using the 'uid', 'recurrenceid' and 'duration'.   204     """   205    206     confirmed = None   207    208     for found_uid, found_recurrenceid, found_duration in entries:   209         if uid == found_uid and recurrenceid == found_recurrenceid:   210             if found_duration.startswith("-"):   211                 confirmed = None   212             else:   213                 confirmed = found_uid, found_recurrenceid, found_duration   214    215     return confirmed   216    217 # Collective free/busy maintenance.   218    219 def schedule_across_quota(handler, args):   220    221     """   222     Check the current object of the given 'handler' against the schedules   223     managed by the quota.   224     """   225    226     quota, organiser = _get_quota_and_identity(handler, args)   227    228     # If newer than any old version, discard old details from the   229     # free/busy record and check for suitability.   230    231     periods = handler.get_periods(handler.obj)   232     freebusy = handler.get_journal().get_freebusy(quota, organiser)   233     scheduled = handler.can_schedule(freebusy, periods)   234    235     return scheduled and "ACCEPTED" or "DECLINED"   236    237 def add_to_quota_freebusy(handler, args):   238    239     """   240     Record details of the current object of the 'handler' in the applicable   241     free/busy resource.   242     """   243    244     quota, organiser = _get_quota_and_identity(handler, args)   245    246     journal = handler.get_journal()   247     freebusy = journal.get_freebusy(quota, organiser)   248     handler.update_freebusy(freebusy, organiser, True)   249     journal.set_freebusy(quota, organiser, freebusy)   250    251 def remove_from_quota_freebusy(handler, args):   252    253     """   254     Remove details of the current object of the 'handler' from the applicable   255     free/busy resource.   256     """   257    258     quota, organiser = _get_quota_and_identity(handler, args)   259    260     journal = handler.get_journal()   261     freebusy = journal.get_freebusy(quota, organiser)   262     handler.remove_from_freebusy(freebusy)   263     journal.set_freebusy(quota, organiser, freebusy)   264    265 def _get_quota_and_identity(handler, args):   266    267     """   268     Combine information about the current object from the 'handler' with the   269     given 'args' to return a tuple containing the quota group and the user   270     identity involved.   271     """   272    273     quota = args and args[0] or handler.user   274    275     # Obtain the identity for whom the scheduling will apply.   276    277     organiser = get_uri(handler.obj.get_value("ORGANIZER"))   278    279     return quota, organiser   280    281 # Locking and unlocking.   282    283 def lock_journal(handler, args):   284    285     "Using the 'handler' and 'args', lock the journal for the quota."   286    287     handler.get_journal().acquire_lock(_get_quota(handler, args))   288    289 def unlock_journal(handler, args):   290    291     "Using the 'handler' and 'args', unlock the journal for the quota."   292    293     handler.get_journal().release_lock(_get_quota(handler, args))   294    295 def _get_quota(handler, args):   296    297     "Return the quota using the 'handler' and 'args'."   298    299     return args and args[0] or handler.user   300    301 # Registry of scheduling functions.   302    303 scheduling_functions = {   304     "check_quota" : check_quota,   305     "schedule_across_quota" : schedule_across_quota,   306     }   307    308 # Registries of locking and unlocking functions.   309    310 locking_functions = {   311     "check_quota" : lock_journal,   312     "schedule_across_quota" : lock_journal,   313     }   314    315 unlocking_functions = {   316     "check_quota" : unlock_journal,   317     "schedule_across_quota" : unlock_journal,   318     }   319    320 # Registries of listener functions.   321    322 confirmation_functions = {   323     "add_to_quota" : add_to_quota,   324     "add_to_quota_freebusy" : add_to_quota_freebusy,   325     }   326    327 retraction_functions = {   328     "remove_from_quota" : remove_from_quota,   329     "remove_from_quota_freebusy" : remove_from_quota_freebusy,   330     }   331    332 # vim: tabstop=4 expandtab shiftwidth=4