root / pykota / trunk / bin / edpykota @ 2452

Revision 2452, 28.3 kB (checked in by jerome, 19 years ago)

Upgraded database schema.
Added -i | --ingroups command line option to repykota.
Added -C | --comment command line option to edpykota.
Added 'noquota', 'noprint', and 'nochange' as switches for edpykota's
-l | --limitby command line option.
Severity : entirely new features, in need of testers :-)

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
Line 
1#! /usr/bin/env python
2# -*- coding: ISO-8859-15 -*-
3
4# PyKota Print Quota Editor
5#
6# PyKota - Print Quotas for CUPS and LPRng
7#
8# (c) 2003, 2004, 2005 Jerome Alet <alet@librelogiciel.com>
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation; either version 2 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program; if not, write to the Free Software
21# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22#
23# $Id$
24#
25#
26
27import sys
28import os
29import pwd
30import grp
31from pykota.tool import PyKotaTool, PyKotaToolError, crashed, N_
32from pykota.config import PyKotaConfigError
33from pykota.storage import PyKotaStorageError
34
35__doc__ = N_("""edpykota v%(__version__)s (c) %(__years__)s %(__author__)s
36
37A Print Quota editor for PyKota.
38
39command line usage :
40
41  edpykota [options] user1 user2 ... userN
42 
43  edpykota [options] group1 group2 ... groupN
44
45options :
46
47  -v | --version       Prints edpykota's version number then exits.
48  -h | --help          Prints this message then exits.
49 
50  -a | --add           Adds users and/or printers if they don't
51                       exist on the Quota Storage Server.
52                       
53  -d | --delete        Deletes users/groups from the quota storage.
54                       Printers are never deleted.
55                       
56  -c | --charge p[,j]  Sets the price per page and per job to charge
57                       for a particular printer. Job price is optional.
58                       If both are to be set, separate them with a comma.
59                       Floating point values are allowed.
60                       
61  -o | --overcharge f  Sets the overcharging factor applied to the user
62                       when computing the cost of a print job. Positive or
63                       negative floating point values are allowed,
64                       this allows you to do some really creative
65                       things like giving money to an user whenever
66                       he prints. The number of pages in a print job
67                       is not modified by this coefficient, only the
68                       cost of the job for a particular user.
69                       Only users have a coefficient.
70 
71  -i | --ingroups g1[,g2...]  Puts the users into each of the groups
72                              listed, separated by commas. The groups
73                              must already exist in the Quota Storage.
74 
75  -u | --users         Edit users print quotas, this is the default.
76 
77  -P | --printer p     Edit quotas on printer p only. Actually p can
78                       use wildcards characters to select only
79                       some printers. The default value is *, meaning
80                       all printers.
81                       You can specify several names or wildcards,
82                       by separating them with commas.
83 
84  -G | --pgroups pg1[,pg2...] Adds the printer(s) to the printer groups
85                       pg1, pg2, etc... which must already exist.
86                       A printer group is just like a normal printer,
87                       only that it is usually unknown from the printing
88                       system. Create printer groups exactly the same
89                       way that you create printers, then add other
90                       printers to them with this option.
91                       Accounting is done on a printer and on all
92                       the printer groups it belongs to, quota checking
93                       is done on a printer and on all the printer groups
94                       it belongs to.
95 
96  -g | --groups        Edit users groups print quotas instead of users.
97                         
98  -p | --prototype u|g Uses user u or group g as a prototype to set
99                       print quotas
100                       
101  -n | --noquota       Doesn't set a quota but only does accounting.
102                       This is the same as --limitby noquota.
103 
104  -r | --reset         Resets the actual page counter for the user
105                       or group to zero on the specified printers.
106                       The life time page counter is kept unchanged.
107                       
108  -R | --hardreset     Resets the actual and life time page counters
109                       for the user or group to zero on the specified
110                       printers. This is a shortcut for '--used 0'.
111                       
112  -l | --limitby l     Choose if the user/group is limited in printing                     
113                       by its account balance or by its page quota.
114                       The default value is 'quota'. Allowed values
115                       are 'quota' 'balance' 'noquota' 'noprint' 
116                       and 'nochange' :
117                       
118                         - quota : limit by number of pages per printer.
119                         - balance : limit by number of credits in account.
120                         - noquota : no limit, accounting still done.
121                         - nochange : no limit, accounting not done.
122                         - noprint : printing is denied.
123                       NB : nochange and noprint are not supported for groups.
124                       
125  -b | --balance b     Sets the user's account balance to b.                     
126                       Account balance may be increase or decreased
127                       if b is prefixed with + or -.
128                       WARNING : when decreasing account balance,
129                       the total paid so far by the user is decreased
130                       too.
131                       Groups don't have a real balance, but the
132                       sum of their users' account balance.
133                       
134  -C | --comment txt   Defines some informational text to be associated
135                       with a change to an user's account balance.
136                       Only meaningful if -b | --balance is also used.
137                       
138  -S | --softlimit sl  Sets the quota soft limit to sl pages.                       
139 
140  -H | --hardlimit hl  Sets the quota hard limit to hl pages.
141
142  -U | --used usage    Sets the pagecounters for the user to usage pages;
143                       useful for migrating users from a different system
144                       where they have already used some pages. Actual
145                       and Life Time page counters may be increased or decreased
146                       if usage is prefixed with + or -.
147                       WARNING : BOTH page counters are modified in all cases,
148                       so be careful.
149                       NB : if 'usage' equals '0', then the action taken is
150                       the same as if --hardreset was used.
151
152  user1 through userN and group1 through groupN can use wildcards
153  if the --add option is not set.
154 
155examples :                             
156
157  $ edpykota --add -p jerome john paul george ringo/ringo@example.com
158 
159  This will add users john, paul, george and ringo to the quota
160  database, and set their print quotas to the same values than user
161  jerome. User jerome must already exist.
162  User ringo's email address will also be set to 'ringo@example.com'
163 
164  $ edpykota --printer lp -S 50 -H 60 jerome
165 
166  This will set jerome's print quota on the lp printer to a soft limit
167  of 50 pages, and a hard limit of 60 pages. If either user jerome or
168  printer lp doesn't exist on the Quota Storage Server then nothing is done.
169
170  $ edpykota --add --printer lp --ingroups coders,it -S 50 -H 60 jerome
171 
172  Same as above, but if either user jerome or printer lp doesn't exist
173  on the Quota Storage Server they are automatically added. Also
174  user jerome is put into the groups "coders" and "it" which must
175  already exist in the Quota Storage.
176           
177  $ edpykota -g -S 500 -H 550 financial support           
178 
179  This will set print quota soft limit to 500 pages and hard limit
180  to 550 pages for groups financial and support on all printers.
181 
182  $ edpykota --reset jerome "jo*"
183 
184  This will reset jerome's page counter to zero on all printers, as
185  well as every user whose name begins with 'jo'.
186  Their life time page counter on each printer will be kept unchanged.
187  You can also reset the life time page counters by using the
188  --hardreset | -R command line option.
189 
190  $ edpykota --printer hpcolor --noquota jerome
191 
192  This will tell PyKota to not limit jerome when printing on the
193  hpcolor printer. All his jobs will be allowed on this printer, but
194  accounting of the pages he prints will still be kept.
195  Print Quotas for jerome on other printers are unchanged.
196 
197  $ edpykota --limitby balance jerome
198 
199  This will tell PyKota to limit jerome by his account's balance
200  when printing.
201 
202  $ edpykota --balance +10.0 jerome
203 
204  This will increase jerome's account balance by 10.0 (in your
205  own currency). You can decrease the account balance with a
206  dash prefix, and set it to a fixed amount with no prefix.
207 
208  $ edpykota --delete jerome rachel
209 
210  This will completely delete jerome and rachel from the Quota Storage
211  database. All their quotas and jobs will be deleted too.
212 
213  $ edpykota --printer lp --charge 0.1
214 
215  This will set the page price for printer lp to 0.1. Job price
216  will not be changed.
217 
218  $ edpykota --printer hplj1,hplj2 --pgroups Laser,HP
219 
220  This will put printers hplj1 and hplj2 in printers groups Laser and HP.
221  When printing either on hplj1 or hplj2, print quota will also be
222  checked and accounted for on virtual printers Laser and HP.
223 
224  $ edpykota --overcharge 2.5 poorstudent
225 
226  This will overcharge the poorstudent user by a factor of 2.5.
227 
228  $ edpykota --overcharge -1 jerome
229 
230  User jerome will actually earn money whenever he prints.
231 
232  $ edpykota --overcharge 0 boss
233 
234  User boss can print at will, it won't cost him anything because the
235  cost of each print job will be multiplied by zero before charging
236  his account.
237""") 
238       
239class EdPyKota(PyKotaTool) :       
240    """A class for edpykota."""
241    def main(self, names, options) :
242        """Edit user or group quotas."""
243        if not self.config.isAdmin :
244            raise PyKotaToolError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], _("You're not allowed to use this command."))
245       
246        suffix = (options["groups"] and "Group") or "User"       
247       
248        softlimit = hardlimit = None
249       
250        if options["noquota"] :
251            options["limitby"] = "noquota"
252           
253        limitby = options["limitby"]
254        if limitby :
255            limitby = limitby.strip().lower()
256        if limitby :
257            if limitby not in ('quota', 'balance', 'noquota', \
258                                        'noprint', 'nochange') :
259                raise PyKotaToolError, _("Invalid limitby value %s") % options["limitby"]
260            if limitby in ('noquota', 'nochange') :   
261                options["noquota"] = 1
262            if (limitby in ('nochange', 'noprint')) and options["groups"] :   
263                raise PyKotaToolError, _("Invalid limitby value %s") % options["limitby"]
264
265        used = options["used"]
266        if used :
267            used = used.strip()
268            try :
269                int(used)
270            except ValueError :
271                raise PyKotaToolError, _("Invalid used value %s.") % used
272
273        if not options["noquota"] :
274            if options["softlimit"] :
275                try :
276                    softlimit = int(options["softlimit"].strip())
277                except ValueError :   
278                    raise PyKotaToolError, _("Invalid softlimit value %s.") % options["softlimit"]
279            if options["hardlimit"] :
280                try :
281                    hardlimit = int(options["hardlimit"].strip())
282                except ValueError :   
283                    raise PyKotaToolError, _("Invalid hardlimit value %s.") % options["hardlimit"]
284            if (softlimit is not None) and (hardlimit is not None) and (hardlimit < softlimit) :       
285                # error, exchange them
286                self.printInfo(_("Hard limit %i is less than soft limit %i, values will be exchanged.") % (hardlimit, softlimit))
287                (softlimit, hardlimit) = (hardlimit, softlimit)
288           
289        overcharge = options["overcharge"]
290        if overcharge :
291            try :
292                overcharge = float(overcharge.strip())
293            except (ValueError, AttributeError) :   
294                raise PyKotaToolError, _("Invalid overcharge value %s") % options["overcharge"]
295               
296        balance = options["balance"]
297        if balance :
298            balance = balance.strip()
299            try :
300                balancevalue = float(balance)
301            except ValueError :   
302                raise PyKotaToolError, _("Invalid balance value %s") % options["balance"]
303           
304        if options["charge"] :
305            try :
306                charges = [float(part) for part in options["charge"].split(',', 1)]
307            except ValueError :   
308                raise PyKotaToolError, _("Invalid charge amount value %s") % options["charge"]
309            else :   
310                if len(charges) > 2 :
311                    charges = charges[:2]
312                if len(charges) != 2 :
313                    charges = [charges[0], None]
314                   
315        if options["ingroups"] :   
316            groupnames = [gname.strip() for gname in options["ingroups"].split(',')]
317        else :   
318            groupnames = []
319           
320        rejectunknown = self.config.getRejectUnknown()   
321        printeradded = 0
322        printers = self.storage.getMatchingPrinters(options["printer"])
323        if not printers :
324            pname = options["printer"]
325            if options["add"] and pname :
326                if self.isValidName(pname) :
327                    printers = [ self.storage.addPrinter(pname) ]
328                    if printers[0].Exists :
329                        printeradded = 1
330                    else :   
331                        raise PyKotaToolError, _("Impossible to add printer %s") % pname
332                else :   
333                    raise PyKotaToolError, _("Invalid printer name %s") % pname
334            else :
335                raise PyKotaToolError, _("There's no printer matching %s") % pname
336        if not names :   
337            if options["delete"] :   
338                raise PyKotaToolError, _("You have to pass user or group names on the command line")
339            else :
340                names = getattr(self.storage, "getAll%ssNames" % suffix)() # all users or groups
341               
342        printersgroups = []       
343        if options["pgroups"] :       
344            printersgroups = self.storage.getMatchingPrinters(options["pgroups"])
345           
346        if options["prototype"] :   
347            protoentry = getattr(self.storage, "get%s" % suffix)(options["prototype"])
348            if not protoentry.Exists :
349                raise PyKotaToolError, _("Prototype object %s not found in Quota Storage.") % protoentry.Name
350            else :   
351                limitby = protoentry.LimitBy
352                balancevalue = protoentry.AccountBalance
353                if balancevalue is not None :
354                    balance = str(abs(balancevalue))
355                else :   
356                    balance = None
357                overcharge = getattr(protoentry, "OverCharge", None)
358           
359        missingusers = {}
360        missinggroups = {}   
361        todelete = {}   
362        changed = {} # tracks changes made at the user/group level
363        for printer in printers :
364            for pgroup in printersgroups :
365                pgroup.addPrinterToGroup(printer)   
366               
367            if options["charge"] :
368                (perpage, perjob) = charges
369                printer.setPrices(perpage, perjob)   
370               
371            if options["prototype"] :
372                protoquota = getattr(self.storage, "get%sPQuota" % suffix)(protoentry, printer)
373                if not protoquota.Exists :
374                    self.printInfo(_("Prototype %s not found in Quota Storage for printer %s.") % (protoentry.Name, printer.Name))
375                else :   
376                    (softlimit, hardlimit) = (protoquota.SoftLimit, protoquota.HardLimit)
377                   
378            if not options["noquota"] :   
379                if hardlimit is None :   
380                    hardlimit = softlimit
381                    if hardlimit is not None :
382                        self.printInfo(_("Undefined hard limit set to soft limit (%s) on printer %s.") % (str(hardlimit), printer.Name))
383                if softlimit is None :   
384                    softlimit = hardlimit
385                    if softlimit is not None :
386                        self.printInfo(_("Undefined soft limit set to hard limit (%s) on printer %s.") % (str(softlimit), printer.Name))
387                       
388            if options["add"] :   
389                allentries = []   
390                for name in names :
391                    email = ""
392                    if not options["groups"] :
393                        splitname = name.split('/', 1)     # username/email
394                        if len(splitname) == 1 :
395                            splitname.append("")
396                        (name, email) = splitname
397                        if email and (email.count('@') != 1) :
398                            self.printInfo(_("Invalid email address %s") % email)
399                            email = ""
400                    entry = getattr(self.storage, "get%s" % suffix)(name)
401                    if email and not options["groups"] :
402                        entry.Email = email
403                    entrypquota = getattr(self.storage, "get%sPQuota" % suffix)(entry, printer)
404                    allentries.append((entry, entrypquota))
405            else :   
406                allentries = getattr(self.storage, "getPrinter%ssAndQuotas" % suffix)(printer, names)
407               
408            # TODO : do this only once !!!   
409            allnames = [entry.Name for (entry, dummy) in allentries]
410            for name in names :   
411                if not self.matchString(name, allnames) :
412                    if options["groups"] :
413                        missinggroups[name] = 1
414                    else :   
415                        missingusers[name] = 1
416               
417            for (entry, entrypquota) in allentries :
418                if not changed.has_key(entry.Name) :
419                    changed[entry.Name] = {}
420                    if not options["groups"] :
421                        changed[entry.Name]["ingroups"] = []
422                       
423                if not entry.Exists :       
424                    # not found
425                    if options["add"] :
426                        # In case we want to add something, it is crucial
427                        # that we DON'T check with the system accounts files
428                        # like /etc/passwd because users may be defined
429                        # only remotely
430                        if self.isValidName(entry.Name) :
431                            reject = 0
432                            if rejectunknown :
433                                if options["groups"] :
434                                    try :
435                                        grp.getgrnam(entry.Name)
436                                    except KeyError :   
437                                        self.printInfo(_("Unknown group %s") % entry.Name, "error")
438                                        reject = 1
439                                else :   
440                                    try :
441                                        pwd.getpwnam(entry.Name)
442                                    except KeyError :   
443                                        self.printInfo(_("Unknown user %s") % entry.Name, "error")
444                                        reject = 1
445                            if not reject :       
446                                entry = getattr(self.storage, "add%s" % suffix)(entry)
447                        else :   
448                            if options["groups"] :
449                                self.printInfo(_("Invalid group name %s") % entry.Name)
450                            else :   
451                                self.printInfo(_("Invalid user name %s") % entry.Name)
452                    else :
453                        if options["groups"] :
454                            missinggroups[entry.Name] = 1
455                        else :   
456                            missingusers[entry.Name] = 1
457                elif options["delete"] :               
458                    todelete[entry.Name] = entry
459                               
460                if entry.Exists and (not entrypquota.Exists) :
461                    # not found
462                    if options["add"] :
463                        entrypquota = getattr(self.storage, "add%sPQuota" % suffix)(entry, printer)
464                       
465                if not entrypquota.Exists :     
466                    self.printInfo(_("Quota not found for object %s on printer %s.") % (entry.Name, printer.Name))
467                else :   
468                    if options["noquota"] or options["prototype"] or ((softlimit is not None) and (hardlimit is not None)) :
469                        entrypquota.setLimits(softlimit, hardlimit)
470                    if limitby :
471                        if changed[entry.Name].get("limitby") is None :
472                            entry.setLimitBy(limitby)
473                            changed[entry.Name]["limitby"] = limitby
474                   
475                    if options["reset"] :
476                        entrypquota.reset()
477                       
478                    if options["hardreset"] :   
479                        entrypquota.hardreset()
480                       
481                    if not options["groups"] :
482                        if used :
483                            entrypquota.setUsage(used)
484                           
485                        if overcharge is not None :   
486                            if changed[entry.Name].get("overcharge") is None :
487                                entry.setOverChargeFactor(overcharge)
488                                changed[entry.Name]["overcharge"] = overcharge
489                               
490                        if balance :
491                            if changed[entry.Name].get("balance") is None :
492                                if balance.startswith("+") or balance.startswith("-") :
493                                    newbalance = float(entry.AccountBalance or 0.0) + balancevalue
494                                    newlifetimepaid = float(entry.LifeTimePaid or 0.0) + balancevalue
495                                    entry.setAccountBalance(newbalance, newlifetimepaid, options["comment"])
496                                else :
497                                    diff = balancevalue - float(entry.AccountBalance or 0.0)
498                                    newlifetimepaid = float(entry.LifeTimePaid or 0.0) + diff
499                                    entry.setAccountBalance(balancevalue, newlifetimepaid, options["comment"])
500                                changed[entry.Name]["balance"] = balance
501                               
502                        for groupname in groupnames :       
503                            # not executed if option --ingroups is not used
504                            if groupname not in changed[entry.Name]["ingroups"] :
505                                group = self.storage.getGroup(groupname)
506                                if group.Exists :
507                                    self.storage.addUserToGroup(entry, group)
508                                    changed[entry.Name]["ingroups"].append(groupname)
509                                else :
510                                    self.printInfo(_("Group %s not found in the PyKota Storage.") % groupname)
511                       
512        # Now outputs the list of nonexistent users and groups               
513        for name in missingusers.keys() :
514            self.printInfo(_("Nonexistent user %s") % name, level="warn")
515        for name in missinggroups.keys() :
516            self.printInfo(_("Nonexistent group %s") % name, level="warn")
517       
518        # Now delete what has to be deleted               
519        for (name, entry) in todelete.items() :               
520            entry.delete()
521                     
522if __name__ == "__main__" : 
523    retcode = 0
524    try :
525        defaults = { \
526                     "printer" : "*", \
527                     "comment" : "", \
528                   }
529        short_options = "vhdo:c:C:l:b:i:naugrp:P:S:H:G:RU:"
530        long_options = ["help", "version", "comment=", \
531                        "overcharge=", "charge=", "delete", "limitby=", \
532                        "balance=", "ingroups=", "noquota", "add", "users", \
533                        "groups", "reset", "hardreset", "prototype=", \
534                        "printer=", "softlimit=", "hardlimit=", "pgroups=", \
535                        "used="]
536       
537        # Initializes the command line tool
538        editor = EdPyKota(doc=__doc__)
539        editor.deferredInit()
540       
541        # parse and checks the command line
542        (options, args) = editor.parseCommandline(sys.argv[1:], short_options, long_options)
543       
544        # sets long options
545        options["help"] = options["h"] or options["help"]
546        options["version"] = options["v"] or options["version"]
547        options["add"] = options["a"] or options["add"]
548        options["users"] = options["u"] or options["users"]
549        options["groups"] = options["g"] or options["groups"]
550        options["prototype"] = options["p"] or options["prototype"]
551        options["printer"] = options["P"] or options["printer"] or defaults["printer"]
552        options["softlimit"] = options["S"] or options["softlimit"]
553        options["hardlimit"] = options["H"] or options["hardlimit"] 
554        options["reset"] = options["r"] or options["reset"] 
555        options["noquota"] = options["n"] or options["noquota"]
556        options["limitby"] = options["l"] or options["limitby"]
557        options["balance"] = options["b"] or options["balance"] 
558        options["delete"] = options["d"] or options["delete"] 
559        options["ingroups"] = options["i"] or options["ingroups"]
560        options["charge"] = options["c"] or options["charge"]
561        options["pgroups"] = options["G"] or options["pgroups"]
562        options["hardreset"] = options["R"] or options["hardreset"] 
563        options["used"] = options["U"] or options["used"]
564        options["overcharge"] = options["o"] or options["overcharge"]
565        options["comment"] = options["C"] or options["comment"] or defaults["comment"]
566       
567        if options["help"] :
568            editor.display_usage_and_quit()
569        elif options["version"] :
570            editor.display_version_and_quit()
571        elif options["users"] and options["groups"] :   
572            raise PyKotaToolError, _("incompatible options, see help.")
573        elif (options["add"] or options["prototype"]) and options["delete"] :   
574            raise PyKotaToolError, _("incompatible options, see help.")
575        elif (options["reset"] or options["hardreset"] or options["limitby"] or options["used"] or options["balance"] or options["overcharge"] or options["softlimit"] or options["hardlimit"]) and options["prototype"] :
576            raise PyKotaToolError, _("incompatible options, see help.")
577        elif options["noquota"] and (options["prototype"] or options["hardlimit"] or options["softlimit"]) :
578            raise PyKotaToolError, _("incompatible options, see help.")
579        elif options["groups"] and (options["balance"] or options["ingroups"] or options["used"] or options["overcharge"]) :
580            raise PyKotaToolError, _("incompatible options, see help.")
581        elif options["comment"] and not options["balance"] :   
582            raise PyKotaToolError, _("incompatible options, see help.")
583        else :
584            retcode = editor.main(args, options)
585    except KeyboardInterrupt :       
586        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
587    except SystemExit :       
588        pass
589    except :
590        try :
591            editor.crashed("edpykota failed")
592        except :   
593            crashed("edpykota failed")
594        retcode = -1
595
596    try :
597        editor.storage.close()
598    except (TypeError, NameError, AttributeError) :   
599        pass
600       
601    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.