root / pykota / trunk / bin / pkturnkey @ 3413

Revision 3413, 22.3 kB (checked in by jerome, 16 years ago)

Removed unnecessary spaces at EOL.

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