root / pykota / trunk / bin / pkturnkey @ 3102

Revision 3102, 23.5 kB (checked in by jerome, 18 years ago)

Now pkturnkey ensures print queues are rerouted through PyKota.

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