root / pykota / trunk / bin / pkturnkey @ 2745

Revision 2745, 22.5 kB (checked in by jerome, 18 years ago)

Reactivated the pkturnkey command line tool.

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