source: trunk/debathena/config/printing-config/debathena/printing/common.py @ 24853

Revision 24853, 10.2 KB checked in by jdreed, 14 years ago (diff)
Use global keyboard correctly
Line 
1"""Debathena printing configuration"""
2
3
4import getopt
5import os
6import socket
7import sys
8import urllib
9
10import cups
11import hesiod
12
13
14_loaded = False
15CUPS_FRONTENDS = [
16    'printers.mit.edu',
17    'cluster-printers.mit.edu',
18    'cups.mit.edu',
19    ]
20CUPS_BACKENDS = []
21cupsd = None
22
23
24SYSTEM_CUPS = 0
25SYSTEM_LPRNG = 1
26SYSTEMS = [SYSTEM_CUPS, SYSTEM_LPRNG]
27
28
29def _hesiod_lookup(hes_name, hes_type):
30    """A wrapper with somewhat graceful error handling."""
31    try:
32        h = hesiod.Lookup(hes_name, hes_type)
33        if len(h.results) > 0:
34            return h.results
35    except IOError:
36        return []
37
38
39def _setup():
40    global _loaded, cupsd
41    if not _loaded:
42        CUPS_BACKENDS = [s.lower() for s in
43                         _hesiod_lookup('cups-print', 'sloc') +
44                         _hesiod_lookup('cups-cluster', 'sloc')]
45        try:
46            cupsd = cups.Connection()
47        except RuntimeError:
48            pass
49
50        _loaded = True
51
52
53def error(code, message):
54    """Exit out with an error"""
55    sys.stderr.write(message)
56    sys.exit(code)
57
58
59def get_cups_uri(printer):
60    _setup()
61    if cupsd:
62        try:
63            attrs = cupsd.getPrinterAttributes(printer)
64            return attrs.get('device-uri')
65        except cups.IPPError:
66            pass
67
68
69def parse_args(args, optinfos):
70    """Parse an argument list, given multiple ways to parse it.
71
72    The Debathena printing wrapper scripts sometimes have to support
73    multiple, independent argument sets from the different printing
74    systems' versions of commands.
75
76    parse_args tries to parse arguments with a series of different
77    argument specifiers, returning the first parse that succeeds.
78
79    The optinfos argument provides information about the various ways
80    to parse the arguments, including the order in which to attempt
81    the parses. optinfos should be a list of 2-tuples of the form
82    (opt_identifier, optinfo).
83
84    The opt_identifier from the first tuple that successfully parses
85    is included as part of the return value. optinfo is a list of
86    short options in the same format as getopt().
87
88    Args:
89      args: The argv-style argument list to parse
90      optinfos: A list of 2-tuples of the form (opt_identifier,
91        optinfo).
92
93    Returns:
94      A tuple of (opt_identifier, options, arguments), where options
95      and arguments are returned by the first run of getopt that
96      succeeds.
97    """
98
99    for opt_identifier, optinfo in optinfos:
100      try:
101          options, arguments = getopt.gnu_getopt(args, optinfo)
102          return opt_identifier, options, arguments
103      except getopt.GetoptError:
104          # That version doesn't work, so try the next one
105          continue
106
107
108def extract_opt(options, optname):
109    """Finds a particular argument and removes it.
110
111    Useful when you want to find a particular argument, extract_opt
112    looks through a list of options in the format getopt returns
113    them. It finds all instances of optnames and extracts them.
114
115    Args:
116      options: A list of options as returned from getopt (i.e. [('-P',
117        'barbar')])
118      optname: The option to extract. The option should have an
119        opening dash (i.e. '-P')
120
121    Returns:
122      A tuple of (extracted, remaining), where extracted is the list
123      of arguments that matched optname, and remaining is the list of
124      arguments that don't
125    """
126    extracted = []
127    remaining = []
128    for o, v in options:
129        if o == optname:
130            extracted.append((o, v))
131        else:
132            remaining.append((o, v))
133    return extracted, remaining
134
135
136def get_default_printer():
137    """Find and return the default printer"""
138    _setup()
139
140    if 'PRINTER' in os.environ:
141        return os.environ['PRINTER']
142
143    if cupsd:
144        default = cupsd.getDefault()
145        if default:
146            return default
147
148    for result in _hesiod_lookup(socket.getfqdn(), 'cluster'):
149        key, value = result.split(None, 1)
150        if key == 'lpr':
151            return value
152
153
154def canonicalize_queue(queue):
155    """Canonicalize local queue names to Athena queue names
156
157    If the passed-in queue name is a local print queue that bounces to
158    an Athena print queue, canonicalize to the Athena print queue.
159
160    If the queue does not exist on the default CUPS server, then
161    assume it is an already-canonicalized Athena queue.
162
163    If the queue refers to a local queue that does not bounce to an
164    Athena queue (such as a local printer), then return None
165
166    Args:
167      The name of either a local or Athena print queue
168
169    Return:
170      The name of the canonicalized Athena queue, or None if the queue
171      does not refer to an Athena queue.
172    """
173    _setup()
174    uri = get_cups_uri(queue)
175    if not uri:
176        return queue
177
178    proto = rest = hostport = path = host = port = None
179    (proto, rest) = urllib.splittype(uri)
180    if rest:
181        (hostport, path) = urllib.splithost(rest)
182    if hostport:
183        (host, port) = urllib.splitport(hostport)
184    if (proto and host and path and
185        proto == 'ipp' and
186        host.lower() in CUPS_FRONTENDS + CUPS_BACKENDS):
187        # Canonicalize the queue name to Athena's, in case someone has
188        # a local printer called something memorable like 'w20' that
189        # points to 'ajax' or something
190        if path[0:10] == '/printers/':
191            return path[10:]
192        elif path[0:9] == '/classes/':
193            return path[9:]
194
195
196def get_hesiod_print_server(queue):
197    """Find the print server for a given queue from Hesiod
198
199    Args:
200      The name of an Athena print queue
201
202    Returns:
203      The print server the queue is served by, or None if the queue
204      does not exist
205    """
206    pcap = _hesiod_lookup(queue, 'pcap')
207    if pcap:
208        for field in pcap[0].split(':'):
209            if field[0:3] == 'rm=':
210                return field[3:]
211
212
213def is_cups_server(rm):
214    """See if a host is accepting connections on port 631.
215
216    Args:
217      A hostname
218
219    Returns:
220      True if the server is accepting connections, otherwise False
221    """
222    try:
223        s = socket.socket()
224        s.settimeout(0.3)
225        s.connect((rm, 631))
226        s.close()
227
228        return True
229    except (socket.error, socket.timeout):
230        return False
231
232
233def find_queue(queue):
234    """Figure out which printing system to use for a given printer
235
236    This function makes a best effort to figure out which server and
237    which printing system should be used for printing to queue.
238
239    If a specified queue appears to be an Athena print queue, we use
240    Hesiod to determine the print server. If the print server in
241    Hesiod accepts connections on port 631, we conclude that jobs
242    should be sent to that server over CUPS. Otherwise, we assume
243    LPRng.
244
245    A queue is assumed to be an Athena print queue if it's not
246    configured in the default CUPS server. It's also assumed to be an
247    Athena print queue if the default CUPS server simply bounces jobs
248    to any of the Athena print servers.
249
250    If a queue is not an Athena print queue, then we always use the
251    default CUPS server.
252
253    Note that users might configure a local print queue pointing to an
254    Athena print queue with a different name from the Athena print
255    queue (i.e. have a w20 queue that bounces jobs to the ajax Athena
256    queue). In that scenario, we still want to send the job directly
257    to the Athena print server, but we also need to translate the
258    name. Therefore, find_queue includes the translated queue name in
259    its return values.
260
261    Args:
262      queue: The name of a print queue
263
264    Returns:
265      A tuple of (printing_system, print_server, queue_name)
266
267      printing_system is one of the PRINT_* constants in this module
268    """
269    athena_queue = canonicalize_queue(queue)
270    # If a queue isn't an Athena queue, punt straight to the default
271    # CUPS server
272    if not athena_queue:
273        return SYSTEM_CUPS, None, queue
274    queue = athena_queue
275
276    # Get rid of any instance on the queue name
277    # TODO The purpose of instances is to have different sets of default
278    # options. Queues may also have default options on the null
279    # instance. Figure out if we need to do anything about them
280    queue = queue.split('/')[0]
281
282    # If we're still here, the queue is definitely an Athena print
283    # queue; it was either in the local cupsd pointing to Athena, or the
284    # local cupsd didn't know about it.
285    # Figure out what Athena thinks the backend server is, and whether
286    # that server is running a cupsd; if not, fall back to LPRng
287
288    rm = get_hesiod_print_server(queue)
289    if not rm:
290        # In the unlikely event we're wrong about it being an Athena
291        # print queue, the local cupsd is good enough
292        return SYSTEM_CUPS, None, queue
293
294    # See if rm is running a cupsd. If not, assume it's an LPRng server.
295    if is_cups_server(rm):
296        return SYSTEM_CUPS, rm, queue
297    else:
298        return SYSTEM_LPRNG, rm, queue
299
300
301def dispatch_command(system, command, args):
302    """Dispatch a command to a printing-system-specific version of command.
303
304    Given a printing system, a command name, and a set of arguments,
305    execute the correct backend command to handle the request.
306
307    This function wraps os.execvp, so it assumes that it can terminate
308    its invoker.
309
310    Args:
311      system: A SYSTEM_* constant from this module
312      command: The non-system-specific printing command being wrapped
313      args: All arguments to pass to the command (excluding a value
314        for argv[0])
315    """
316    if system == SYSTEM_CUPS:
317        prefix = 'cups-'
318    elif system == SYSTEM_LPRNG:
319        prefix = 'mit-'
320    else:
321        error(1, '\nError: Unknown printing infrastructure\n\n')
322
323    if os.environ.get('DEBATHENA_DEBUG'):
324        sys.stderr.write('I: Running CUPS_SERVER=%s %s%s %s\n' %
325                         (os.environ.get('CUPS_SERVER', ''),
326                          prefix,
327                          command,
328                          ' '.join(args)))
329    os.execvp('%s%s' % (prefix, command), [command] + args)
330
331
332__all__ = ['SYSTEM_CUPS', 'SYSTEM_LPRNG', 'SYSTEMS'
333           'get_cups_uri',
334           'parse_args',
335           'extract_opt',
336           'extract_last_opt',
337           'get_default_printer',
338           'canonicalize_queue',
339           'get_hesiod_print_server',
340           'is_cups_server',
341           'find_queue',
342           ]
Note: See TracBrowser for help on using the repository browser.