root / pykota / trunk / bin / pkturnkey @ 2609

Revision 2609, 21.3 kB (checked in by jerome, 18 years ago)

Insufficient permission to run a command now exits with status -2.
Error in command line options now exits with status -2.
Processing interrupted with Ctrl+C (or SIGTERM) now exits with status -3.

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