root / pykota / trunk / bin / pkturnkey @ 2831

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

Adds a hint for preaccounter directives.

  • 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 "preaccounter : software()" 
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.