root / pykota / trunk / bin / pkprinters @ 3549

Revision 3549, 17.3 kB (checked in by jerome, 14 years ago)

Removed support for the MaxJobSize? attribute for users group print quota
entries : I couldn't see a real use for this at the moment, and it would
complexify the code. This support might reappear later however. Added full
support for the MaxJobSize? attribute for user print quota entries,
editable with edpykota's new --maxjobsize command line switch. Changed
the internal handling of the MaxJobSize? attribute for printers :
internally 0 used to mean unlimited, it now allows one to forbid
printing onto a particular printer. The database upgrade script (only
for PostgreSQL) takes care of this.
IMPORTANT : the database schema changes. A database upgrade script is
provided for PostgreSQL only. The LDAP schema doesn't change to not
break any existing LDAP directory, so the pykotaMaxJobSize attribute is
still allowed on group print quota entries, but never used.
Seems to work as expected, for a change :-)
Fixes #15.

  • 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: utf-8 -*-
3#
4# PyKota : Print Quotas for CUPS
5#
6# (c) 2003-2009 Jerome Alet <alet@librelogiciel.com>
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# This program is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with this program.  If not, see <http://www.gnu.org/licenses/>.
19#
20# $Id$
21#
22#
23
24"""A printers manager for PyKota."""
25
26import os
27import sys
28import pwd
29
30import pykota.appinit
31from pykota.utils import run
32from pykota.commandline import PyKotaOptionParser
33from pykota.errors import PyKotaCommandLineError
34from pykota.tool import PyKotaTool
35from pykota.storage import StoragePrinter
36from pykota.progressbar import Percent
37
38from pkipplib import pkipplib
39
40class PKPrinters(PyKotaTool) :
41    """A class for a printers manager."""
42    def modifyPrinter(self, printer, charges, perpage, perjob, description, passthrough, nopassthrough, maxjobsize) :
43        if charges :
44            printer.setPrices(perpage, perjob)
45        if description is not None :        # NB : "" is allowed !
46            printer.setDescription(description)
47        if nopassthrough :
48            printer.setPassThrough(False)
49        if passthrough :
50            printer.setPassThrough(True)
51        if maxjobsize is not None :
52            if maxjobsize == "unlimited" :
53                printer.setMaxJobSize(None)
54            else :
55                printer.setMaxJobSize(maxjobsize)
56
57    def managePrintersGroups(self, pgroups, printer, remove) :
58        """Manage printer group membership."""
59        for pgroup in pgroups :
60            if remove :
61                pgroup.delPrinterFromGroup(printer)
62            else :
63                pgroup.addPrinterToGroup(printer)
64
65    def getPrinterDeviceURI(self, printername) :
66        """Returns the Device URI attribute for a particular printer."""
67        if not printername :
68            return ""
69        cups = pkipplib.CUPS()
70        req = cups.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES)
71        req.operation["printer-uri"] = ("uri", cups.identifierToURI("printers", printername))
72        req.operation["requested-attributes"] = ("keyword", "device-uri")
73        try :
74            return cups.doRequest(req).printer["device-uri"][0][1]
75        except (AttributeError, IndexError, KeyError) :
76            self.printInfo(_("Impossible to retrieve %(printername)s's DeviceURI") % locals(), "warn")
77            return ""
78
79    def isPrinterCaptured(self, printername=None, deviceuri=None) :
80        """Returns True if the printer is already redirected through PyKota's backend, else False."""
81        if (deviceuri or self.getPrinterDeviceURI(printername)).find("cupspykota:") != -1 :
82            return True
83        else :
84            return False
85
86    def reroutePrinterThroughPyKota(self, printer) :
87        """Reroutes a CUPS printer through PyKota."""
88        uri = self.getPrinterDeviceURI(printer.Name)
89        if uri and (not self.isPrinterCaptured(deviceuri=uri)) :
90             newuri = "cupspykota://%s" % uri
91             os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri))
92             self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri))
93
94    def deroutePrinterFromPyKota(self, printer) :
95        """Deroutes a PyKota printer through CUPS only."""
96        uri = self.getPrinterDeviceURI(printer.Name)
97        if uri and self.isPrinterCaptured(deviceuri=uri) :
98             newuri = uri.replace("cupspykota:", "")
99             if newuri.startswith("//") :
100                 newuri = newuri[2:]
101             os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri))
102             self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri))
103
104    def main(self, names, options) :
105        """Manage printers."""
106        islist = (options.action == "list")
107        isadd = (options.action == "add")
108        isdelete = (options.action == "delete")
109
110        if not islist :
111            self.adminOnly()
112
113        if not names :
114            if isdelete or isadd :
115                raise PyKotaCommandLineError, _("You must specify printers names on the command line.")
116            names = [u"*"]
117
118        if options.remove and not options.groups :
119            raise PyKotaCommandLineError, _("You must specify printers groups names on the command line.")
120        elif (((islist or isdelete) and (options.charge  \
121                                        or options.groups \
122                                        or options.remove \
123                                        or options.description \
124                                        or options.skipexisting \
125                                        or options.passthrough \
126                                        or options.nopassthrough \
127                                        or options.maxjobsize))) \
128              or (options.cups and islist) :
129            raise PyKotaCommandLineError, _("Incompatible command line options. Please look at the online help or manual page.")
130
131        if not islist :
132            percent = Percent(self)
133
134        if not isadd :
135            if not islist :
136                percent.display("%s..." % _("Extracting datas"))
137            printers = self.storage.getMatchingPrinters(",".join(names))
138            if not printers :
139                if not islist :
140                    percent.display("\n")
141                raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(names)
142            if not islist :
143                percent.setSize(len(printers))
144
145        if islist :
146            for printer in printers :
147                parents = ", ".join([p.Name for p in self.storage.getParentPrinters(printer)])
148                self.display("%s [%s] (%s + #*%s)\n" % \
149                                     (printer.Name,
150                                      printer.Description,
151                                      printer.PricePerJob,
152                                      printer.PricePerPage))
153                self.display("    %s\n" % \
154                                     (_("Passthrough mode : %s") % ((printer.PassThrough and _("ON")) or _("OFF"))))
155                self.display("    %s\n" % \
156                                     (_("Maximum job size : %s") % (((printer.MaxJobSize is not None) and (_("%s pages") % printer.MaxJobSize)) or _("Unlimited"))))
157                self.display("    %s\n" % (_("Routed through PyKota : %s") % ((self.isPrinterCaptured(printer.Name) and _("YES")) or _("NO"))))
158                if parents :
159                    self.display("    %s %s\n" % (_("in"), parents))
160                self.display("\n")
161        elif isdelete :
162            percent.display("\n%s..." % _("Deletion"))
163            self.storage.deleteManyPrinters(printers)
164            percent.display("\n")
165            if options.cups :
166                percent.display("%s...\n" % _("Rerouting printers to CUPS"))
167                for printer in printers :
168                    self.deroutePrinterFromPyKota(printer)
169                    percent.oneMore()
170        else :
171            if options.groups :
172                printersgroups = self.storage.getMatchingPrinters(options.groups)
173                if not printersgroups :
174                    raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(options.groups.split(','))
175            else :
176                printersgroups = []
177
178            if options.charge :
179                try :
180                    charges = [float(part) for part in options.charge.split(',', 1)]
181                except ValueError :
182                    raise PyKotaCommandLineError, _("Invalid charge amount value %s") % options.charge
183                else :
184                    if len(charges) > 2 :
185                        charges = charges[:2]
186                    if len(charges) != 2 :
187                        charges = [charges[0], None]
188                    (perpage, perjob) = charges
189            else :
190                charges = perpage = perjob = None
191
192            if options.maxjobsize :
193                if options.maxjobsize.lower() == "unlimited" :
194                    maxjobsize = "unlimited"
195                else :
196                    try :
197                        maxjobsize = int(options.maxjobsize)
198                        if maxjobsize < 0 :
199                            raise ValueError
200                    except ValueError :
201                        raise PyKotaCommandLineError, _("Invalid maximum job size value %s") % options.maxjobsize
202            else :
203                maxjobsize = None
204
205            description = options.description
206            if description :
207                description = description.strip()
208
209            self.storage.beginTransaction()
210            try :
211                if isadd :
212                    percent.display("%s...\n" % _("Creation"))
213                    percent.setSize(len(names))
214                    for pname in names :
215                        if self.isValidName(pname) :
216                            printer = StoragePrinter(self.storage, pname)
217                            self.modifyPrinter(printer,
218                                               charges,
219                                               perpage,
220                                               perjob,
221                                               description,
222                                               options.passthrough,
223                                               options.nopassthrough,
224                                               maxjobsize)
225                            oldprinter = self.storage.addPrinter(printer)
226
227                            if options.cups :
228                                 self.reroutePrinterThroughPyKota(printer)
229
230                            if oldprinter is not None :
231                                if options.skipexisting :
232                                    self.logdebug(_("Printer %s already exists, skipping.") % pname)
233                                else :
234                                    self.logdebug(_("Printer %s already exists, will be modified.") % pname)
235                                    self.modifyPrinter(oldprinter,
236                                                       charges,
237                                                       perpage,
238                                                       perjob,
239                                                       description,
240                                                       options.passthrough,
241                                                       options.nopassthrough,
242                                                       maxjobsize)
243                                    oldprinter.save()
244                                    self.managePrintersGroups(printersgroups,
245                                                              oldprinter,
246                                                              options.remove)
247                            else :
248                                self.managePrintersGroups(printersgroups, \
249                                                          self.storage.getPrinter(pname), \
250                                                          options.remove)
251                        else :
252                            raise PyKotaCommandLineError, _("Invalid printer name %s") % pname
253                        percent.oneMore()
254                else :
255                    percent.display("\n%s...\n" % _("Modification"))
256                    for printer in printers :
257                        self.modifyPrinter(printer,
258                                           charges,
259                                           perpage,
260                                           perjob,
261                                           description,
262                                           options.passthrough,
263                                           options.nopassthrough,
264                                           maxjobsize)
265                        printer.save()
266                        self.managePrintersGroups(printersgroups,
267                                                  printer,
268                                                  options.remove)
269                        if options.cups :
270                            self.reroutePrinterThroughPyKota(printer)
271                        percent.oneMore()
272            except :
273                self.storage.rollbackTransaction()
274                raise
275            else :
276                self.storage.commitTransaction()
277
278        if not islist :
279            percent.done()
280
281if __name__ == "__main__" :
282    parser = PyKotaOptionParser(description=_("Manages PyKota printers."),
283                                usage="pkprinters [options] printer1 printer2 ... printerN")
284    parser.add_option("-a", "--add",
285                            action="store_const",
286                            const="add",
287                            dest="action",
288                            help=_("Add new, or modify existing, printers."))
289    parser.add_option("-c", "--charge",
290                            dest="charge",
291                            help=_("Set the cost per page, and optionally per job, for printing to the specified printers. If both are to be set, separate them with a comma. Floating point and negative values are allowed."))
292    parser.add_option("-C", "--cups",
293                            action="store_true",
294                            dest="cups",
295                            help=_("Tell CUPS to either start or stop managing the specified printers with PyKota."))
296    parser.add_option("-d", "--delete",
297                            action="store_const",
298                            const="delete",
299                            dest="action",
300                            help=_("Delete the specified printers. Also purge the print quota entries and printing history matching the specified printers."))
301    parser.add_option("-D", "--description",
302                            dest="description",
303                            help=_("Set a textual description for the specified printers."))
304    parser.add_option("-g", "--groups",
305                            dest="groups",
306                            help=_("If the --remove option is not used, the default action is to add the specified printers to the specified printers groups. Otherwise they are removed from these groups. The specified printers groups must already exist, and should be created beforehand just like normal printers with this very command."))
307    parser.add_option("-l", "--list",
308                            action="store_const",
309                            const="list",
310                            dest="action",
311                            help=_("Display detailed informations about the specified printers."))
312    parser.add_option("-m", "--maxjobsize",
313                            dest="maxjobsize",
314                            help=_("Set the maximum job size in pages allowed on the specified printers. Accepted values are '0' to forbid printing, 'unlimited' to allow unrestricted printing, or any positive integer value."))
315    parser.add_option("-n", "--nopassthrough",
316                            action="store_true",
317                            dest="nopassthrough",
318                            help=_("Deactivate passthrough mode for the specified printers. This is the normal mode of operations, in which print jobs are accounted for, and are checked against printing quotas and available credits."))
319    parser.add_option("-p", "--passthrough",
320                            action="store_true",
321                            dest="passthrough",
322                            help=_("Activate passthrough mode for the specified printers. In this mode, jobs sent to these printers are not accounted for. This can be useful for exams during which no user should be charged for his printouts."))
323    parser.add_option("-r", "--remove",
324                            action="store_true",
325                            dest="remove",
326                            help=_("When combined with the --groups option, remove printers from the specified printers groups."))
327    parser.add_option("-s", "--skipexisting",
328                            action="store_true",
329                            dest="skipexisting",
330                            help=_("If --add is used, ensure that existing printers won't be modified."))
331
332    parser.add_example('--add --cups -D "HP Printer" --charge 0.05,0.1 hp2100 hp2200',
333                       _("Would create three printers named 'hp2100', and 'hp2200' in PyKota's database, while telling CUPS to route all print jobs through PyKota for these printers. Each of them would have 'HP Printer' as its description. Printing to any of them would cost 0.05 credit per page, plus 0.1 credit for each job."))
334    parser.add_example('--delete "*"',
335                       _("Would delete all existing printers and matching print quota entries and printing history from PyKota's database. USE WITH CARE."))
336    parser.add_example('--groups Laser,HP "hp*"',
337                       _("Would add all printers which name begins with 'hp' to the 'Laser' and 'HP' printers groups, which must already exist."))
338    parser.add_example("--groups Lexmark --remove hp2200",
339                       _("Would remove printer 'hp2200' from the 'Lexmark' printers group."))
340    run(parser, PKPrinters)
Note: See TracBrowser for help on using the browser.