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