root / pykota / trunk / bin / pkturnkey @ 2877

Revision 2877, 23.7 kB (checked in by jerome, 18 years ago)

Added support for pysnmp v4.x in addition to v3.4.x

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