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