Migrate from deprecated ‘rfc822’ module to current ‘email.parser’ module.
[dput.git] / dput / dcut.py
blob8e74f87c759e1baa2b0cb3bbb3aa8b566e05692d
1 #! /usr/bin/python2
2 # -*- coding: utf-8; -*-
4 # dput/dcut.py
5 # Part of ‘dput’, a Debian package upload toolkit.
7 # Copyright © 2015 Ben Finney <ben+debian@benfinney.id.au>
8 # Copyright © 2008–2013 Y Giridhar Appaji Nag <appaji@debian.org>
9 # Copyright © 2004–2009 Thomas Viehmann <tv@beamnet.de>
10 # Copyright © 2000–2004 Christian Kurz <shorty@debian.org>
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # You should have received a copy of the GNU General Public License along
23 # with this program; if not, write to the Free Software Foundation, Inc.,
24 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 """ dcut — Debian command upload tool. """
28 import sys
29 import os
30 import tempfile
31 import string
32 import time
33 import subprocess
34 import pwd
36 from .helper import dputhelper
37 from . import dput
40 files_to_remove = []
42 progname = "dcut"
43 version = "0.2.1"
45 USAGE = """Usage: %s [options] [host] command [, command]
46 Supported options (see man page for long forms):
47 -c file Config file to parse.
48 -d Enable debug messages.
49 -h Display this help message.
50 -s Simulate the commands file creation only.
51 -v Display version information.
52 -m maintaineraddress
53 Use maintainer information in "Uploader:" field.
54 -k keyid
55 Use this keyid for signing.
56 -O file Write commands to file.
57 -U file Upload specified commands file (presently no checks).
58 -i changes
59 Upload a commands file to remove files listed in .changes.
60 Supported commands: mv, rm
61 (No paths or command-line options allowed on ftp-master.)
62 """ % (sys.argv[0])
64 validcommands = ('rm', 'cancel', 'reschedule')
67 def getoptions():
68 # seed some defaults
69 options = {
70 'debug': 0, 'simulate': 0, 'config': None, 'host': None,
71 'uploader': None, 'keyid': None, 'passive': 0,
72 'filetocreate': None, 'filetoupload': None, 'changes': None}
73 # enable debugging very early
74 if ('-d' in sys.argv[1:] or '--debug' in sys.argv[1:]):
75 options['debug'] = 1
76 print "D: %s %s" % (progname, version)
78 # check environment for maintainer
79 if options['debug']:
80 print "D: trying to get maintainer email from environment"
82 if 'DEBEMAIL' in os.environ:
83 if os.environ['DEBEMAIL'].find('<') < 0:
84 options['uploader'] = os.environ.get("DEBFULLNAME", '')
85 if options['uploader']:
86 options['uploader'] += ' '
87 options['uploader'] += '<%s>' % (os.environ['DEBEMAIL'])
88 else:
89 options['uploader'] = os.environ['DEBEMAIL']
90 if options['debug']:
91 print "D: Uploader from env: %s" % (options['uploader'])
92 elif 'EMAIL' in os.environ:
93 if os.environ['EMAIL'].find('<') < 0:
94 options['uploader'] = os.environ.get("DEBFULLNAME", '')
95 if options['uploader']:
96 options['uploader'] += ' '
97 options['uploader'] += '<%s>' % (os.environ['EMAIL'])
98 else:
99 options['uploader'] = os.environ['EMAIL']
100 if options['debug']:
101 print "D: Uploader from env: %s" % (options['uploader'])
102 else:
103 if options['debug']:
104 print "D: Guessing uploader"
105 pwrec = pwd.getpwuid(os.getuid())
106 try:
107 s = open('/etc/mailname').read().strip()
108 except IOError:
109 s = ''
110 if not s:
111 if options['debug']:
112 print "D: Guessing uploader: /etc/mailname was a failure"
113 s = os.popen('/bin/hostname --fqdn').read().strip()
114 if s:
115 options['uploader'] = (
116 '%s <%s@%s>' % (pwrec[4].split(',')[0], pwrec[0], s))
117 if options['debug']:
118 print "D: Guessed uploader: %s" % (options['uploader'])
119 else:
120 if options['debug']:
121 print "D: Couldn't guess uploader"
122 # parse command line arguments
123 (opts, arguments) = dputhelper.getopt(
124 sys.argv[1:],
125 'c:dDhsvm:k:PU:O:i:', [
126 'config=', 'debug',
127 'help', 'simulate', 'version', 'host=',
128 'maintainteraddress=', 'keyid=',
129 'passive', 'upload=', 'output=', 'input='
132 for (option, arg) in opts:
133 if options['debug']:
134 print 'D: processing arg "%s", option "%s"' % (option, arg)
135 if option in ('-h', '--help'):
136 print USAGE
137 sys.exit(0)
138 elif option in ('-v', '--version'):
139 print progname, version
140 sys.exit(0)
141 elif option in ('-d', '--debug'):
142 options['debug'] = 1
143 elif option in ('-c', '--config'):
144 options['config'] = arg
145 elif option in ('-m', '--maintaineraddress'):
146 options['uploader'] = arg
147 elif option in ('-k', '--keyid'):
148 options['keyid'] = arg
149 elif option in ('-s', '--simulate'):
150 options['simulate'] = 1
151 elif option in ('-P', '--passive'):
152 options['passive'] = 1
153 elif option in ('-U', '--upload'):
154 options['filetoupload'] = arg
155 elif option in ('-O', '--output'):
156 options['filetocreate'] = arg
157 elif option == '--host':
158 options['host'] = arg
159 elif option in ('-i', '--input'):
160 options['changes'] = arg
161 else:
162 sys.stderr.write(
163 "%s internal error: Option %s, argument %s unknown\n"
164 % (progname, option, arg))
165 sys.exit(1)
167 if not options['host'] and arguments and arguments[0] not in validcommands:
168 options['host'] = arguments[0]
169 if options['debug']:
170 print 'D: first argument "%s" treated as host' % (options['host'])
171 del arguments[0]
173 # we don't create command files without uploader
174 if (
175 not options['uploader']
176 and (options['filetoupload'] or options['changes'])):
177 sys.stderr.write(
178 "%s error: command file cannot be created"
179 " without maintainer email\n"
180 % progname)
181 sys.stderr.write(
182 '%s please set $DEBEMAIL, $EMAIL'
183 ' or use the "-m" option\n'
184 % (len(progname) * ' '))
185 sys.exit(1)
187 return options, arguments
190 def parse_queuecommands(arguments, options, config):
191 commands = []
192 # want to consume a copy of arguments
193 arguments = arguments[:]
194 arguments.append(0)
195 curarg = []
196 while arguments:
197 if arguments[0] in validcommands:
198 curarg = [arguments[0]]
199 if arguments[0] == 'rm':
200 if len(arguments) > 1 and arguments[1] == '--nosearchdirs':
201 del arguments[1]
202 else:
203 curarg.append('--searchdirs')
204 else:
205 if not curarg and arguments[0] != 0:
206 sys.stderr.write(
207 'Error: Could not parse commands at "%s"\n'
208 % (arguments[0]))
209 sys.exit(1)
210 if str(arguments[0])[-1] in (',', ';', 0):
211 curarg.append(arguments[0][0:-1])
212 arguments[0] = ','
213 if arguments[0] in (',', ';', 0) and curarg:
214 # TV-TODO: syntax check for #args etc.
215 if options['debug']:
216 print (
217 'D: Successfully parsed command "%s"'
218 % (' '.join(curarg)))
219 commands.append(' '.join(curarg))
220 curarg = []
221 else:
222 # TV-TODO: maybe syntax check the arguments here
223 curarg.append(arguments[0])
224 del arguments[0]
225 if not commands:
226 sys.stderr.write("Error: no arguments given, see dcut -h\n")
227 sys.exit(1)
228 return commands
231 def write_commands(commands, options, config, tempdir):
232 if options['filetocreate']:
233 filename = options['filetocreate']
234 else:
235 translationorig = (
236 ''.join(map(chr, range(256)))
237 + string.ascii_letters + string.digits)
238 translationdest = 256 * '_' + string.ascii_letters + string.digits
239 translationmap = {
240 ord(orig_char): ord(dest_char)
241 for (orig_char, dest_char)
242 in zip(translationorig, translationdest)}
243 uploadpartforname = options['uploader'].translate(translationmap)
244 filename = (
245 progname + '.%s.%d.%d.commands' %
246 (uploadpartforname, int(time.time()), os.getpid()))
247 if tempdir:
248 filename = os.path.join(tempdir, filename)
249 files_to_remove.append(filename)
250 f = open(filename, "w")
251 f.write("Uploader: %s\n" % options['uploader'])
252 f.write("Commands:\n %s\n\n" % ('\n '.join(commands)))
253 f.close()
254 debsign_cmdline = ['debsign']
255 debsign_cmdline.append('-m%s' % options['uploader'])
256 if options['keyid']:
257 debsign_cmdline.append('-k%s' % options['keyid'])
258 debsign_cmdline.append('%s' % filename)
259 if options['debug']:
260 print "D: calling debsign:", debsign_cmdline
261 debsign_prog = subprocess.Popen(debsign_cmdline)
262 if os.waitpid(debsign_prog.pid, 0)[1]:
263 sys.stderr.write("Error: debsign failed.\n")
264 sys.exit(1)
265 return filename
268 def upload_stolen_from_dput_main(
269 host, upload_methods, config, debug, simulate,
270 files_to_upload, ftp_passive_mode):
271 # Check the upload methods that we have as default and per host
272 if debug:
273 print "D: Default Method: %s" % config.get('DEFAULT', 'method')
274 if config.get('DEFAULT', 'method') not in upload_methods:
275 sys.stderr.write(
276 "Unknown upload method: %s\n"
277 % config.get('DEFAULT', 'method'))
278 sys.exit(1)
279 if debug:
280 print "D: Host Method: %s" % config.get(host, 'method')
281 if config.get(host, 'method') not in upload_methods:
282 sys.stderr.write(
283 "Unknown upload method: %s\n" % config.get(host, 'method'))
284 sys.exit(1)
286 # Inspect the Config and set appropriate upload method
287 if not config.get(host, 'method'):
288 method = config.get('DEFAULT', 'method')
289 else:
290 method = config.get(host, 'method')
292 # Check now the login and redefine it if needed
293 if (
294 config.has_option(host, 'login')
295 and config.get(host, 'login') != 'username'):
296 login = config.get(host, 'login')
297 elif (
298 config.has_option('DEFAULT', 'login')
299 and config.get('DEFAULT', 'login') != 'username'):
300 login = config.get('DEFAULT', 'login')
301 else:
302 # Try to get the login from the enviroment
303 if 'USER' in os.environ:
304 login = os.environ['USER']
305 else:
306 print "$USER not set, will use login information."
307 # Else use the current username
308 login = pwd.getpwuid(os.getuid())[0]
309 if debug:
310 print "D: User-ID: %s" % os.getuid()
311 if debug:
312 print (
313 "D: Neither host %s nor default login used. Using %s"
314 % (host, login))
315 if debug:
316 print "D: Login to use: %s" % login
318 # Messy, yes. But it isn't referenced by the upload method anyway.
319 if config.get(host, 'method') == 'local':
320 fqdn = 'localhost'
321 else:
322 fqdn = config.get(host, 'fqdn')
323 incoming = config.get(host, 'incoming')
324 # Do the actual upload
325 if not simulate:
326 if debug:
327 print "D: FQDN: %s" % fqdn
328 print "D: Login: %s" % login
329 print "D: Incoming: %s" % incoming
330 if method == 'ftp':
331 ftp_mode = config.getboolean(host, 'passive_ftp')
332 if ftp_passive_mode == 1:
333 ftp_mode = 1
334 if ftp_mode == 1:
335 if debug:
336 if ftp_passive_mode == 1:
337 print "D: Using passive ftp"
338 else:
339 print "D: Using active ftp"
340 upload_methods[method](
341 fqdn, login, incoming,
342 files_to_upload, debug, ftp_mode)
343 elif method == 'scp':
344 if debug and config.getboolean(host, 'scp_compress'):
345 print "D: Setting compression for scp"
346 scp_compress = config.getboolean(host, 'scp_compress')
347 ssh_config_options = [
348 y for y in (
349 x.strip() for x in
350 config.get(host, 'ssh_config_options').split('\n'))
351 if y]
352 upload_methods[method](
353 fqdn, login, incoming,
354 files_to_upload, debug, scp_compress, ssh_config_options)
355 else:
356 upload_methods[method](
357 fqdn, login, incoming,
358 files_to_upload, debug, 0)
359 # Or just simulate it.
360 else:
361 for file in files_to_upload:
362 sys.stderr.write(
363 "Uploading with %s: %s to %s:%s\n"
364 % (method, file, fqdn, incoming))
365 os.system("cat %s" % file)
368 def dcut():
369 options, arguments = getoptions()
370 # dput read_configs sets dput.config
371 if options['debug']:
372 print 'D: calling dput.read_configs'
373 dput.read_configs(options['config'], options['debug'])
374 config = dput.config
375 if (
376 not options['host']
377 and config.has_option('DEFAULT', 'default_host_main')):
378 options['host'] = config.get('DEFAULT', 'default_host_main')
379 if options['debug']:
380 print 'D: Using host "%s" (default_host_main)' % (options['host'])
381 if not options['host']:
382 options['host'] = 'ftp-master'
383 if options['debug']:
384 print 'D: Using host "%s" (hardcoded)' % (options['host'])
385 tempdir = None
386 filename = None
387 try:
388 if not (options['filetoupload'] or options['filetocreate']):
389 tempdir = tempfile.mkdtemp(prefix=progname + '.')
390 if not options['filetocreate']:
391 if not options['host']:
392 print "Error: No host specified and no default found in config"
393 sys.exit(1)
394 if not config.has_section(options['host']):
395 print "No host %s found in config" % (options['host'])
396 sys.exit(1)
397 else:
398 if config.has_option(options['host'], 'allow_dcut'):
399 dcut_allowed = config.getboolean(
400 options['host'], 'allow_dcut')
401 else:
402 dcut_allowed = config.getboolean('DEFAULT', 'allow_dcut')
403 if not dcut_allowed:
404 print 'Error: dcut is not supported for this upload queue.'
405 sys.exit(1)
406 if options['filetoupload']:
407 if arguments:
408 print (
409 'Error: cannot take commands'
410 ' when uploading existing file,')
411 print ' "%s" found' % (' '.join(arguments))
412 sys.exit(1)
413 commands = None
414 filename = options['filetoupload']
415 if not filename.endswith(".commands"):
416 print 'Error: I\'m insisting on the .commands extension, which'
417 print ' "%s" doesnt seem to have.' % filename
418 # TV-TODO: check file to be readable?
419 elif options['changes']:
420 parse_changes = dput.parse_changes
421 removecommands = create_commands(options, config, parse_changes)
422 filename = write_commands(removecommands, options, config, tempdir)
423 else:
424 commands = parse_queuecommands(arguments, options, config)
425 filename = write_commands(commands, options, config, tempdir)
426 if not options['filetocreate']:
427 dput.import_upload_functions()
428 upload_methods = dput.upload_methods
429 upload_stolen_from_dput_main(
430 options['host'], upload_methods, config,
431 options['debug'], options['simulate'],
432 [filename], options['passive'])
433 finally:
434 # we use sys.exit, so we need to clean up here
435 if tempdir:
436 # file is temporary iff in tempdir
437 for filename in files_to_remove:
438 os.unlink(filename)
439 os.rmdir(tempdir)
442 def create_commands(options, config, parse_changes):
443 """ Get the removal commands from a package changes file.
445 Parse the specified ‘foo.changes’ file and returns commands to
446 remove files named in it.
449 changes_file = options['changes']
450 if options['debug']:
451 print "D: Parsing changes file (%s) for files to remove" % changes_file
452 try:
453 chg_fd = open(changes_file, 'r')
454 except IOError:
455 print "Can't open changes file: %s" % changes_file
456 sys.exit(1)
457 the_changes = parse_changes(chg_fd)
458 chg_fd.close
459 removecommands = ['rm --searchdirs ' + os.path.basename(changes_file)]
460 for file in the_changes['files'].strip().split('\n'):
461 # filename only
462 fn = file.split()[4]
463 rm = 'rm --searchdirs ' + fn
464 if options['debug']:
465 print "D: Will remove %s with '%s'" % (fn, rm)
466 removecommands.append(rm)
467 return removecommands
470 if __name__ == "__main__":
471 try:
472 dcut()
473 except dputhelper.DputException as e:
474 sys.stderr.write("%s\n" % e)
475 sys.exit(1)
478 # Local variables:
479 # coding: utf-8
480 # mode: python
481 # End:
482 # vim: fileencoding=utf-8 filetype=python :