root / pykota / trunk / bin / pkturnkey @ 3433

Revision 3433, 21.6 kB (checked in by jerome, 15 years ago)

Now pkturnkey supports new style command line handling code.

  • 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
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
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            hasV4 = False
166            try :
167                from pysnmp.asn1.encoding.ber.error import TypeMismatchError
168                from pysnmp.mapping.udp.role import Manager
169                from pysnmp.proto.api import alpha
170            except ImportError :
171                logerr("pysnmp doesn't seem to be installed. SNMP checks will be ignored !\n")
172                return False
173        else :
174            hasV4 = True
175
176        if hasV4 :
177            def retrieveSNMPValues(hostname, community) :
178                """Retrieves a printer's internal page counter and status via SNMP."""
179                errorIndication, errorStatus, errorIndex, varBinds = \
180                     cmdgen.CommandGenerator().getCmd(cmdgen.CommunityData("pykota", community, 0), \
181                                                      cmdgen.UdpTransportTarget((hostname, 161)), \
182                                                      tuple([int(i) for i in pageCounterOID.split('.')]))
183                if errorIndication :
184                    raise "No SNMP !"
185                elif errorStatus :
186                    raise "No SNMP !"
187                else :
188                    self.SNMPOK = True
189        else :
190            def retrieveSNMPValues(hostname, community) :
191                """Retrieves a printer's internal page counter and status via SNMP."""
192                ver = alpha.protoVersions[alpha.protoVersionId1]
193                req = ver.Message()
194                req.apiAlphaSetCommunity(community)
195                req.apiAlphaSetPdu(ver.GetRequestPdu())
196                req.apiAlphaGetPdu().apiAlphaSetVarBindList((pageCounterOID, ver.Null()))
197                tsp = Manager()
198                try :
199                    tsp.sendAndReceive(req.berEncode(), \
200                                       (hostname, 161), \
201                                       (handleAnswer, req))
202                except :
203                    raise "No SNMP !"
204                tsp.close()
205
206            def handleAnswer(wholemsg, notusedhere, req):
207                """Decodes and handles the SNMP answer."""
208                ver = alpha.protoVersions[alpha.protoVersionId1]
209                rsp = ver.Message()
210                try :
211                    rsp.berDecode(wholemsg)
212                except TypeMismatchError, msg :
213                    raise "No SNMP !"
214                else :
215                    if req.apiAlphaMatch(rsp):
216                        errorStatus = rsp.apiAlphaGetPdu().apiAlphaGetErrorStatus()
217                        if errorStatus:
218                            raise "No SNMP !"
219                        else:
220                            self.values = []
221                            for varBind in rsp.apiAlphaGetPdu().apiAlphaGetVarBindList():
222                                self.values.append(varBind.apiAlphaGetOidVal()[1].rawAsn1Value)
223                            try :
224                                pagecounter = self.values[0]
225                            except :
226                                raise "No SNMP !"
227                            else :
228                                self.SNMPOK = True
229                                return True
230
231        self.SNMPOK = False
232        try :
233            retrieveSNMPValues(hostname, community)
234        except :
235            self.SNMPOK = False
236        return self.SNMPOK
237
238    def supportsPJL(self, hostname, port) :
239        """Returns 1 if the printer accepts PJL queries over TCP, else 0."""
240        def alarmHandler(signum, frame) :
241            raise "Timeout !"
242
243        pjlsupport = False
244        signal.signal(signal.SIGALRM, alarmHandler)
245        signal.alarm(2) # wait at most 2 seconds
246        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
247        try :
248            s.connect((hostname, port))
249            s.send("\033%-12345X@PJL INFO STATUS\r\n\033%-12345X")
250            answer = s.recv(1024)
251            if not answer.startswith("@PJL") :
252                raise "No PJL !"
253        except :
254            pass
255        else :
256            pjlsupport = True
257        s.close()
258        signal.alarm(0)
259        signal.signal(signal.SIGALRM, signal.SIG_IGN)
260        return pjlsupport
261
262    def hintConfig(self, printers) :
263        """Gives some hints about what to put into pykota.conf"""
264        if not printers :
265            return
266        sys.stderr.flush() # ensure outputs don't mix
267        self.display("\n--- CUT ---\n")
268        self.display("# Here are some lines that we suggest you add at the end\n")
269        self.display("# of the pykota.conf file. These lines gives possible\n")
270        self.display("# values for the way print jobs' size will be computed.\n")
271        self.display("# NB : it is possible that a manual configuration gives\n")
272        self.display("# better results for you. As always, your mileage may vary.\n")
273        self.display("#\n")
274        for (name, uri) in printers :
275            self.display("[%s]\n" % name)
276            accounter = "software()"
277            try :
278                uri = uri.split("cupspykota:", 2)[-1]
279            except (ValueError, IndexError) :
280                pass
281            else :
282                while uri and uri.startswith("/") :
283                    uri = uri[1:]
284                try :
285                    (backend, destination) = uri.split(":", 1)
286                    if backend not in ("ipp", "http", "https", "lpd", "socket") :
287                        raise ValueError
288                except ValueError :
289                    pass
290                else :
291                    while destination.startswith("/") :
292                        destination = destination[1:]
293                    checkauth = destination.split("@", 1)
294                    if len(checkauth) == 2 :
295                        destination = checkauth[1]
296                    parts = destination.split("/")[0].split(":")
297                    if len(parts) == 2 :
298                        (hostname, port) = parts
299                        try :
300                            port = int(port)
301                        except ValueError :
302                            port = 9100
303                    else :
304                        (hostname, port) = parts[0], 9100
305
306                    if self.supportsSNMP(hostname, "public") :
307                        accounter = "hardware(snmp)"
308                    elif self.supportsPJL(hostname, 9100) :
309                        accounter = "hardware(pjl)"
310                    elif self.supportsPJL(hostname, 9101) :
311                        accounter = "hardware(pjl:9101)"
312                    elif self.supportsPJL(hostname, port) :
313                        accounter = "hardware(pjl:%s)" % port
314
315            self.display("preaccounter : software()\n")
316            self.display("accounter : %s\n" % accounter)
317            self.display("\n")
318        self.display("--- CUT ---\n")
319
320    def main(self, names, options) :
321        """Intializes PyKota's database."""
322        self.adminOnly()
323
324        if options.uidmin or options.uidmax :
325            if not options.dousers :
326                self.printInfo(_("The --uidmin or --uidmax command line option implies --dousers as well."), "warn")
327            options.dousers = True
328
329        if options.gidmin or options.gidmax :
330            if not options.dogroups :
331                self.printInfo(_("The --gidmin or --gidmax command line option implies --dogroups as well."), "warn")
332            options.dogroups = True
333
334        if options.dogroups :
335            if not options.dousers :
336                self.printInfo(_("The --dogroups command line option implies --dousers as well."), "warn")
337            options.dousers = True
338
339        if not names :
340            names = [u"*"]
341
342        self.printInfo(_("Please be patient..."))
343        dryrun = not options.force
344        if dryrun :
345            self.printInfo(_("Don't worry, the database WILL NOT BE MODIFIED."))
346        else :
347            self.printInfo(_("Please WORRY NOW, the database WILL BE MODIFIED."))
348
349        if options.dousers :
350            if not options.uidmin :
351                self.printInfo(_("System users will have a print account as well !"), "warn")
352                uidmin = 0
353            else :
354                try :
355                    uidmin = int(options.uidmin)
356                except :
357                    try :
358                        uidmin = pwd.getpwnam(options.uidmin).pw_uid
359                    except KeyError, msg :
360                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
361                                                   % (options.uidmin, msg)
362
363            if not options.uidmax :
364                uidmax = sys.maxint
365            else :
366                try :
367                    uidmax = int(options.uidmax)
368                except :
369                    try :
370                        uidmax = pwd.getpwnam(options.uidmax).pw_uid
371                    except KeyError, msg :
372                        raise PyKotaCommandLineError, _("Unknown username %s : %s") \
373                                                   % (options.uidmax, msg)
374
375            if uidmin > uidmax :
376                (uidmin, uidmax) = (uidmax, uidmin)
377            users = self.listUsers(uidmin, uidmax)
378        else :
379            users = []
380
381        if options.dogroups :
382            if not options.gidmin :
383                self.printInfo(_("System groups will have a print account as well !"), "warn")
384                gidmin = 0
385            else :
386                try :
387                    gidmin = int(options.gidmin)
388                except :
389                    try :
390                        gidmin = grp.getgrnam(options.gidmin).gr_gid
391                    except KeyError, msg :
392                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
393                                                   % (options.gidmin, msg)
394
395            if not options.gidmax :
396                gidmax = sys.maxint
397            else :
398                try :
399                    gidmax = int(options.gidmax)
400                except :
401                    try :
402                        gidmax = grp.getgrnam(options.gidmax).gr_gid
403                    except KeyError, msg :
404                        raise PyKotaCommandLineError, _("Unknown groupname %s : %s") \
405                                                   % (options.gidmax, msg)
406
407            if gidmin > gidmax :
408                (gidmin, gidmax) = (gidmax, gidmin)
409            groups = self.listGroups(gidmin, gidmax, users)
410            if not options.emptygroups :
411                for (groupname, members) in groups.items() :
412                    if not members :
413                        del groups[groupname]
414        else :
415            groups = []
416
417        printers = self.listPrinters(names)
418        if printers :
419            self.createPrinters(printers, dryrun)
420            self.createUsers([entry[0] for entry in users], printers, dryrun)
421            self.createGroups(groups, printers, dryrun)
422
423        if dryrun :
424            self.printInfo(_("Simulation terminated."))
425        else :
426            self.printInfo(_("Database initialized !"))
427
428        if options.doconf :
429            self.hintConfig(printers)
430
431
432if __name__ == "__main__" :
433    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."),
434                                usage="pkturnkey [options] printer1 printer2 ... printerN")
435    parser.add_option("-c", "--doconf",
436                            action="store_true",
437                            dest="doconf",
438                            help=_("Try to autodetect the best print accounting settings for existing CUPS printers. All printers must be switched ON beforehand."))
439    parser.add_option("-d", "--dousers",
440                            action="store_true",
441                            dest="dousers",
442                            help=_("Create accounts for users, and allocate print quota entries for them."))
443    parser.add_option("-D", "--dogroups",
444                            action="store_true",
445                            dest="dogroups",
446                            help=_("Create accounts for users groups, and allocate print quota entries for them."))
447    parser.add_option("-e", "--emptygroups",
448                            action="store_true",
449                            dest="emptygroups",
450                            help=_("Also include groups which don't have any member."))
451    parser.add_option("-f", "--force",
452                            action="store_true",
453                            dest="force",
454                            help=_("Modifies PyKota's database content for real, instead of faking it (for safety reasons)."))
455    parser.add_option("-u", "--uidmin",
456                            dest="uidmin",
457                            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."))
458    parser.add_option("-U", "--uidmax",
459                            dest="uidmax",
460                            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."))
461    parser.add_option("-g", "--gidmin",
462                            dest="gidmin",
463                            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."))
464    parser.add_option("-G", "--gidmax",
465                            dest="gidmax",
466                            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."))
467
468    parser.add_example("--dousers --uidmin jerome HPLASER1 HPLASER2",
469                       _("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'."))
470    parser.add_example("--force --dousers --uidmin jerome HPLASER1 HPLASER2",
471                       _("Would do the same as the example above, but for real. Please take great care when using the --force command line option."))
472    parser.add_example("--doconf",
473                       _("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"))
474    run(parser, PKTurnKey)
Note: See TracBrowser for help on using the browser.