root / pykota / trunk / bin / pkturnkey @ 3424

Revision 3424, 22.6 kB (checked in by jerome, 16 years ago)

Ported the fix for #25 to trunk. References #25.

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