root / pykota / trunk / bin / pkturnkey @ 2829

Revision 2829, 22.5 kB (checked in by jerome, 19 years ago)

Did a pass with pylint.

  • 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.role import Manager
226            from pysnmp.proto.api import alpha
227        except ImportError :   
228            sys.stderr.write("pysnmp doesn't seem to be installed. SNMP checks will be ignored !\n")
229            return 0
230           
231        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
232        def retrieveSNMPValues(hostname, community) :   
233            """Retrieves a printer's internal page counter and status via SNMP."""
234            ver = alpha.protoVersions[alpha.protoVersionId1]
235            req = ver.Message()
236            req.apiAlphaSetCommunity(community)
237            req.apiAlphaSetPdu(ver.GetRequestPdu())
238            req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()))
239            tsp = Manager()
240            try :
241                tsp.sendAndReceive(req.berEncode(), \
242                                   (hostname, 161), \
243                                   (handleAnswer, req))
244            except :   
245                raise "No SNMP !"
246            tsp.close()
247       
248        def handleAnswer(wholemsg, notusedhere, req):
249            """Decodes and handles the SNMP answer."""
250            ver = alpha.protoVersions[alpha.protoVersionId1]
251            rsp = ver.Message()
252            try :
253                rsp.berDecode(wholemsg)
254            except TypeMismatchError, msg :   
255                raise "No SNMP !"
256            else :
257                if req.apiAlphaMatch(rsp):
258                    errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
259                    if errorStatus:
260                        raise "No SNMP !"
261                    else:
262                        self.values = []
263                        for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
264                            self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
265                        try :   
266                            pagecounter = self.values[0]
267                        except :
268                            raise "No SNMP !"
269                        else :   
270                            self.SNMPOK = 1
271                            return 1
272           
273        self.SNMPOK = 0
274        try :
275            retrieveSNMPValues(hostname, community)
276        except :   
277            self.SNMPOK = 0
278        return self.SNMPOK
279       
280    def supportsPJL(self, hostname, port) :
281        """Returns 1 if the printer accepts PJL queries over TCP, else 0."""
282        def alarmHandler(signum, frame) :
283            raise "Timeout !"
284       
285        pjlsupport = 0
286        signal.signal(signal.SIGALRM, alarmHandler)
287        signal.alarm(2) # wait at most 2 seconds
288        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
289        try :
290            s.connect((hostname, port))
291            s.send("\033%-12345X@PJL INFO STATUS\r\n\033%-12345X")
292            answer = s.recv(1024)
293            if not answer.startswith("@PJL") :
294                raise "No PJL !"
295        except :   
296            pass
297        else :   
298            pjlsupport = 1
299        s.close()
300        signal.alarm(0)
301        signal.signal(signal.SIGALRM, signal.SIG_IGN)
302        return pjlsupport
303           
304    def hintConfig(self, printers) :   
305        """Gives some hints about what to put into pykota.conf"""
306        if not printers :
307            return
308        sys.stderr.flush() # ensure outputs don't mix   
309        print     
310        print "--- CUT ---"
311        print "# Here are some lines that we suggest you add at the end"
312        print "# of the pykota.conf file. These lines gives possible"
313        print "# values for the way print jobs' size will be computed."
314        print "# NB : it is possible that a manual configuration gives"
315        print "# better results for you. As always, your mileage may vary."
316        print "#"
317        for (name, uri) in printers :
318            print "[%s]" % name
319            accounter = "software()"
320            try :
321                uri = uri.split("cupspykota:", 2)[-1]
322            except (ValueError, IndexError) :   
323                pass
324            else :   
325                while uri and uri.startswith("/") :
326                    uri = uri[1:]
327                try :
328                    (backend, destination) = uri.split(":", 1) 
329                    if backend not in ("ipp", "http", "https", "lpd", "socket") :
330                        raise ValueError
331                except ValueError :   
332                    pass
333                else :       
334                    while destination.startswith("/") :
335                        destination = destination[1:]
336                    checkauth = destination.split("@", 1)   
337                    if len(checkauth) == 2 :
338                        destination = checkauth[1]
339                    parts = destination.split("/")[0].split(":")
340                    if len(parts) == 2 :
341                        (hostname, port) = parts
342                        try :
343                            port = int(port)
344                        except ValueError :
345                            port = 9100
346                    else :   
347                        (hostname, port) = parts[0], 9100
348                       
349                    if self.supportsSNMP(hostname, "public") :
350                        accounter = "hardware(snmp)"
351                    elif self.supportsPJL(hostname, 9100) :
352                        accounter = "hardware(pjl)"
353                    elif self.supportsPJL(hostname, 9101) :
354                        accounter = "hardware(pjl:9101)"
355                    elif self.supportsPJL(hostname, port) :   
356                        accounter = "hardware(pjl:%s)" % port
357                   
358            print "accounter : %s" % accounter
359            print
360        print "--- CUT ---"
361       
362    def main(self, names, options) :
363        """Intializes PyKota's database."""
364        if not self.config.isAdmin :
365            raise PyKotaCommandLineError, "%s : %s" % (pwd.getpwuid(os.geteuid())[0], \
366                                   _("You're not allowed to use this command."))
367           
368        if not names :
369            names = ["*"]
370           
371        self.printInfo(_("Please be patient..."))
372        dryrun = not options["force"]
373        if dryrun :
374            self.printInfo(_("Don't worry, the database WILL NOT BE MODIFIED."))
375        else :   
376            self.printInfo(_("Please WORRY NOW, the database WILL BE MODIFIED."))
377           
378        if options["dousers"] :   
379            if not options["uidmin"] :   
380                self.printInfo(_("System users will have a print account as well !"), "warn")
381                uidmin = 0
382            else :   
383                try :
384                    uidmin = int(options["uidmin"])
385                except :   
386                    try :
387                        uidmin = pwd.getpwnam(options["uidmin"])[2]
388                    except KeyError, msg :   
389                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
390                                                   % (options["uidmin"], msg)
391                       
392            if not options["uidmax"] :   
393                uidmax = sys.maxint
394            else :   
395                try :
396                    uidmax = int(options["uidmax"])
397                except :   
398                    try :
399                        uidmax = pwd.getpwnam(options["uidmax"])[2]
400                    except KeyError, msg :   
401                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
402                                                   % (options["uidmax"], msg)
403           
404            if uidmin > uidmax :           
405                (uidmin, uidmax) = (uidmax, uidmin)
406            users = self.listUsers(uidmin, uidmax)
407        else :   
408            users = []
409           
410        if options["dogroups"] :   
411            if not options["gidmin"] :   
412                self.printInfo(_("System groups will have a print account as well !"), "warn")
413                gidmin = 0
414            else :   
415                try :
416                    gidmin = int(options["gidmin"])
417                except :   
418                    try :
419                        gidmin = grp.getgrnam(options["gidmin"])[2]
420                    except KeyError, msg :   
421                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
422                                                   % (options["gidmin"], msg)
423                       
424            if not options["gidmax"] :   
425                gidmax = sys.maxint
426            else :   
427                try :
428                    gidmax = int(options["gidmax"])
429                except :   
430                    try :
431                        gidmax = grp.getgrnam(options["gidmax"])[2]
432                    except KeyError, msg :   
433                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
434                                                   % (options["gidmax"], msg)
435           
436            if gidmin > gidmax :           
437                (gidmin, gidmax) = (gidmax, gidmin)
438            groups = self.listGroups(gidmin, gidmax, users)
439            if not options["emptygroups"] :
440                for (groupname, members) in groups.items() :
441                    if not members :
442                        del groups[groupname]
443        else :   
444            groups = []
445           
446        printers = self.listPrinters(names)
447        if printers :
448            self.createPrinters(printers, dryrun)
449            self.createUsers([entry[0] for entry in users], printers, dryrun)
450            self.createGroups(groups, printers, dryrun)
451       
452        if dryrun :
453            self.printInfo(_("Simulation terminated."))
454        else :   
455            self.printInfo(_("Database initialized !"))
456       
457        if options["doconf"] :   
458            self.hintConfig(printers)
459                   
460                     
461if __name__ == "__main__" : 
462    retcode = 0
463    try :
464        short_options = "hvdDefu:U:g:G:c"
465        long_options = ["help", "version", "dousers", "dogroups", \
466                        "emptygroups", "force", "uidmin=", "uidmax=", \
467                        "gidmin=", "gidmax=", "doconf"]
468       
469        # Initializes the command line tool
470        manager = PKTurnKey(doc=__doc__)
471        manager.deferredInit()
472       
473        # parse and checks the command line
474        (options, args) = manager.parseCommandline(sys.argv[1:], \
475                                                   short_options, \
476                                                   long_options, \
477                                                   allownothing=1)
478       
479        # sets long options
480        options["help"] = options["h"] or options["help"]
481        options["version"] = options["v"] or options["version"]
482        options["dousers"] = options["d"] or options["dousers"]
483        options["dogroups"] = options["D"] or options["dogroups"]
484        options["emptygroups"] = options["e"] or options["emptygroups"]
485        options["force"] = options["f"] or options["force"]
486        options["uidmin"] = options["u"] or options["uidmin"]
487        options["uidmax"] = options["U"] or options["uidmax"]
488        options["gidmin"] = options["g"] or options["gidmin"]
489        options["gidmax"] = options["G"] or options["gidmax"]
490        options["doconf"] = options["c"] or options["doconf"]
491       
492        if options["uidmin"] or options["uidmax"] :
493            if not options["dousers"] :
494                manager.printInfo(_("The --uidmin or --uidmax command line option implies --dousers as well."), "warn")
495            options["dousers"] = 1   
496           
497        if options["gidmin"] or options["gidmax"] :
498            if not options["dogroups"] :
499                manager.printInfo(_("The --gidmin or --gidmax command line option implies --dogroups as well."), "warn")
500            options["dogroups"] = 1
501       
502        if options["dogroups"] :
503            if not options["dousers"] :
504                manager.printInfo(_("The --dogroups command line option implies --dousers as well."), "warn")
505            options["dousers"] = 1   
506           
507        if options["help"] :
508            manager.display_usage_and_quit()
509        elif options["version"] :
510            manager.display_version_and_quit()
511        else :
512            retcode = manager.main(args, options)
513    except KeyboardInterrupt :       
514        sys.stderr.write("\nInterrupted with Ctrl+C !\n")
515        retcode = -3
516    except PyKotaCommandLineError, msg :   
517        sys.stderr.write("%s : %s\n" % (sys.argv[0], msg))
518        retcode = -2
519    except SystemExit :       
520        pass
521    except :
522        try :
523            manager.crashed("pkturnkey failed")
524        except :   
525            crashed("pkturnkey failed")
526        retcode = -1
527
528    try :
529        manager.storage.close()
530    except (TypeError, NameError, AttributeError) :   
531        pass
532       
533    sys.exit(retcode)   
Note: See TracBrowser for help on using the browser.