1 | #! /usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*-*- |
---|
3 | # |
---|
4 | # PyKota : Print Quotas for CUPS |
---|
5 | # |
---|
6 | # (c) 2003, 2004, 2005, 2006, 2007, 2008 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 tool that can be used to fill PyKota's database from system accounts, and detect the best accouting settings for the existing printers.""" |
---|
25 | |
---|
26 | import sys |
---|
27 | import os |
---|
28 | import pwd |
---|
29 | import grp |
---|
30 | import socket |
---|
31 | import signal |
---|
32 | |
---|
33 | from pkipplib import pkipplib |
---|
34 | |
---|
35 | import pykota.appinit |
---|
36 | from pykota.utils import run |
---|
37 | from pykota.commandline import PyKotaOptionParser |
---|
38 | from pykota.errors import PyKotaToolError, PyKotaCommandLineError |
---|
39 | from pykota.tool import Tool |
---|
40 | |
---|
41 | class PKTurnKey(Tool) : |
---|
42 | """A class for an initialization tool.""" |
---|
43 | def listPrinters(self, namestomatch) : |
---|
44 | """Returns a list of tuples (queuename, deviceuri) for all existing print queues.""" |
---|
45 | self.printInfo("Extracting all print queues.") |
---|
46 | printers = [] |
---|
47 | server = pkipplib.CUPS() |
---|
48 | for queuename in server.getPrinters() : |
---|
49 | req = server.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES) |
---|
50 | req.operation["printer-uri"] = ("uri", server.identifierToURI("printers", queuename)) |
---|
51 | req.operation["requested-attributes"] = ("keyword", "device-uri") |
---|
52 | result = server.doRequest(req) |
---|
53 | try : |
---|
54 | deviceuri = result.printer["device-uri"][0][1] |
---|
55 | except (AttributeError, IndexError, KeyError) : |
---|
56 | deviceuri = None |
---|
57 | if deviceuri is not None : |
---|
58 | if self.matchString(queuename, namestomatch) : |
---|
59 | printers.append((queuename, deviceuri)) |
---|
60 | else : |
---|
61 | self.printInfo("Print queue %s skipped." % queuename) |
---|
62 | return printers |
---|
63 | |
---|
64 | def listUsers(self, uidmin, uidmax) : |
---|
65 | """Returns a list of users whose uids are between uidmin and uidmax.""" |
---|
66 | self.printInfo("Extracting all users whose uid is between %s and %s." % (uidmin, uidmax)) |
---|
67 | return [(entry.pw_name, entry.pw_gid) for entry in pwd.getpwall() if uidmin <= entry.pw_uid <= uidmax] |
---|
68 | |
---|
69 | def listGroups(self, gidmin, gidmax, users) : |
---|
70 | """Returns a list of groups whose gids are between gidmin and gidmax.""" |
---|
71 | self.printInfo("Extracting all groups whose gid is between %s and %s." % (gidmin, gidmax)) |
---|
72 | groups = [(entry.gr_name, entry.gr_gid, entry.gr_mem) for entry in grp.getgrall() if gidmin <= entry.gr_gid <= gidmax] |
---|
73 | gidusers = {} |
---|
74 | usersgid = {} |
---|
75 | for u in users : |
---|
76 | gidusers.setdefault(u[1], []).append(u[0]) |
---|
77 | usersgid.setdefault(u[0], []).append(u[1]) |
---|
78 | |
---|
79 | membership = {} |
---|
80 | for g in range(len(groups)) : |
---|
81 | (gname, gid, members) = groups[g] |
---|
82 | newmembers = {} |
---|
83 | for m in members : |
---|
84 | newmembers[m] = m |
---|
85 | try : |
---|
86 | usernames = gidusers[gid] |
---|
87 | except KeyError : |
---|
88 | pass |
---|
89 | else : |
---|
90 | for username in usernames : |
---|
91 | if not newmembers.has_key(username) : |
---|
92 | newmembers[username] = username |
---|
93 | for member in newmembers.keys() : |
---|
94 | if not usersgid.has_key(member) : |
---|
95 | del newmembers[member] |
---|
96 | membership[gname] = newmembers.keys() |
---|
97 | return membership |
---|
98 | |
---|
99 | def runCommand(self, command, dryrun) : |
---|
100 | """Launches an external command.""" |
---|
101 | self.printInfo("%s" % command) |
---|
102 | if not dryrun : |
---|
103 | os.system(command) |
---|
104 | |
---|
105 | def createPrinters(self, printers, dryrun=0) : |
---|
106 | """Creates all printers in PyKota's database.""" |
---|
107 | if printers : |
---|
108 | args = open("/tmp/pkprinters.args", "w") |
---|
109 | args.write('--add\n--cups\n--skipexisting\n--description\n"printer created from pkturnkey"\n') |
---|
110 | args.write("%s\n" % "\n".join(['"%s"' % p[0] for p in printers])) |
---|
111 | args.close() |
---|
112 | self.runCommand("pkprinters --arguments /tmp/pkprinters.args", dryrun) |
---|
113 | |
---|
114 | def createUsers(self, users, printers, dryrun=0) : |
---|
115 | """Creates all users in PyKota's database.""" |
---|
116 | if users : |
---|
117 | args = open("/tmp/pkusers.users.args", "w") |
---|
118 | args.write('--add\n--skipexisting\n--description\n"user created from pkturnkey"\n--limitby\nnoquota\n') |
---|
119 | args.write("%s\n" % "\n".join(['"%s"' % u for u in users])) |
---|
120 | args.close() |
---|
121 | self.runCommand("pkusers --arguments /tmp/pkusers.users.args", dryrun) |
---|
122 | |
---|
123 | printersnames = [p[0] for p in printers] |
---|
124 | args = open("/tmp/edpykota.users.args", "w") |
---|
125 | args.write('--add\n--skipexisting\n--noquota\n--printer\n') |
---|
126 | args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames])) |
---|
127 | args.write("%s\n" % "\n".join(['"%s"' % u for u in users])) |
---|
128 | args.close() |
---|
129 | self.runCommand("edpykota --arguments /tmp/edpykota.users.args", dryrun) |
---|
130 | |
---|
131 | def createGroups(self, groups, printers, dryrun=0) : |
---|
132 | """Creates all groups in PyKota's database.""" |
---|
133 | if groups : |
---|
134 | args = open("/tmp/pkusers.groups.args", "w") |
---|
135 | args.write('--groups\n--add\n--skipexisting\n--description\n"group created from pkturnkey"\n--limitby\nnoquota\n') |
---|
136 | args.write("%s\n" % "\n".join(['"%s"' % g for g in groups])) |
---|
137 | args.close() |
---|
138 | self.runCommand("pkusers --arguments /tmp/pkusers.groups.args", dryrun) |
---|
139 | |
---|
140 | printersnames = [p[0] for p in printers] |
---|
141 | args = open("/tmp/edpykota.groups.args", "w") |
---|
142 | args.write('--groups\n--add\n--skipexisting\n--noquota\n--printer\n') |
---|
143 | args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames])) |
---|
144 | args.write("%s\n" % "\n".join(['"%s"' % g for g in groups])) |
---|
145 | args.close() |
---|
146 | self.runCommand("edpykota --arguments /tmp/edpykota.groups.args", dryrun) |
---|
147 | |
---|
148 | revmembership = {} |
---|
149 | for (groupname, usernames) in groups.items() : |
---|
150 | for username in usernames : |
---|
151 | revmembership.setdefault(username, []).append(groupname) |
---|
152 | commands = [] |
---|
153 | for (username, groupnames) in revmembership.items() : |
---|
154 | commands.append('pkusers --ingroups %s "%s"' \ |
---|
155 | % (",".join(['"%s"' % g for g in groupnames]), username)) |
---|
156 | for command in commands : |
---|
157 | self.runCommand(command, dryrun) |
---|
158 | |
---|
159 | def supportsSNMP(self, hostname, community) : |
---|
160 | """Returns 1 if the printer accepts SNMP queries, else 0.""" |
---|
161 | pageCounterOID = "1.3.6.1.2.1.43.10.2.1.4.1.1" # SNMPv2-SMI::mib-2.43.10.2.1.4.1.1 |
---|
162 | try : |
---|
163 | from pysnmp.entity.rfc3413.oneliner import cmdgen |
---|
164 | except ImportError : |
---|
165 | hasV4 = False |
---|
166 | try : |
---|
167 | from pysnmp.asn1.encoding.ber.error import TypeMismatchError |
---|
168 | from pysnmp.mapping.udp.role import Manager |
---|
169 | from pysnmp.proto.api import alpha |
---|
170 | except ImportError : |
---|
171 | logerr("pysnmp doesn't seem to be installed. SNMP checks will be ignored !\n") |
---|
172 | return False |
---|
173 | else : |
---|
174 | hasV4 = True |
---|
175 | |
---|
176 | if hasV4 : |
---|
177 | def retrieveSNMPValues(hostname, community) : |
---|
178 | """Retrieves a printer's internal page counter and status via SNMP.""" |
---|
179 | errorIndication, errorStatus, errorIndex, varBinds = \ |
---|
180 | cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", community, 0), \ |
---|
181 | cmdgen.UdpTransportTarget((hostname, 161)), \ |
---|
182 | tuple([int(i) for i in pageCounterOID.split('.')])) |
---|
183 | if errorIndication : |
---|
184 | raise "No SNMP !" |
---|
185 | elif errorStatus : |
---|
186 | raise "No SNMP !" |
---|
187 | else : |
---|
188 | self.SNMPOK = True |
---|
189 | else : |
---|
190 | def retrieveSNMPValues(hostname, community) : |
---|
191 | """Retrieves a printer's internal page counter and status via SNMP.""" |
---|
192 | ver = alpha.protoVersions[alpha.protoVersionId1] |
---|
193 | req = ver.Message() |
---|
194 | req.apiAlphaSetCommunity(community) |
---|
195 | req.apiAlphaSetPdu(ver.GetRequestPdu()) |
---|
196 | req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null())) |
---|
197 | tsp = Manager() |
---|
198 | try : |
---|
199 | tsp.sendAndReceive(req.berEncode(), \ |
---|
200 | (hostname, 161), \ |
---|
201 | (handleAnswer, req)) |
---|
202 | except : |
---|
203 | raise "No SNMP !" |
---|
204 | tsp.close() |
---|
205 | |
---|
206 | def handleAnswer(wholemsg, notusedhere, req): |
---|
207 | """Decodes and handles the SNMP answer.""" |
---|
208 | ver = alpha.protoVersions[alpha.protoVersionId1] |
---|
209 | rsp = ver.Message() |
---|
210 | try : |
---|
211 | rsp.berDecode(wholemsg) |
---|
212 | except TypeMismatchError, msg : |
---|
213 | raise "No SNMP !" |
---|
214 | else : |
---|
215 | if req.apiAlphaMatch(rsp): |
---|
216 | errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus() |
---|
217 | if errorStatus: |
---|
218 | raise "No SNMP !" |
---|
219 | else: |
---|
220 | self.values = [] |
---|
221 | for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList(): |
---|
222 | self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value) |
---|
223 | try : |
---|
224 | pagecounter = self.values[0] |
---|
225 | except : |
---|
226 | raise "No SNMP !" |
---|
227 | else : |
---|
228 | self.SNMPOK = True |
---|
229 | return True |
---|
230 | |
---|
231 | self.SNMPOK = False |
---|
232 | try : |
---|
233 | retrieveSNMPValues(hostname, community) |
---|
234 | except : |
---|
235 | self.SNMPOK = False |
---|
236 | return self.SNMPOK |
---|
237 | |
---|
238 | def supportsPJL(self, hostname, port) : |
---|
239 | """Returns 1 if the printer accepts PJL queries over TCP, else 0.""" |
---|
240 | def alarmHandler(signum, frame) : |
---|
241 | raise "Timeout !" |
---|
242 | |
---|
243 | pjlsupport = False |
---|
244 | signal.signal(signal.SIGALRM, alarmHandler) |
---|
245 | signal.alarm(2) # wait at most 2 seconds |
---|
246 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
---|
247 | try : |
---|
248 | s.connect((hostname, port)) |
---|
249 | s.send("\033%-12345X@PJL INFO STATUS\r\n\033%-12345X") |
---|
250 | answer = s.recv(1024) |
---|
251 | if not answer.startswith("@PJL") : |
---|
252 | raise "No PJL !" |
---|
253 | except : |
---|
254 | pass |
---|
255 | else : |
---|
256 | pjlsupport = True |
---|
257 | s.close() |
---|
258 | signal.alarm(0) |
---|
259 | signal.signal(signal.SIGALRM, signal.SIG_IGN) |
---|
260 | return pjlsupport |
---|
261 | |
---|
262 | def hintConfig(self, printers) : |
---|
263 | """Gives some hints about what to put into pykota.conf""" |
---|
264 | if not printers : |
---|
265 | return |
---|
266 | sys.stderr.flush() # ensure outputs don't mix |
---|
267 | self.display("\n--- CUT ---\n") |
---|
268 | self.display("# Here are some lines that we suggest you add at the end\n") |
---|
269 | self.display("# of the pykota.conf file. These lines gives possible\n") |
---|
270 | self.display("# values for the way print jobs' size will be computed.\n") |
---|
271 | self.display("# NB : it is possible that a manual configuration gives\n") |
---|
272 | self.display("# better results for you. As always, your mileage may vary.\n") |
---|
273 | self.display("#\n") |
---|
274 | for (name, uri) in printers : |
---|
275 | self.display("[%s]\n" % name) |
---|
276 | accounter = "software()" |
---|
277 | try : |
---|
278 | uri = uri.split("cupspykota:", 2)[-1] |
---|
279 | except (ValueError, IndexError) : |
---|
280 | pass |
---|
281 | else : |
---|
282 | while uri and uri.startswith("/") : |
---|
283 | uri = uri[1:] |
---|
284 | try : |
---|
285 | (backend, destination) = uri.split(":", 1) |
---|
286 | if backend not in ("ipp", "http", "https", "lpd", "socket") : |
---|
287 | raise ValueError |
---|
288 | except ValueError : |
---|
289 | pass |
---|
290 | else : |
---|
291 | while destination.startswith("/") : |
---|
292 | destination = destination[1:] |
---|
293 | checkauth = destination.split("@", 1) |
---|
294 | if len(checkauth) == 2 : |
---|
295 | destination = checkauth[1] |
---|
296 | parts = destination.split("/")[0].split(":") |
---|
297 | if len(parts) == 2 : |
---|
298 | (hostname, port) = parts |
---|
299 | try : |
---|
300 | port = int(port) |
---|
301 | except ValueError : |
---|
302 | port = 9100 |
---|
303 | else : |
---|
304 | (hostname, port) = parts[0], 9100 |
---|
305 | |
---|
306 | if self.supportsSNMP(hostname, "public") : |
---|
307 | accounter = "hardware(snmp)" |
---|
308 | elif self.supportsPJL(hostname, 9100) : |
---|
309 | accounter = "hardware(pjl)" |
---|
310 | elif self.supportsPJL(hostname, 9101) : |
---|
311 | accounter = "hardware(pjl:9101)" |
---|
312 | elif self.supportsPJL(hostname, port) : |
---|
313 | accounter = "hardware(pjl:%s)" % port |
---|
314 | |
---|
315 | self.display("preaccounter : software()\n") |
---|
316 | self.display("accounter : %s\n" % accounter) |
---|
317 | self.display("\n") |
---|
318 | self.display("--- CUT ---\n") |
---|
319 | |
---|
320 | def main(self, names, options) : |
---|
321 | """Intializes PyKota's database.""" |
---|
322 | self.adminOnly() |
---|
323 | |
---|
324 | if options.uidmin or options.uidmax : |
---|
325 | if not options.dousers : |
---|
326 | self.printInfo(_("The --uidmin or --uidmax command line option implies --dousers as well."), "warn") |
---|
327 | options.dousers = True |
---|
328 | |
---|
329 | if options.gidmin or options.gidmax : |
---|
330 | if not options.dogroups : |
---|
331 | self.printInfo(_("The --gidmin or --gidmax command line option implies --dogroups as well."), "warn") |
---|
332 | options.dogroups = True |
---|
333 | |
---|
334 | if options.dogroups : |
---|
335 | if not options.dousers : |
---|
336 | self.printInfo(_("The --dogroups command line option implies --dousers as well."), "warn") |
---|
337 | options.dousers = True |
---|
338 | |
---|
339 | if not names : |
---|
340 | names = [u"*"] |
---|
341 | |
---|
342 | self.printInfo(_("Please be patient...")) |
---|
343 | dryrun = not options.force |
---|
344 | if dryrun : |
---|
345 | self.printInfo(_("Don't worry, the database WILL NOT BE MODIFIED.")) |
---|
346 | else : |
---|
347 | self.printInfo(_("Please WORRY NOW, the database WILL BE MODIFIED.")) |
---|
348 | |
---|
349 | if options.dousers : |
---|
350 | if not options.uidmin : |
---|
351 | self.printInfo(_("System users will have a print account as well !"), "warn") |
---|
352 | uidmin = 0 |
---|
353 | else : |
---|
354 | try : |
---|
355 | uidmin = int(options.uidmin) |
---|
356 | except : |
---|
357 | try : |
---|
358 | uidmin = pwd.getpwnam(options.uidmin).pw_uid |
---|
359 | except KeyError, msg : |
---|
360 | raise PyKotaCommandLineError, _("Unknown username %s : %s") \ |
---|
361 | % (options.uidmin, msg) |
---|
362 | |
---|
363 | if not options.uidmax : |
---|
364 | uidmax = sys.maxint |
---|
365 | else : |
---|
366 | try : |
---|
367 | uidmax = int(options.uidmax) |
---|
368 | except : |
---|
369 | try : |
---|
370 | uidmax = pwd.getpwnam(options.uidmax).pw_uid |
---|
371 | except KeyError, msg : |
---|
372 | raise PyKotaCommandLineError, _("Unknown username %s : %s") \ |
---|
373 | % (options.uidmax, msg) |
---|
374 | |
---|
375 | if uidmin > uidmax : |
---|
376 | (uidmin, uidmax) = (uidmax, uidmin) |
---|
377 | users = self.listUsers(uidmin, uidmax) |
---|
378 | else : |
---|
379 | users = [] |
---|
380 | |
---|
381 | if options.dogroups : |
---|
382 | if not options.gidmin : |
---|
383 | self.printInfo(_("System groups will have a print account as well !"), "warn") |
---|
384 | gidmin = 0 |
---|
385 | else : |
---|
386 | try : |
---|
387 | gidmin = int(options.gidmin) |
---|
388 | except : |
---|
389 | try : |
---|
390 | gidmin = grp.getgrnam(options.gidmin).gr_gid |
---|
391 | except KeyError, msg : |
---|
392 | raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \ |
---|
393 | % (options.gidmin, msg) |
---|
394 | |
---|
395 | if not options.gidmax : |
---|
396 | gidmax = sys.maxint |
---|
397 | else : |
---|
398 | try : |
---|
399 | gidmax = int(options.gidmax) |
---|
400 | except : |
---|
401 | try : |
---|
402 | gidmax = grp.getgrnam(options.gidmax).gr_gid |
---|
403 | except KeyError, msg : |
---|
404 | raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \ |
---|
405 | % (options.gidmax, msg) |
---|
406 | |
---|
407 | if gidmin > gidmax : |
---|
408 | (gidmin, gidmax) = (gidmax, gidmin) |
---|
409 | groups = self.listGroups(gidmin, gidmax, users) |
---|
410 | if not options.emptygroups : |
---|
411 | for (groupname, members) in groups.items() : |
---|
412 | if not members : |
---|
413 | del groups[groupname] |
---|
414 | else : |
---|
415 | groups = [] |
---|
416 | |
---|
417 | printers = self.listPrinters(names) |
---|
418 | if printers : |
---|
419 | self.createPrinters(printers, dryrun) |
---|
420 | self.createUsers([entry[0] for entry in users], printers, dryrun) |
---|
421 | self.createGroups(groups, printers, dryrun) |
---|
422 | |
---|
423 | if dryrun : |
---|
424 | self.printInfo(_("Simulation terminated.")) |
---|
425 | else : |
---|
426 | self.printInfo(_("Database initialized !")) |
---|
427 | |
---|
428 | if options.doconf : |
---|
429 | self.hintConfig(printers) |
---|
430 | |
---|
431 | |
---|
432 | if __name__ == "__main__" : |
---|
433 | parser = PyKotaOptionParser(description=_("A turn key tool for PyKota. When launched, this command will initialize PyKota's database with all existing print queues and some or all users. For now, no prices or limits are set, so printing is fully accounted for, but not limited. That's why you'll probably want to also use edpykota once the database has been initialized."), |
---|
434 | usage="pkturnkey [options] printer1 printer2 ... printerN") |
---|
435 | parser.add_option("-c", "--doconf", |
---|
436 | action="store_true", |
---|
437 | dest="doconf", |
---|
438 | help=_("Try to autodetect the best print accounting settings for existing CUPS printers. All printers must be switched ON beforehand.")) |
---|
439 | parser.add_option("-d", "--dousers", |
---|
440 | action="store_true", |
---|
441 | dest="dousers", |
---|
442 | help=_("Create accounts for users, and allocate print quota entries for them.")) |
---|
443 | parser.add_option("-D", "--dogroups", |
---|
444 | action="store_true", |
---|
445 | dest="dogroups", |
---|
446 | help=_("Create accounts for users groups, and allocate print quota entries for them.")) |
---|
447 | parser.add_option("-e", "--emptygroups", |
---|
448 | action="store_true", |
---|
449 | dest="emptygroups", |
---|
450 | help=_("Also include groups which don't have any member.")) |
---|
451 | parser.add_option("-f", "--force", |
---|
452 | action="store_true", |
---|
453 | dest="force", |
---|
454 | help=_("Modifies PyKota's database content for real, instead of faking it (for safety reasons).")) |
---|
455 | parser.add_option("-u", "--uidmin", |
---|
456 | dest="uidmin", |
---|
457 | help=_("Only include users whose uid is greater than or equal to this parameter. If you pass an username instead, his uid will be used automatically.")) |
---|
458 | parser.add_option("-U", "--uidmax", |
---|
459 | dest="uidmax", |
---|
460 | help=_("Only include users whose uid is lesser than or equal to this parameter. If you pass an username instead, his uid will be used automatically.")) |
---|
461 | parser.add_option("-g", "--gidmin", |
---|
462 | dest="gidmin", |
---|
463 | help=_("Only include users groups whose gid is greater than or equal to this parameter. If you pass a groupname instead, its gid will be used automatically.")) |
---|
464 | parser.add_option("-G", "--gidmax", |
---|
465 | dest="gidmax", |
---|
466 | help=_("Only include users groups whose gid is lesser than or equal to this parameter. If you pass a groupname instead, its gid will be used automatically.")) |
---|
467 | |
---|
468 | parser.add_example("--dousers --uidmin jerome HPLASER1 HPLASER2", |
---|
469 | _("Would simulate the creation in PyKota's database of the printing accounts for all users whose uid is greater than or equal to 'jerome''s. Each of them would be given a print quota entry on printers 'HPLASER1' and 'HPLASER2'.")) |
---|
470 | parser.add_example("--force --dousers --uidmin jerome HPLASER1 HPLASER2", |
---|
471 | _("Would do the same as the example above, but for real. Please take great care when using the --force command line option.")) |
---|
472 | parser.add_example("--doconf", |
---|
473 | _("Would try to automatically detect the best print accounting settings for all active printers, and generate some lines for you to add into your pykota.conf")) |
---|
474 | run(parser, PKTurnKey) |
---|