root / pykota / trunk / bin / pkturnkey @ 3429

Revision 3429, 22.7 kB (checked in by jerome, 14 years ago)

Changed the way informations are output, especially to replace 'print'
statements which won't exist anymore in Python 3.

  • 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, 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
24import sys
25import os
26import pwd
27import grp
28import socket
29import signal
30
31from pkipplib import pkipplib
32
33import pykota.appinit
34from pykota.utils import *
35
36from pykota.errors import PyKotaToolError, PyKotaCommandLineError
37from pykota.tool import Tool
38
39__doc__ = N_("""pkturnkey v%(__version__)s (c) %(__years__)s %(__author__)s
40
41A turn key tool for PyKota. When launched, this command will initialize
42PyKota's database with all existing print queues and some or all users.
43For now, no prices or limits are set, so printing is fully accounted
44for, but not limited. That's why you'll probably want to also use
45edpykota once the database has been initialized.
46
47command line usage :
48
49  pkturnkey [options] [printqueues names]
50
51options :
52
53  -v | --version       Prints pkturnkey version number then exits.
54  -h | --help          Prints this message then exits.
55
56  -c | --doconf        Give hints about what to put into pykota.conf
57
58  -d | --dousers       Manages users accounts as well.
59
60  -D | --dogroups      Manages users groups as well.
61                       Implies -d | --dousers.
62
63  -e | --emptygroups   Includes empty groups.
64
65  -f | --force         Modifies the database instead of printing what
66                       it would do.
67
68  -u | --uidmin uid    Only adds users whose uid is greater than or equal to
69                       uid. You can pass an username there as well, and its
70                       uid will be used automatically.
71                       If not set, 0 will be used automatically.
72                       Implies -d | --dousers.
73
74  -U | --uidmax uid    Only adds users whose uid is lesser than or equal to
75                       uid. You can pass an username there as well, and its
76                       uid will be used automatically.
77                       If not set, a large value will be used automatically.
78                       Implies -d | --dousers.
79
80  -g | --gidmin gid    Only adds groups whose gid is greater than or equal to
81                       gid. You can pass a groupname there as well, and its
82                       gid will be used automatically.
83                       If not set, 0 will be used automatically.
84                       Implies -D | --dogroups.
85
86  -G | --gidmax gid    Only adds groups whose gid is lesser than or equal to
87                       gid. You can pass a groupname there as well, and its
88                       gid will be used automatically.
89                       If not set, a large value will be used automatically.
90                       Implies -D | --dogroups.
91
92examples :
93
94  $ pkturnkey --dousers --uidmin jerome
95
96  Will simulate the initialization of PyKota's database will all existing
97  printers and print accounts for all users whose uid is greater than
98  or equal to jerome's one. Won't manage any users group.
99
100  To REALLY initialize the database instead of simulating it, please
101  use the -f | --force command line switch.
102
103  You can limit the initialization to only a subset of the existing
104  printers, by passing their names at the end of the command line.
105""")
106
107class PKTurnKey(Tool) :
108    """A class for an initialization tool."""
109    def listPrinters(self, namestomatch) :
110        """Returns a list of tuples (queuename, deviceuri) for all existing print queues."""
111        self.printInfo("Extracting all print queues.")
112        printers = []
113        server = pkipplib.CUPS()
114        for queuename in server.getPrinters() :
115            req = server.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES)
116            req.operation["printer-uri"] = ("uri", server.identifierToURI("printers", queuename))
117            req.operation["requested-attributes"] = ("keyword", "device-uri")
118            result = server.doRequest(req)
119            try :
120                deviceuri = result.printer["device-uri"][0][1]
121            except (AttributeError, IndexError, KeyError) :
122                deviceuri = None
123            if deviceuri is not None :
124                if self.matchString(queuename, namestomatch) :
125                    printers.append((queuename, deviceuri))
126                else :
127                    self.printInfo("Print queue %s skipped." % queuename)
128        return printers
129
130    def listUsers(self, uidmin, uidmax) :
131        """Returns a list of users whose uids are between uidmin and uidmax."""
132        self.printInfo("Extracting all users whose uid is between %s and %s." % (uidmin, uidmax))
133        return [(entry[0], entry[3]) for entry in pwd.getpwall() if uidmin <= entry[2] <= uidmax]
134
135    def listGroups(self, gidmin, gidmax, users) :
136        """Returns a list of groups whose gids are between gidmin and gidmax."""
137        self.printInfo("Extracting all groups whose gid is between %s and %s." % (gidmin, gidmax))
138        groups = [(entry[0], entry[2], entry[3]) for entry in grp.getgrall() if gidmin <= entry[2] <= gidmax]
139        gidusers = {}
140        usersgid = {}
141        for u in users :
142            gidusers.setdefault(u[1], []).append(u[0])
143            usersgid.setdefault(u[0], []).append(u[1])
144
145        membership = {}
146        for g in range(len(groups)) :
147            (gname, gid, members) = groups[g]
148            newmembers = {}
149            for m in members :
150                newmembers[m] = m
151            try :
152                usernames = gidusers[gid]
153            except KeyError :
154                pass
155            else :
156                for username in usernames :
157                    if not newmembers.has_key(username) :
158                        newmembers[username] = username
159            for member in newmembers.keys() :
160                if not usersgid.has_key(member) :
161                    del newmembers[member]
162            membership[gname] = newmembers.keys()
163        return membership
164
165    def runCommand(self, command, dryrun) :
166        """Launches an external command."""
167        self.printInfo("%s" % command)
168        if not dryrun :
169            os.system(command)
170
171    def createPrinters(self, printers, dryrun=0) :
172        """Creates all printers in PyKota's database."""
173        if printers :
174            args = open("/tmp/pkprinters.args", "w")
175            args.write('--add\n--cups\n--skipexisting\n--description\n"printer created from pkturnkey"\n')
176            args.write("%s\n" % "\n".join(['"%s"' % p[0] for p in printers]))
177            args.close()
178            self.runCommand("pkprinters --arguments /tmp/pkprinters.args", dryrun)
179
180    def createUsers(self, users, printers, dryrun=0) :
181        """Creates all users in PyKota's database."""
182        if users :
183            args = open("/tmp/pkusers.users.args", "w")
184            args.write('--add\n--skipexisting\n--description\n"user created from pkturnkey"\n--limitby\nnoquota\n')
185            args.write("%s\n" % "\n".join(['"%s"' % u for u in users]))
186            args.close()
187            self.runCommand("pkusers --arguments /tmp/pkusers.users.args", dryrun)
188
189            printersnames = [p[0] for p in printers]
190            args = open("/tmp/edpykota.users.args", "w")
191            args.write('--add\n--skipexisting\n--noquota\n--printer\n')
192            args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames]))
193            args.write("%s\n" % "\n".join(['"%s"' % u for u in users]))
194            args.close()
195            self.runCommand("edpykota --arguments /tmp/edpykota.users.args", dryrun)
196
197    def createGroups(self, groups, printers, dryrun=0) :
198        """Creates all groups in PyKota's database."""
199        if groups :
200            args = open("/tmp/pkusers.groups.args", "w")
201            args.write('--groups\n--add\n--skipexisting\n--description\n"group created from pkturnkey"\n--limitby\nnoquota\n')
202            args.write("%s\n" % "\n".join(['"%s"' % g for g in groups]))
203            args.close()
204            self.runCommand("pkusers --arguments /tmp/pkusers.groups.args", dryrun)
205
206            printersnames = [p[0] for p in printers]
207            args = open("/tmp/edpykota.groups.args", "w")
208            args.write('--groups\n--add\n--skipexisting\n--noquota\n--printer\n')
209            args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames]))
210            args.write("%s\n" % "\n".join(['"%s"' % g for g in groups]))
211            args.close()
212            self.runCommand("edpykota --arguments /tmp/edpykota.groups.args", dryrun)
213
214            revmembership = {}
215            for (groupname, usernames) in groups.items() :
216                for username in usernames :
217                    revmembership.setdefault(username, []).append(groupname)
218            commands = []
219            for (username, groupnames) in revmembership.items() :
220                commands.append('pkusers --ingroups %s "%s"' \
221                    % (",".join(['"%s"' % g for g in groupnames]), username))
222            for command in commands :
223                self.runCommand(command, dryrun)
224
225    def supportsSNMP(self, hostname, community) :
226        """Returns 1 if the printer accepts SNMP queries, else 0."""
227        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
228        try :
229            from pysnmp.entity.rfc3413.oneliner import cmdgen
230        except ImportError :
231            hasV4 = False
232            try :
233                from pysnmp.asn1.encoding.ber.error import TypeMismatchError
234                from pysnmp.mapping.udp.role import Manager
235                from pysnmp.proto.api import alpha
236            except ImportError :
237                logerr("pysnmp doesn't seem to be installed. SNMP checks will be ignored !\n")
238                return 0
239        else :
240            hasV4 = True
241
242        if hasV4 :
243            def retrieveSNMPValues(hostname, community) :
244                """Retrieves a printer's internal page counter and status via SNMP."""
245                errorIndication, errorStatus, errorIndex, varBinds = \
246                     cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", community, 0), \
247                                                      cmdgen.UdpTransportTarget((hostname, 161)), \
248                                                      tuple([int(i) for i in pageCounterOID.split('.')]))
249                if errorIndication :
250                    raise "No SNMP !"
251                elif errorStatus :
252                    raise "No SNMP !"
253                else :
254                    self.SNMPOK = True
255        else :
256            def retrieveSNMPValues(hostname, community) :
257                """Retrieves a printer's internal page counter and status via SNMP."""
258                ver = alpha.protoVersions[alpha.protoVersionId1]
259                req = ver.Message()
260                req.apiAlphaSetCommunity(community)
261                req.apiAlphaSetPdu(ver.GetRequestPdu())
262                req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()))
263                tsp = Manager()
264                try :
265                    tsp.sendAndReceive(req.berEncode(), \
266                                       (hostname, 161), \
267                                       (handleAnswer, req))
268                except :
269                    raise "No SNMP !"
270                tsp.close()
271
272            def handleAnswer(wholemsg, notusedhere, req):
273                """Decodes and handles the SNMP answer."""
274                ver = alpha.protoVersions[alpha.protoVersionId1]
275                rsp = ver.Message()
276                try :
277                    rsp.berDecode(wholemsg)
278                except TypeMismatchError, msg :
279                    raise "No SNMP !"
280                else :
281                    if req.apiAlphaMatch(rsp):
282                        errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
283                        if errorStatus:
284                            raise "No SNMP !"
285                        else:
286                            self.values = []
287                            for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
288                                self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
289                            try :
290                                pagecounter = self.values[0]
291                            except :
292                                raise "No SNMP !"
293                            else :
294                                self.SNMPOK = 1
295                                return 1
296
297        self.SNMPOK = 0
298        try :
299            retrieveSNMPValues(hostname, community)
300        except :
301            self.SNMPOK = 0
302        return self.SNMPOK
303
304    def supportsPJL(self, hostname, port) :
305        """Returns 1 if the printer accepts PJL queries over TCP, else 0."""
306        def alarmHandler(signum, frame) :
307            raise "Timeout !"
308
309        pjlsupport = 0
310        signal.signal(signal.SIGALRM, alarmHandler)
311        signal.alarm(2) # wait at most 2 seconds
312        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
313        try :
314            s.connect((hostname, port))
315            s.send("\033%-12345X@PJL INFO STATUS\r\n\033%-12345X")
316            answer = s.recv(1024)
317            if not answer.startswith("@PJL") :
318                raise "No PJL !"
319        except :
320            pass
321        else :
322            pjlsupport = 1
323        s.close()
324        signal.alarm(0)
325        signal.signal(signal.SIGALRM, signal.SIG_IGN)
326        return pjlsupport
327
328    def hintConfig(self, printers) :
329        """Gives some hints about what to put into pykota.conf"""
330        if not printers :
331            return
332        sys.stderr.flush() # ensure outputs don't mix
333        self.display("\n--- CUT ---\n")
334        self.display("# Here are some lines that we suggest you add at the end\n")
335        self.display("# of the pykota.conf file. These lines gives possible\n")
336        self.display("# values for the way print jobs' size will be computed.\n")
337        self.display("# NB : it is possible that a manual configuration gives\n")
338        self.display("# better results for you. As always, your mileage may vary.\n")
339        self.display("#\n")
340        for (name, uri) in printers :
341            self.display("[%s]\n" % name)
342            accounter = "software()"
343            try :
344                uri = uri.split("cupspykota:", 2)[-1]
345            except (ValueError, IndexError) :
346                pass
347            else :
348                while uri and uri.startswith("/") :
349                    uri = uri[1:]
350                try :
351                    (backend, destination) = uri.split(":", 1)
352                    if backend not in ("ipp", "http", "https", "lpd", "socket") :
353                        raise ValueError
354                except ValueError :
355                    pass
356                else :
357                    while destination.startswith("/") :
358                        destination = destination[1:]
359                    checkauth = destination.split("@", 1)
360                    if len(checkauth) == 2 :
361                        destination = checkauth[1]
362                    parts = destination.split("/")[0].split(":")
363                    if len(parts) == 2 :
364                        (hostname, port) = parts
365                        try :
366                            port = int(port)
367                        except ValueError :
368                            port = 9100
369                    else :
370                        (hostname, port) = parts[0], 9100
371
372                    if self.supportsSNMP(hostname, "public") :
373                        accounter = "hardware(snmp)"
374                    elif self.supportsPJL(hostname, 9100) :
375                        accounter = "hardware(pjl)"
376                    elif self.supportsPJL(hostname, 9101) :
377                        accounter = "hardware(pjl:9101)"
378                    elif self.supportsPJL(hostname, port) :
379                        accounter = "hardware(pjl:%s)" % port
380
381            self.display("preaccounter : software()\n")
382            self.display("accounter : %s\n" % accounter)
383            self.display("\n")
384        self.display("--- CUT ---\n")
385
386    def main(self, names, options) :
387        """Intializes PyKota's database."""
388        self.adminOnly()
389
390        if not names :
391            names = ["*"]
392
393        self.printInfo(_("Please be patient..."))
394        dryrun = not options["force"]
395        if dryrun :
396            self.printInfo(_("Don't worry, the database WILL NOT BE MODIFIED."))
397        else :
398            self.printInfo(_("Please WORRY NOW, the database WILL BE MODIFIED."))
399
400        if options["dousers"] :
401            if not options["uidmin"] :
402                self.printInfo(_("System users will have a print account as well !"), "warn")
403                uidmin = 0
404            else :
405                try :
406                    uidmin = int(options["uidmin"])
407                except :
408                    try :
409                        uidmin = pwd.getpwnam(options["uidmin"])[2]
410                    except KeyError, msg :
411                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
412                                                   % (options["uidmin"], msg)
413
414            if not options["uidmax"] :
415                uidmax = sys.maxint
416            else :
417                try :
418                    uidmax = int(options["uidmax"])
419                except :
420                    try :
421                        uidmax = pwd.getpwnam(options["uidmax"])[2]
422                    except KeyError, msg :
423                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
424                                                   % (options["uidmax"], msg)
425
426            if uidmin > uidmax :
427                (uidmin, uidmax) = (uidmax, uidmin)
428            users = self.listUsers(uidmin, uidmax)
429        else :
430            users = []
431
432        if options["dogroups"] :
433            if not options["gidmin"] :
434                self.printInfo(_("System groups will have a print account as well !"), "warn")
435                gidmin = 0
436            else :
437                try :
438                    gidmin = int(options["gidmin"])
439                except :
440                    try :
441                        gidmin = grp.getgrnam(options["gidmin"])[2]
442                    except KeyError, msg :
443                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
444                                                   % (options["gidmin"], msg)
445
446            if not options["gidmax"] :
447                gidmax = sys.maxint
448            else :
449                try :
450                    gidmax = int(options["gidmax"])
451                except :
452                    try :
453                        gidmax = grp.getgrnam(options["gidmax"])[2]
454                    except KeyError, msg :
455                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
456                                                   % (options["gidmax"], msg)
457
458            if gidmin > gidmax :
459                (gidmin, gidmax) = (gidmax, gidmin)
460            groups = self.listGroups(gidmin, gidmax, users)
461            if not options["emptygroups"] :
462                for (groupname, members) in groups.items() :
463                    if not members :
464                        del groups[groupname]
465        else :
466            groups = []
467
468        printers = self.listPrinters(names)
469        if printers :
470            self.createPrinters(printers, dryrun)
471            self.createUsers([entry[0] for entry in users], printers, dryrun)
472            self.createGroups(groups, printers, dryrun)
473
474        if dryrun :
475            self.printInfo(_("Simulation terminated."))
476        else :
477            self.printInfo(_("Database initialized !"))
478
479        if options["doconf"] :
480            self.hintConfig(printers)
481
482
483if __name__ == "__main__" :
484    retcode = 0
485    try :
486        short_options = "hvdDefu:U:g:G:c"
487        long_options = ["help", "version", "dousers", "dogroups", \
488                        "emptygroups", "force", "uidmin=", "uidmax=", \
489                        "gidmin=", "gidmax=", "doconf"]
490
491        # Initializes the command line tool
492        manager = PKTurnKey(doc=__doc__)
493        manager.deferredInit()
494
495        # parse and checks the command line
496        (options, args) = manager.parseCommandline(sys.argv[1:], \
497                                                   short_options, \
498                                                   long_options, \
499                                                   allownothing=1)
500
501        # sets long options
502        options["help"] = options["h"] or options["help"]
503        options["version"] = options["v"] or options["version"]
504        options["dousers"] = options["d"] or options["dousers"]
505        options["dogroups"] = options["D"] or options["dogroups"]
506        options["emptygroups"] = options["e"] or options["emptygroups"]
507        options["force"] = options["f"] or options["force"]
508        options["uidmin"] = options["u"] or options["uidmin"]
509        options["uidmax"] = options["U"] or options["uidmax"]
510        options["gidmin"] = options["g"] or options["gidmin"]
511        options["gidmax"] = options["G"] or options["gidmax"]
512        options["doconf"] = options["c"] or options["doconf"]
513
514        if options["uidmin"] or options["uidmax"] :
515            if not options["dousers"] :
516                manager.printInfo(_("The --uidmin or --uidmax command line option implies --dousers as well."), "warn")
517            options["dousers"] = 1
518
519        if options["gidmin"] or options["gidmax"] :
520            if not options["dogroups"] :
521                manager.printInfo(_("The --gidmin or --gidmax command line option implies --dogroups as well."), "warn")
522            options["dogroups"] = 1
523
524        if options["dogroups"] :
525            if not options["dousers"] :
526                manager.printInfo(_("The --dogroups command line option implies --dousers as well."), "warn")
527            options["dousers"] = 1
528
529        if options["help"] :
530            manager.display_usage_and_quit()
531        elif options["version"] :
532            manager.display_version_and_quit()
533        else :
534            retcode = manager.main(args, options)
535    except KeyboardInterrupt :
536        logerr("\nInterrupted with Ctrl+C !\n")
537        retcode = -3
538    except PyKotaCommandLineError, msg :
539        logerr("%s : %s\n" % (sys.argv[0], msg))
540        retcode = -2
541    except SystemExit :
542        pass
543    except :
544        try :
545            manager.crashed("pkturnkey failed")
546        except :
547            crashed("pkturnkey failed")
548        retcode = -1
549
550    try :
551        manager.storage.close()
552    except (TypeError, NameError, AttributeError) :
553        pass
554
555    sys.exit(retcode)
Note: See TracBrowser for help on using the browser.