1 | """Debathena printing configuration""" |
---|
2 | |
---|
3 | |
---|
4 | import getopt |
---|
5 | import os |
---|
6 | import socket |
---|
7 | import sys |
---|
8 | import urllib |
---|
9 | |
---|
10 | import cups |
---|
11 | import hesiod |
---|
12 | |
---|
13 | |
---|
14 | _loaded = False |
---|
15 | CUPS_FRONTENDS = [ |
---|
16 | 'printers.mit.edu', |
---|
17 | 'cluster-printers.mit.edu', |
---|
18 | 'cups.mit.edu', |
---|
19 | ] |
---|
20 | CUPS_BACKENDS = [] |
---|
21 | cupsd = None |
---|
22 | |
---|
23 | |
---|
24 | SYSTEM_CUPS = 0 |
---|
25 | SYSTEM_LPRNG = 1 |
---|
26 | SYSTEMS = [SYSTEM_CUPS, SYSTEM_LPRNG] |
---|
27 | |
---|
28 | |
---|
29 | def _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 | |
---|
39 | def _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 | |
---|
53 | def error(code, message): |
---|
54 | """Exit out with an error""" |
---|
55 | sys.stderr.write(message) |
---|
56 | sys.exit(code) |
---|
57 | |
---|
58 | |
---|
59 | def 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 | |
---|
69 | def 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 | |
---|
108 | def 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 | |
---|
136 | def 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 | |
---|
154 | def 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 | |
---|
196 | def 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 | |
---|
213 | def 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 | |
---|
233 | def 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 | |
---|
301 | def 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 | ] |
---|