root / pykota / trunk / bin / pkturnkey @ 3260

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

Changed license to GNU GPL v3 or later.
Changed Python source encoding from ISO-8859-15 to UTF-8 (only ASCII
was used anyway).

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