root / pykota / trunk / bin / pkturnkey @ 3288

Revision 3288, 23.4 kB (checked in by jerome, 16 years ago)

Moved all exceptions definitions to a dedicated module.

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