imip-agent

Annotated imiptools/handlers/scheduling/quota.py

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