root / pykota / branches / 1.26_fixes / bin / pkturnkey @ 3416

Revision 3416, 22.9 kB (checked in by jerome, 16 years ago)

Fixes #25 using the third way. Needs to be ported to trunk if pkturnkey
still makes it in 1.27 final, which I'm really not sure about,
considering how it's easy, and probably more versatile, to do the same with
some shell scripting.

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