root / pykota / trunk / bin / pkturnkey @ 3506

Revision 3506, 19.1 kB (checked in by jerome, 15 years ago)

Removed all support for pysnmp v3.x
Applied the patch from Ilya Etingof and Börje Sennung to fix #47.

  • 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-2009 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
24"""A tool that can be used to fill PyKota's database from system accounts, and detect the best accouting settings for the existing printers."""
25
26import sys
27import os
28import pwd
29import grp
30import socket
31import signal
32
33from pkipplib import pkipplib
34
35import pykota.appinit
36from pykota.utils import run, logerr
37from pykota.commandline import PyKotaOptionParser
38from pykota.errors import PyKotaToolError, PyKotaCommandLineError
39from pykota.tool import Tool
40
41class PKTurnKey(Tool) :
42    """A class for an initialization tool."""
43    def listPrinters(self, namestomatch) :
44        """Returns a list of tuples (queuename, deviceuri) for all existing print queues."""
45        self.printInfo("Extracting all print queues.")
46        printers = []
47        server = pkipplib.CUPS()
48        for queuename in server.getPrinters() :
49            req = server.newRequest(pkipplib.IPP_GET_PRINTER_ATTRIBUTES)
50            req.operation["printer-uri"] = ("uri", server.identifierToURI("printers", queuename))
51            req.operation["requested-attributes"] = ("keyword", "device-uri")
52            result = server.doRequest(req)
53            try :
54                deviceuri = result.printer["device-uri"][0][1]
55            except (AttributeError, IndexError, KeyError) :
56                deviceuri = None
57            if deviceuri is not None :
58                if self.matchString(queuename, namestomatch) :
59                    printers.append((queuename, deviceuri))
60                else :
61                    self.printInfo("Print queue %s skipped." % queuename)
62        return printers
63
64    def listUsers(self, uidmin, uidmax) :
65        """Returns a list of users whose uids are between uidmin and uidmax."""
66        self.printInfo("Extracting all users whose uid is between %s and %s." % (uidmin, uidmax))
67        return [(entry.pw_name, entry.pw_gid) for entry in pwd.getpwall() if uidmin <= entry.pw_uid <= uidmax]
68
69    def listGroups(self, gidmin, gidmax, users) :
70        """Returns a list of groups whose gids are between gidmin and gidmax."""
71        self.printInfo("Extracting all groups whose gid is between %s and %s." % (gidmin, gidmax))
72        groups = [(entry.gr_name, entry.gr_gid, entry.gr_mem) for entry in grp.getgrall() if gidmin <= entry.gr_gid <= gidmax]
73        gidusers = {}
74        usersgid = {}
75        for u in users :
76            gidusers.setdefault(u[1], []).append(u[0])
77            usersgid.setdefault(u[0], []).append(u[1])
78
79        membership = {}
80        for g in range(len(groups)) :
81            (gname, gid, members) = groups[g]
82            newmembers = {}
83            for m in members :
84                newmembers[m] = m
85            try :
86                usernames = gidusers[gid]
87            except KeyError :
88                pass
89            else :
90                for username in usernames :
91                    if not newmembers.has_key(username) :
92                        newmembers[username] = username
93            for member in newmembers.keys() :
94                if not usersgid.has_key(member) :
95                    del newmembers[member]
96            membership[gname] = newmembers.keys()
97        return membership
98
99    def runCommand(self, command, dryrun) :
100        """Launches an external command."""
101        self.printInfo("%s" % command)
102        if not dryrun :
103            os.system(command)
104
105    def createPrinters(self, printers, dryrun=0) :
106        """Creates all printers in PyKota's database."""
107        if printers :
108            args = open("/tmp/pkprinters.args", "w")
109            args.write('--add\n--cups\n--skipexisting\n--description\n"printer created from pkturnkey"\n')
110            args.write("%s\n" % "\n".join(['"%s"' % p[0] for p in printers]))
111            args.close()
112            self.runCommand("pkprinters --arguments /tmp/pkprinters.args", dryrun)
113
114    def createUsers(self, users, printers, dryrun=0) :
115        """Creates all users in PyKota's database."""
116        if users :
117            args = open("/tmp/pkusers.users.args", "w")
118            args.write('--add\n--skipexisting\n--description\n"user created from pkturnkey"\n--limitby\nnoquota\n')
119            args.write("%s\n" % "\n".join(['"%s"' % u for u in users]))
120            args.close()
121            self.runCommand("pkusers --arguments /tmp/pkusers.users.args", dryrun)
122
123            printersnames = [p[0] for p in printers]
124            args = open("/tmp/edpykota.users.args", "w")
125            args.write('--add\n--skipexisting\n--noquota\n--printer\n')
126            args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames]))
127            args.write("%s\n" % "\n".join(['"%s"' % u for u in users]))
128            args.close()
129            self.runCommand("edpykota --arguments /tmp/edpykota.users.args", dryrun)
130
131    def createGroups(self, groups, printers, dryrun=0) :
132        """Creates all groups in PyKota's database."""
133        if groups :
134            args = open("/tmp/pkusers.groups.args", "w")
135            args.write('--groups\n--add\n--skipexisting\n--description\n"group created from pkturnkey"\n--limitby\nnoquota\n')
136            args.write("%s\n" % "\n".join(['"%s"' % g for g in groups]))
137            args.close()
138            self.runCommand("pkusers --arguments /tmp/pkusers.groups.args", dryrun)
139
140            printersnames = [p[0] for p in printers]
141            args = open("/tmp/edpykota.groups.args", "w")
142            args.write('--groups\n--add\n--skipexisting\n--noquota\n--printer\n')
143            args.write("%s\n" % ",".join(['"%s"' % p for p in printersnames]))
144            args.write("%s\n" % "\n".join(['"%s"' % g for g in groups]))
145            args.close()
146            self.runCommand("edpykota --arguments /tmp/edpykota.groups.args", dryrun)
147
148            revmembership = {}
149            for (groupname, usernames) in groups.items() :
150                for username in usernames :
151                    revmembership.setdefault(username, []).append(groupname)
152            commands = []
153            for (username, groupnames) in revmembership.items() :
154                commands.append('pkusers --ingroups %s "%s"' \
155                    % (",".join(['"%s"' % g for g in groupnames]), username))
156            for command in commands :
157                self.runCommand(command, dryrun)
158
159    def supportsSNMP(self, hostname, community) :
160        """Returns 1 if the printer accepts SNMP queries, else 0."""
161        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
162        try :
163            from pysnmp.entity.rfc3413.oneliner import cmdgen
164        except ImportError :
165            logerr("pysnmp doesn't seem to be installed. PyKota needs pysnmp v4.x, otherwise SNMP checks will be ignored !\n")
166            return False
167        try :
168            errorIndication, errorStatus, errorIndex, varBinds = \
169                cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", community, 0), \
170                                                     cmdgen.UdpTransportTarget((hostname, 161)), \
171                                                     tuple([int(i) for i in pageCounterOID.split('.')]))
172            if errorIndication :
173                raise "No SNMP !"
174            elif errorStatus :
175                raise "No SNMP !"
176            else :
177                self.SNMPOK = True
178        except :
179            self.SNMPOK = False
180        return self.SNMPOK
181
182    def supportsPJL(self, hostname, port) :
183        """Returns 1 if the printer accepts PJL queries over TCP, else 0."""
184        def alarmHandler(signum, frame) :
185            raise "Timeout !"
186
187        pjlsupport = False
188        signal.signal(signal.SIGALRM, alarmHandler)
189        signal.alarm(2) # wait at most 2 seconds
190        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
191        try :
192            s.connect((hostname, port))
193            s.send("\033%-12345X@PJL INFO STATUS\r\n\033%-12345X")
194            answer = s.recv(1024)
195            if not answer.startswith("@PJL") :
196                raise "No PJL !"
197        except :
198            pass
199        else :
200            pjlsupport = True
201        s.close()
202        signal.alarm(0)
203        signal.signal(signal.SIGALRM, signal.SIG_IGN)
204        return pjlsupport
205
206    def hintConfig(self, printers) :
207        """Gives some hints about what to put into pykota.conf"""
208        if not printers :
209            return
210        sys.stderr.flush() # ensure outputs don't mix
211        self.display("\n--- CUT ---\n")
212        self.display("# Here are some lines that we suggest you add at the end\n")
213        self.display("# of the pykota.conf file. These lines gives possible\n")
214        self.display("# values for the way print jobs' size will be computed.\n")
215        self.display("# NB : it is possible that a manual configuration gives\n")
216        self.display("# better results for you. As always, your mileage may vary.\n")
217        self.display("#\n")
218        for (name, uri) in printers :
219            self.display("[%s]\n" % name)
220            accounter = "software()"
221            try :
222                uri = uri.split("cupspykota:", 2)[-1]
223            except (ValueError, IndexError) :
224                pass
225            else :
226                while uri and uri.startswith("/") :
227                    uri = uri[1:]
228                try :
229                    (backend, destination) = uri.split(":", 1)
230                    if backend not in ("ipp", "http", "https", "lpd", "socket") :
231                        raise ValueError
232                except ValueError :
233                    pass
234                else :
235                    while destination.startswith("/") :
236                        destination = destination[1:]
237                    checkauth = destination.split("@", 1)
238                    if len(checkauth) == 2 :
239                        destination = checkauth[1]
240                    parts = destination.split("/")[0].split(":")
241                    if len(parts) == 2 :
242                        (hostname, port) = parts
243                        try :
244                            port = int(port)
245                        except ValueError :
246                            port = 9100
247                    else :
248                        (hostname, port) = parts[0], 9100
249
250                    if self.supportsSNMP(hostname, "public") :
251                        accounter = "hardware(snmp)"
252                    elif self.supportsPJL(hostname, 9100) :
253                        accounter = "hardware(pjl)"
254                    elif self.supportsPJL(hostname, 9101) :
255                        accounter = "hardware(pjl:9101)"
256                    elif self.supportsPJL(hostname, port) :
257                        accounter = "hardware(pjl:%s)" % port
258
259            self.display("preaccounter : software()\n")
260            self.display("accounter : %s\n" % accounter)
261            self.display("\n")
262        self.display("--- CUT ---\n")
263
264    def main(self, names, options) :
265        """Intializes PyKota's database."""
266        self.adminOnly()
267
268        if options.uidmin or options.uidmax :
269            if not options.dousers :
270                self.printInfo(_("The --uidmin or --uidmax command line option implies --dousers as well."), "warn")
271            options.dousers = True
272
273        if options.gidmin or options.gidmax :
274            if not options.dogroups :
275                self.printInfo(_("The --gidmin or --gidmax command line option implies --dogroups as well."), "warn")
276            options.dogroups = True
277
278        if options.dogroups :
279            if not options.dousers :
280                self.printInfo(_("The --dogroups command line option implies --dousers as well."), "warn")
281            options.dousers = True
282
283        if not names :
284            names = [u"*"]
285
286        self.printInfo(_("Please be patient..."))
287        dryrun = not options.force
288        if dryrun :
289            self.printInfo(_("Don't worry, the database WILL NOT BE MODIFIED."))
290        else :
291            self.printInfo(_("Please WORRY NOW, the database WILL BE MODIFIED."))
292
293        if options.dousers :
294            if not options.uidmin :
295                self.printInfo(_("System users will have a print account as well !"), "warn")
296                uidmin = 0
297            else :
298                try :
299                    uidmin = int(options.uidmin)
300                except :
301                    try :
302                        uidmin = pwd.getpwnam(options.uidmin).pw_uid
303                    except KeyError, msg :
304                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
305                                                   % (options.uidmin, msg)
306
307            if not options.uidmax :
308                uidmax = sys.maxint
309            else :
310                try :
311                    uidmax = int(options.uidmax)
312                except :
313                    try :
314                        uidmax = pwd.getpwnam(options.uidmax).pw_uid
315                    except KeyError, msg :
316                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
317                                                   % (options.uidmax, msg)
318
319            if uidmin > uidmax :
320                (uidmin, uidmax) = (uidmax, uidmin)
321            users = self.listUsers(uidmin, uidmax)
322        else :
323            users = []
324
325        if options.dogroups :
326            if not options.gidmin :
327                self.printInfo(_("System groups will have a print account as well !"), "warn")
328                gidmin = 0
329            else :
330                try :
331                    gidmin = int(options.gidmin)
332                except :
333                    try :
334                        gidmin = grp.getgrnam(options.gidmin).gr_gid
335                    except KeyError, msg :
336                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
337                                                   % (options.gidmin, msg)
338
339            if not options.gidmax :
340                gidmax = sys.maxint
341            else :
342                try :
343                    gidmax = int(options.gidmax)
344                except :
345                    try :
346                        gidmax = grp.getgrnam(options.gidmax).gr_gid
347                    except KeyError, msg :
348                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
349                                                   % (options.gidmax, msg)
350
351            if gidmin > gidmax :
352                (gidmin, gidmax) = (gidmax, gidmin)
353            groups = self.listGroups(gidmin, gidmax, users)
354            if not options.emptygroups :
355                for (groupname, members) in groups.items() :
356                    if not members :
357                        del groups[groupname]
358        else :
359            groups = []
360
361        printers = self.listPrinters(names)
362        if printers :
363            self.createPrinters(printers, dryrun)
364            self.createUsers([entry[0] for entry in users], printers, dryrun)
365            self.createGroups(groups, printers, dryrun)
366
367        if dryrun :
368            self.printInfo(_("Simulation terminated."))
369        else :
370            self.printInfo(_("Database initialized !"))
371
372        if options.doconf :
373            self.hintConfig(printers)
374
375
376if __name__ == "__main__" :
377    parser = PyKotaOptionParser(description=_("A turn key tool for PyKota. When launched, this command will initialize PyKota's database with all existing print queues and some or all users. For now, no prices or limits are set, so printing is fully accounted for, but not limited. That's why you'll probably want to also use edpykota once the database has been initialized."),
378                                usage="pkturnkey [options] printer1 printer2 ... printerN")
379    parser.add_option("-c", "--doconf",
380                            action="store_true",
381                            dest="doconf",
382                            help=_("Try to autodetect the best print accounting settings for existing CUPS printers. All printers must be switched ON beforehand."))
383    parser.add_option("-d", "--dousers",
384                            action="store_true",
385                            dest="dousers",
386                            help=_("Create accounts for users, and allocate print quota entries for them."))
387    parser.add_option("-D", "--dogroups",
388                            action="store_true",
389                            dest="dogroups",
390                            help=_("Create accounts for users groups, and allocate print quota entries for them."))
391    parser.add_option("-e", "--emptygroups",
392                            action="store_true",
393                            dest="emptygroups",
394                            help=_("Also include groups which don't have any member."))
395    parser.add_option("-f", "--force",
396                            action="store_true",
397                            dest="force",
398                            help=_("Modifies PyKota's database content for real, instead of faking it (for safety reasons)."))
399    parser.add_option("-u", "--uidmin",
400                            dest="uidmin",
401                            help=_("Only include users whose uid is greater than or equal to this parameter. If you pass an username instead, his uid will be used automatically."))
402    parser.add_option("-U", "--uidmax",
403                            dest="uidmax",
404                            help=_("Only include users whose uid is lesser than or equal to this parameter. If you pass an username instead, his uid will be used automatically."))
405    parser.add_option("-g", "--gidmin",
406                            dest="gidmin",
407                            help=_("Only include users groups whose gid is greater than or equal to this parameter. If you pass a groupname instead, its gid will be used automatically."))
408    parser.add_option("-G", "--gidmax",
409                            dest="gidmax",
410                            help=_("Only include users groups whose gid is lesser than or equal to this parameter. If you pass a groupname instead, its gid will be used automatically."))
411
412    parser.add_example("--dousers --uidmin jerome HPLASER1 HPLASER2",
413                       _("Would simulate the creation in PyKota's database of the printing accounts for all users whose uid is greater than or equal to 'jerome''s. Each of them would be given a print quota entry on printers 'HPLASER1' and 'HPLASER2'."))
414    parser.add_example("--force --dousers --uidmin jerome HPLASER1 HPLASER2",
415                       _("Would do the same as the example above, but for real. Please take great care when using the --force command line option."))
416    parser.add_example("--doconf",
417                       _("Would try to automatically detect the best print accounting settings for all active printers, and generate some lines for you to add into your pykota.conf"))
418    run(parser, PKTurnKey)
Note: See TracBrowser for help on using the browser.