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 | |
---|
26 | import os |
---|
27 | import sys |
---|
28 | import pwd |
---|
29 | |
---|
30 | import pykota.appinit |
---|
31 | from pykota.utils import run |
---|
32 | from pykota.commandline import PyKotaOptionParser |
---|
33 | from pykota.errors import PyKotaCommandLineError |
---|
34 | from pykota.tool import PyKotaTool |
---|
35 | from pykota.storage import StoragePrinter |
---|
36 | from pykota.progressbar import Percent |
---|
37 | |
---|
38 | from pkipplib import pkipplib |
---|
39 | |
---|
40 | class 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 | printer.setMaxJobSize(maxjobsize) |
---|
53 | |
---|
54 | def managePrintersGroups(self, pgroups, printer, remove) : |
---|
55 | """Manage printer group membership.""" |
---|
56 | for pgroup in pgroups : |
---|
57 | if remove : |
---|
58 | pgroup.delPrinterFromGroup(printer) |
---|
59 | else : |
---|
60 | pgroup.addPrinterToGroup(printer) |
---|
61 | |
---|
62 | def getPrinterDeviceURI(self, printername) : |
---|
63 | """Returns the Device URI attribute for a particular printer.""" |
---|
64 | if not printername : |
---|
65 | return "" |
---|
66 | cups = pkipplib.CUPS() |
---|
67 | req = cups.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES) |
---|
68 | req.operation["printer-uri"] = ("uri", cups.identifierToURI("printers", printername)) |
---|
69 | req.operation["requested-attributes"] = ("keyword", "device-uri") |
---|
70 | try : |
---|
71 | return cups.doRequest(req).printer["device-uri"][0][1] |
---|
72 | except (AttributeError, IndexError, KeyError) : |
---|
73 | self.printInfo(_("Impossible to retrieve %(printername)s's DeviceURI") % locals(), "warn") |
---|
74 | return "" |
---|
75 | |
---|
76 | def isPrinterCaptured(self, printername=None, deviceuri=None) : |
---|
77 | """Returns True if the printer is already redirected through PyKota's backend, else False.""" |
---|
78 | if (deviceuri or self.getPrinterDeviceURI(printername)).find("cupspykota:") != -1 : |
---|
79 | return True |
---|
80 | else : |
---|
81 | return False |
---|
82 | |
---|
83 | def reroutePrinterThroughPyKota(self, printer) : |
---|
84 | """Reroutes a CUPS printer through PyKota.""" |
---|
85 | uri = self.getPrinterDeviceURI(printer.Name) |
---|
86 | if uri and (not self.isPrinterCaptured(deviceuri=uri)) : |
---|
87 | newuri = "cupspykota://%s" % uri |
---|
88 | os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri)) |
---|
89 | self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri)) |
---|
90 | |
---|
91 | def deroutePrinterFromPyKota(self, printer) : |
---|
92 | """Deroutes a PyKota printer through CUPS only.""" |
---|
93 | uri = self.getPrinterDeviceURI(printer.Name) |
---|
94 | if uri and self.isPrinterCaptured(deviceuri=uri) : |
---|
95 | newuri = uri.replace("cupspykota:", "") |
---|
96 | if newuri.startswith("//") : |
---|
97 | newuri = newuri[2:] |
---|
98 | os.system('lpadmin -p "%s" -v "%s"' % (printer.Name, newuri)) |
---|
99 | self.logdebug("Printer %s rerouted to %s" % (printer.Name, newuri)) |
---|
100 | |
---|
101 | def main(self, names, options) : |
---|
102 | """Manage printers.""" |
---|
103 | islist = (options.action == "list") |
---|
104 | isadd = (options.action == "add") |
---|
105 | isdelete = (options.action == "delete") |
---|
106 | |
---|
107 | if not islist : |
---|
108 | self.adminOnly() |
---|
109 | |
---|
110 | if not names : |
---|
111 | if isdelete or isadd : |
---|
112 | raise PyKotaCommandLineError, _("You must specify printers names on the command line.") |
---|
113 | names = [u"*"] |
---|
114 | |
---|
115 | if options.remove and not options.groups : |
---|
116 | raise PyKotaCommandLineError, _("You must specify printers groups names on the command line.") |
---|
117 | elif (((islist or isdelete) and (options.charge \ |
---|
118 | or options.groups \ |
---|
119 | or options.remove \ |
---|
120 | or options.description \ |
---|
121 | or options.skipexisting \ |
---|
122 | or options.passthrough \ |
---|
123 | or options.nopassthrough \ |
---|
124 | or options.maxjobsize))) \ |
---|
125 | or (options.cups and islist) : |
---|
126 | raise PyKotaCommandLineError, _("Incompatible command line options. Please look at the online help or manual page.") |
---|
127 | |
---|
128 | if not islist : |
---|
129 | percent = Percent(self) |
---|
130 | |
---|
131 | if not isadd : |
---|
132 | if not islist : |
---|
133 | percent.display("%s..." % _("Extracting datas")) |
---|
134 | printers = self.storage.getMatchingPrinters(",".join(names)) |
---|
135 | if not printers : |
---|
136 | if not islist : |
---|
137 | percent.display("\n") |
---|
138 | raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(names) |
---|
139 | if not islist : |
---|
140 | percent.setSize(len(printers)) |
---|
141 | |
---|
142 | if islist : |
---|
143 | for printer in printers : |
---|
144 | parents = ", ".join([p.Name for p in self.storage.getParentPrinters(printer)]) |
---|
145 | self.display("%s [%s] (%s + #*%s)\n" % \ |
---|
146 | (printer.Name, |
---|
147 | printer.Description, |
---|
148 | printer.PricePerJob, |
---|
149 | printer.PricePerPage)) |
---|
150 | self.display(" %s\n" % \ |
---|
151 | (_("Passthrough mode : %s") % ((printer.PassThrough and _("ON")) or _("OFF")))) |
---|
152 | self.display(" %s\n" % \ |
---|
153 | (_("Maximum job size : %s") % ((printer.MaxJobSize and (_("%s pages") % printer.MaxJobSize)) or _("Unlimited")))) |
---|
154 | self.display(" %s\n" % (_("Routed through PyKota : %s") % ((self.isPrinterCaptured(printer.Name) and _("YES")) or _("NO")))) |
---|
155 | if parents : |
---|
156 | self.display(" %s %s\n" % (_("in"), parents)) |
---|
157 | self.display("\n") |
---|
158 | elif isdelete : |
---|
159 | percent.display("\n%s..." % _("Deletion")) |
---|
160 | self.storage.deleteManyPrinters(printers) |
---|
161 | percent.display("\n") |
---|
162 | if options.cups : |
---|
163 | percent.display("%s...\n" % _("Rerouting printers to CUPS")) |
---|
164 | for printer in printers : |
---|
165 | self.deroutePrinterFromPyKota(printer) |
---|
166 | percent.oneMore() |
---|
167 | else : |
---|
168 | if options.groups : |
---|
169 | printersgroups = self.storage.getMatchingPrinters(options.groups) |
---|
170 | if not printersgroups : |
---|
171 | raise PyKotaCommandLineError, _("There's no printer matching %s") % " ".join(options.groups.split(',')) |
---|
172 | else : |
---|
173 | printersgroups = [] |
---|
174 | |
---|
175 | if options.charge : |
---|
176 | try : |
---|
177 | charges = [float(part) for part in options.charge.split(',', 1)] |
---|
178 | except ValueError : |
---|
179 | raise PyKotaCommandLineError, _("Invalid charge amount value %s") % options.charge |
---|
180 | else : |
---|
181 | if len(charges) > 2 : |
---|
182 | charges = charges[:2] |
---|
183 | if len(charges) != 2 : |
---|
184 | charges = [charges[0], None] |
---|
185 | (perpage, perjob) = charges |
---|
186 | else : |
---|
187 | charges = perpage = perjob = None |
---|
188 | |
---|
189 | if options.maxjobsize : |
---|
190 | try : |
---|
191 | maxjobsize = int(options.maxjobsize) |
---|
192 | if maxjobsize < 0 : |
---|
193 | raise ValueError |
---|
194 | except ValueError : |
---|
195 | raise PyKotaCommandLineError, _("Invalid maximum job size value %s") % options.maxjobsize |
---|
196 | else : |
---|
197 | maxjobsize = None |
---|
198 | |
---|
199 | description = options.description |
---|
200 | if description : |
---|
201 | description = description.strip() |
---|
202 | |
---|
203 | self.storage.beginTransaction() |
---|
204 | try : |
---|
205 | if isadd : |
---|
206 | percent.display("%s...\n" % _("Creation")) |
---|
207 | percent.setSize(len(names)) |
---|
208 | for pname in names : |
---|
209 | if self.isValidName(pname) : |
---|
210 | printer = StoragePrinter(self.storage, pname) |
---|
211 | self.modifyPrinter(printer, |
---|
212 | charges, |
---|
213 | perpage, |
---|
214 | perjob, |
---|
215 | description, |
---|
216 | options.passthrough, |
---|
217 | options.nopassthrough, |
---|
218 | maxjobsize) |
---|
219 | oldprinter = self.storage.addPrinter(printer) |
---|
220 | |
---|
221 | if options.cups : |
---|
222 | self.reroutePrinterThroughPyKota(printer) |
---|
223 | |
---|
224 | if oldprinter is not None : |
---|
225 | if options.skipexisting : |
---|
226 | self.logdebug(_("Printer %s already exists, skipping.") % pname) |
---|
227 | else : |
---|
228 | self.logdebug(_("Printer %s already exists, will be modified.") % pname) |
---|
229 | self.modifyPrinter(oldprinter, |
---|
230 | charges, |
---|
231 | perpage, |
---|
232 | perjob, |
---|
233 | description, |
---|
234 | options.passthrough, |
---|
235 | options.nopassthrough, |
---|
236 | maxjobsize) |
---|
237 | oldprinter.save() |
---|
238 | self.managePrintersGroups(printersgroups, |
---|
239 | oldprinter, |
---|
240 | options.remove) |
---|
241 | else : |
---|
242 | self.managePrintersGroups(printersgroups, \ |
---|
243 | self.storage.getPrinter(pname), \ |
---|
244 | options.remove) |
---|
245 | else : |
---|
246 | raise PyKotaCommandLineError, _("Invalid printer name %s") % pname |
---|
247 | percent.oneMore() |
---|
248 | else : |
---|
249 | percent.display("\n%s...\n" % _("Modification")) |
---|
250 | for printer in printers : |
---|
251 | self.modifyPrinter(printer, |
---|
252 | charges, |
---|
253 | perpage, |
---|
254 | perjob, |
---|
255 | description, |
---|
256 | options.passthrough, |
---|
257 | options.nopassthrough, |
---|
258 | maxjobsize) |
---|
259 | printer.save() |
---|
260 | self.managePrintersGroups(printersgroups, |
---|
261 | printer, |
---|
262 | options.remove) |
---|
263 | if options.cups : |
---|
264 | self.reroutePrinterThroughPyKota(printer) |
---|
265 | percent.oneMore() |
---|
266 | except : |
---|
267 | self.storage.rollbackTransaction() |
---|
268 | raise |
---|
269 | else : |
---|
270 | self.storage.commitTransaction() |
---|
271 | |
---|
272 | if not islist : |
---|
273 | percent.done() |
---|
274 | |
---|
275 | if __name__ == "__main__" : |
---|
276 | parser = PyKotaOptionParser(description=_("Manages PyKota printers."), |
---|
277 | usage="pkprinters [options] printer1 printer2 ... printerN") |
---|
278 | parser.add_option("-a", "--add", |
---|
279 | action="store_const", |
---|
280 | const="add", |
---|
281 | dest="action", |
---|
282 | help=_("Add new, or modify existing, printers.")) |
---|
283 | parser.add_option("-c", "--charge", |
---|
284 | dest="charge", |
---|
285 | 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.")) |
---|
286 | parser.add_option("-C", "--cups", |
---|
287 | action="store_true", |
---|
288 | dest="cups", |
---|
289 | help=_("Tell CUPS to either start or stop managing the specified printers with PyKota.")) |
---|
290 | parser.add_option("-d", "--delete", |
---|
291 | action="store_const", |
---|
292 | const="delete", |
---|
293 | dest="action", |
---|
294 | help=_("Delete the specified printers. Also purge the print quota entries and printing history matching the specified printers.")) |
---|
295 | parser.add_option("-D", "--description", |
---|
296 | dest="description", |
---|
297 | help=_("Set a textual description for the specified printers.")) |
---|
298 | parser.add_option("-g", "--groups", |
---|
299 | dest="groups", |
---|
300 | 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.")) |
---|
301 | parser.add_option("-l", "--list", |
---|
302 | action="store_const", |
---|
303 | const="list", |
---|
304 | dest="action", |
---|
305 | help=_("Display detailed informations about the specified printers.")) |
---|
306 | parser.add_option("-m", "--maxjobsize", |
---|
307 | dest="maxjobsize", |
---|
308 | help=_("Set the maximum job size in pages allowed on the specified printers.")) |
---|
309 | parser.add_option("-n", "--nopassthrough", |
---|
310 | action="store_true", |
---|
311 | dest="nopassthrough", |
---|
312 | 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.")) |
---|
313 | parser.add_option("-p", "--passthrough", |
---|
314 | action="store_true", |
---|
315 | dest="passthrough", |
---|
316 | 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.")) |
---|
317 | parser.add_option("-r", "--remove", |
---|
318 | action="store_true", |
---|
319 | dest="remove", |
---|
320 | help=_("When combined with the --groups option, remove printers from the specified printers groups.")) |
---|
321 | parser.add_option("-s", "--skipexisting", |
---|
322 | action="store_true", |
---|
323 | dest="skipexisting", |
---|
324 | help=_("If --add is used, ensure that existing printers won't be modified.")) |
---|
325 | |
---|
326 | parser.add_example('--add --cups -D "HP Printer" --charge 0.05,0.1 hp2100 hp2200', |
---|
327 | _("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.")) |
---|
328 | parser.add_example('--delete "*"', |
---|
329 | _("Would delete all existing printers and matching print quota entries and printing history from PyKota's database. USE WITH CARE.")) |
---|
330 | parser.add_example('--groups Laser,HP "hp*"', |
---|
331 | _("Would add all printers which name begins with 'hp' to the 'Laser' and 'HP' printers groups, which must already exist.")) |
---|
332 | parser.add_example("--groups Lexmark --remove hp2200", |
---|
333 | _("Would remove printer 'hp2200' from the 'Lexmark' printers group.")) |
---|
334 | run(parser, PKPrinters) |
---|