1.1 --- a/conf/cron/cron.daily/imip-agent Mon Feb 08 18:46:31 2016 +0100
1.2 +++ b/conf/cron/cron.daily/imip-agent Mon Feb 08 20:18:15 2016 +0100
1.3 @@ -4,3 +4,4 @@
1.4 export PYTHONPATH="$INSTALL_DIR"
1.5
1.6 "$INSTALL_DIR/tools/make_freebusy.py" all -n -s
1.7 +"$INSTALL_DIR/tools/update_quotas.py" all -s
2.1 --- a/docs/wiki/CronIntegration Mon Feb 08 18:46:31 2016 +0100
2.2 +++ b/docs/wiki/CronIntegration Mon Feb 08 20:18:15 2016 +0100
2.3 @@ -1,5 +1,22 @@
2.4 = Cron Task Scheduler Integration =
2.5
2.6 +The `conf/cron/cron.daily/imip-agent` file contains commands that update
2.7 +the following:
2.8 +
2.9 + * Free/busy collections for all known users
2.10 + * Quota records for all known quota groups
2.11 +
2.12 +This file should be copied to the appropriate destination. For example:
2.13 +
2.14 +{{{
2.15 +cp conf/cron/cron.daily/imip-agent /etc/cron.daily/
2.16 +}}}
2.17 +
2.18 +Where frequency-specific directories are not supported by cron on a system, a
2.19 +`crontab` entry of the appropriate format is required instead.
2.20 +
2.21 +== Event Recurrences ==
2.22 +
2.23 The periods defined by recurring events are not all recorded in a user's
2.24 free/busy collection if such events recur indefinitely. Instead, only the
2.25 periods within a certain window of time are recorded for such events. As a
2.26 @@ -7,17 +24,6 @@
2.27 time to include periods that were ignored when previously recording
2.28 free/busy information for an event.
2.29
2.30 -The `conf/cron/cron.daily/imip-agent` file contains commands that update
2.31 -free/busy collections for all known users, and this should be copied to the
2.32 -appropriate destination. For example:
2.33 -
2.34 -{{{
2.35 -cp conf/cron/cron.daily/imip-agent /etc/cron.daily/
2.36 -}}}
2.37 -
2.38 -Where frequency-specific directories are not supported by cron on a system, a
2.39 -`crontab` entry of the appropriate format is required instead.
2.40 -
2.41 See the [[../EventRecurrences|guide to event recurrences]] for more information
2.42 on how recurring events are supported.
2.43
2.44 @@ -25,3 +31,18 @@
2.45 `tools/make_freebusy.py` program, which is a general tool that can also
2.46 reset the free/busy records defined for a user or those made available to a
2.47 user.
2.48 +
2.49 +== Quota Journals ==
2.50 +
2.51 +As events are confirmed for resources, where quotas on resources have been
2.52 +imposed, such quotas will be consumed until eventually exhausted, thus
2.53 +preventing future reservations. By expiring records of past events, quotas
2.54 +can effectively be replenished, allowing reservations to be made for future
2.55 +events.
2.56 +
2.57 +See the [[../Resources|resources guide]] for more information in imposing
2.58 +quotas on groups of resources.
2.59 +
2.60 +Responsibility for updating the quota records lies with the
2.61 +`tools/update_quotas.py` program, which can be used manually to update quota
2.62 +information for the indicated quota groups.
3.1 --- a/imip_store.py Mon Feb 08 18:46:31 2016 +0100
3.2 +++ b/imip_store.py Mon Feb 08 20:18:15 2016 +0100
3.3 @@ -961,6 +961,24 @@
3.4 def __init__(self, store_dir=None):
3.5 FileBase.__init__(self, store_dir or JOURNAL_DIR)
3.6
3.7 + # Quota and user identity/group discovery.
3.8 +
3.9 + def get_quotas(self):
3.10 +
3.11 + "Return a list of quotas."
3.12 +
3.13 + return listdir(self.store_dir)
3.14 +
3.15 + def get_quota_users(self, quota):
3.16 +
3.17 + "Return a list of quota users."
3.18 +
3.19 + filename = self.get_object_in_store(quota, "journal")
3.20 + if not filename or not isdir(filename):
3.21 + return []
3.22 +
3.23 + return listdir(filename)
3.24 +
3.25 # Groups of users sharing quotas.
3.26
3.27 def get_groups(self, quota):
4.1 --- a/tools/install.sh Mon Feb 08 18:46:31 2016 +0100
4.2 +++ b/tools/install.sh Mon Feb 08 20:18:15 2016 +0100
4.3 @@ -81,7 +81,7 @@
4.4
4.5 # Tools
4.6
4.7 -TOOLS="fix.sh init.sh init_user.sh make_freebusy.py update_scheduling_modules.py"
4.8 +TOOLS="fix.sh init.sh init_user.sh make_freebusy.py update_quotas.py update_scheduling_modules.py"
4.9
4.10 if [ ! -e "$INSTALL_DIR/tools" ]; then
4.11 mkdir -p "$INSTALL_DIR/tools"
5.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
5.2 +++ b/tools/update_quotas.py Mon Feb 08 20:18:15 2016 +0100
5.3 @@ -0,0 +1,185 @@
5.4 +#!/usr/bin/env python
5.5 +
5.6 +"""
5.7 +Remove expired events from quota journals.
5.8 +
5.9 +Copyright (C) 2014, 2015, 2016 Paul Boddie <paul@boddie.org.uk>
5.10 +
5.11 +This program is free software; you can redistribute it and/or modify it under
5.12 +the terms of the GNU General Public License as published by the Free Software
5.13 +Foundation; either version 3 of the License, or (at your option) any later
5.14 +version.
5.15 +
5.16 +This program is distributed in the hope that it will be useful, but WITHOUT
5.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
5.18 +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
5.19 +details.
5.20 +
5.21 +You should have received a copy of the GNU General Public License along with
5.22 +this program. If not, see <http://www.gnu.org/licenses/>.
5.23 +"""
5.24 +
5.25 +from os.path import split
5.26 +import sys
5.27 +
5.28 +# Find the modules.
5.29 +
5.30 +try:
5.31 + import imiptools
5.32 +except ImportError:
5.33 + parent = split(split(__file__)[0])[0]
5.34 + if split(parent)[1] == "imip-agent":
5.35 + sys.path.append(parent)
5.36 +
5.37 +from codecs import getwriter
5.38 +from imiptools.dates import get_datetime, get_default_timezone, get_time, \
5.39 + to_utc_datetime
5.40 +from imip_store import FileJournal
5.41 +
5.42 +def remove_expired_entries(entries, expiry):
5.43 +
5.44 + "Remove from 'entries' events that end at or before 'expiry'."
5.45 +
5.46 + removed = []
5.47 +
5.48 + i = 0
5.49 + while i < len(entries):
5.50 + uid, recurrenceid, duration, found_expiry = entry = entries[i]
5.51 + found_expiry = get_datetime(found_expiry)
5.52 +
5.53 + if found_expiry <= expiry:
5.54 + removed.append(entry)
5.55 + del entries[i]
5.56 + else:
5.57 + i += 1
5.58 +
5.59 + return removed
5.60 +
5.61 +def update_entries(journal, quota, expiry, store, verbose):
5.62 +
5.63 + """
5.64 + Using the given 'journal' process quota records for the given 'quota', with
5.65 + the given 'expiry' time used to expire events ending before or at this time,
5.66 + with None meaning the current time.
5.67 +
5.68 + If 'store' is set, the stored details will be updated; otherwise, the
5.69 + details will be written to standard output.
5.70 +
5.71 + If 'verbose' is set, messages will be written to standard error.
5.72 + """
5.73 +
5.74 + if not store:
5.75 + stdout = getwriter("utf-8")(sys.stdout)
5.76 + if verbose:
5.77 + stderr = getwriter("utf-8")(sys.stderr)
5.78 +
5.79 + if not expiry:
5.80 + expiry = get_time()
5.81 +
5.82 + journal.acquire_lock(quota)
5.83 +
5.84 + try:
5.85 + for user in journal.get_quota_users(quota):
5.86 + if verbose:
5.87 + print >>stderr, user
5.88 +
5.89 + entries = journal.get_entries(quota, user)
5.90 + removed = remove_expired_entries(entries, expiry)
5.91 +
5.92 + if verbose:
5.93 + for entry in removed:
5.94 + print >>stderr, "Removed", entry
5.95 +
5.96 + # Store the processed entries.
5.97 +
5.98 + if store:
5.99 + journal.set_entries(quota, user, entries)
5.100 +
5.101 + # Alternatively, just write the entries to standard output.
5.102 +
5.103 + else:
5.104 + for entry in entries:
5.105 + print >>stdout, "\t".join([(s or "") for s in entry])
5.106 + finally:
5.107 + journal.release_lock(quota)
5.108 +
5.109 +# Main program.
5.110 +
5.111 +if __name__ == "__main__":
5.112 +
5.113 + # Interpret the command line arguments.
5.114 +
5.115 + quotas = []
5.116 + args = []
5.117 + journal_dir = []
5.118 + expiry = []
5.119 + ignored = []
5.120 +
5.121 + # Collect quota details first, switching to other arguments when encountering
5.122 + # switches.
5.123 +
5.124 + l = quotas
5.125 +
5.126 + for arg in sys.argv[1:]:
5.127 + if arg in ("-s", "-v"):
5.128 + args.append(arg)
5.129 + l = ignored
5.130 + elif arg == "-j":
5.131 + l = journal_dir
5.132 + elif arg == "-e":
5.133 + l = expiry
5.134 + else:
5.135 + l.append(arg)
5.136 +
5.137 + try:
5.138 + quota = quotas[0]
5.139 + except IndexError:
5.140 + print >>sys.stderr, """\
5.141 +Usage: %s <quota> <options>
5.142 +
5.143 +Need a quota along with the -s option if updating the journal.
5.144 +Specify -v for additional messages on standard error.
5.145 +
5.146 +General options:
5.147 +
5.148 +-e indicate an expiry time for events (default is now)
5.149 +-j indicate the journal directory location
5.150 +""" % split(sys.argv[0])[1]
5.151 + sys.exit(1)
5.152 +
5.153 + # Define any other options.
5.154 +
5.155 + store = "-s" in args
5.156 + verbose = "-v" in args
5.157 +
5.158 + # Override defaults if indicated.
5.159 +
5.160 + journal_dir = journal_dir and journal_dir[0] or None
5.161 + expiry = expiry and expiry[0] or None
5.162 +
5.163 + if expiry:
5.164 + expiry = to_utc_datetime(get_datetime(expiry), get_default_timezone())
5.165 + if not expiry:
5.166 + print >>sys.stderr, "Expiry time must be a valid datetime."
5.167 + sys.exit(1)
5.168 +
5.169 + # Obtain store-related objects.
5.170 +
5.171 + journal = FileJournal(journal_dir)
5.172 +
5.173 + # Obtain a list of users for processing.
5.174 +
5.175 + if quota in ("*", "all"):
5.176 + quotas = journal.get_quotas()
5.177 +
5.178 + # Process the given users.
5.179 +
5.180 + if verbose:
5.181 + stderr = getwriter("utf-8")(sys.stderr)
5.182 +
5.183 + for quota in quotas:
5.184 + if verbose:
5.185 + print >>stderr, quota
5.186 + update_entries(journal, quota, expiry, store, verbose)
5.187 +
5.188 +# vim: tabstop=4 expandtab shiftwidth=4