Install importable modules, and symlinks for command programs.
[dput.git] / dcut.py
blob850b3403609b802b82c1dfd6781a760c57db33c8
1 #! /usr/bin/python2
2 # -*- coding: utf-8; -*-
4 # 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
35 import imp
37 app_library_path = "/usr/share/dput"
38 sys.path.insert(0, app_library_path)
39 from helper import dputhelper
42 files_to_remove = []
44 progname = "dcut"
45 version = "0.2.1"
47 USAGE = """Usage: %s [options] [host] command [, command]
48 Supported options (see man page for long forms):
49 -c file Config file to parse.
50 -d Enable debug messages.
51 -h Display this help message.
52 -s Simulate the commands file creation only.
53 -v Display version information.
54 -m maintaineraddress
55 Use maintainer information in "Uploader:" field.
56 -k keyid
57 Use this keyid for signing.
58 -O file Write commands to file.
59 -U file Upload specified commands file (presently no checks).
60 -i changes
61 Upload a commands file to remove files listed in .changes.
62 Supported commands: mv, rm
63 (No paths or command-line options allowed on ftp-master.)
64 """ % (sys.argv[0])
66 validcommands = ('rm', 'cancel', 'reschedule')
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 # enable debugging very early
76 if ('-d' in sys.argv[1:] or '--debug' in sys.argv[1:]):
77 options['debug'] = 1
78 print "D: %s %s" % (progname, version)
80 # check environment for maintainer
81 if options['debug']:
82 print "D: trying to get maintainer email from environment"
84 if 'DEBEMAIL' in os.environ:
85 if os.environ['DEBEMAIL'].find('<') < 0:
86 options['uploader'] = os.environ.get("DEBFULLNAME", '')
87 if options['uploader']:
88 options['uploader'] += ' '
89 options['uploader'] += '<%s>' % (os.environ['DEBEMAIL'])
90 else:
91 options['uploader'] = os.environ['DEBEMAIL']
92 if options['debug']:
93 print "D: Uploader from env: %s" % (options['uploader'])
94 elif 'EMAIL' in os.environ:
95 if os.environ['EMAIL'].find('<') < 0:
96 options['uploader'] = os.environ.get("DEBFULLNAME", '')
97 if options['uploader']:
98 options['uploader'] += ' '
99 options['uploader'] += '<%s>' % (os.environ['EMAIL'])
100 else:
101 options['uploader'] = os.environ['EMAIL']
102 if options['debug']:
103 print "D: Uploader from env: %s" % (options['uploader'])
104 else:
105 if options['debug']:
106 print "D: Guessing uploader"
107 pwrec = pwd.getpwuid(os.getuid())
108 try:
109 s = open('/etc/mailname').read().strip()
110 except IOError:
111 s = ''
112 if not s:
113 if options['debug']:
114 print "D: Guessing uploader: /etc/mailname was a failure"
115 s = os.popen('/bin/hostname --fqdn').read().strip()
116 if s:
117 options['uploader'] = (
118 '%s <%s@%s>' % (pwrec[4].split(',')[0], pwrec[0], s))
119 if options['debug']:
120 print "D: Guessed uploader: %s" % (options['uploader'])
121 else:
122 if options['debug']:
123 print "D: Couldn't guess uploader"
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 print 'D: processing arg "%s", option "%s"' % (option, arg)
137 if option in ('-h', '--help'):
138 print USAGE
139 sys.exit(0)
140 elif option in ('-v', '--version'):
141 print progname, version
142 sys.exit(0)
143 elif option in ('-d', '--debug'):
144 options['debug'] = 1
145 elif option in ('-c', '--config'):
146 options['config'] = arg
147 elif option in ('-m', '--maintaineraddress'):
148 options['uploader'] = arg
149 elif option in ('-k', '--keyid'):
150 options['keyid'] = arg
151 elif option in ('-s', '--simulate'):
152 options['simulate'] = 1
153 elif option in ('-P', '--passive'):
154 options['passive'] = 1
155 elif option in ('-U', '--upload'):
156 options['filetoupload'] = arg
157 elif option in ('-O', '--output'):
158 options['filetocreate'] = arg
159 elif option == '--host':
160 options['host'] = arg
161 elif option in ('-i', '--input'):
162 options['changes'] = arg
163 else:
164 sys.stderr.write(
165 "%s internal error: Option %s, argument %s unknown\n"
166 % (progname, option, arg))
167 sys.exit(1)
169 if not options['host'] and arguments and arguments[0] not in validcommands:
170 options['host'] = arguments[0]
171 if options['debug']:
172 print 'D: first argument "%s" treated as host' % (options['host'])
173 del arguments[0]
175 # we don't create command files without uploader
176 if (
177 not options['uploader']
178 and (options['filetoupload'] or options['changes'])):
179 sys.stderr.write(
180 "%s error: command file cannot be created"
181 " without maintainer email\n"
182 % progname)
183 sys.stderr.write(
184 '%s please set $DEBEMAIL, $EMAIL'
185 ' or use the "-m" option\n'
186 % (len(progname) * ' '))
187 sys.exit(1)
189 return options, arguments
192 def parse_queuecommands(arguments, options, config):
193 commands = []
194 # want to consume a copy of arguments
195 arguments = arguments[:]
196 arguments.append(0)
197 curarg = []
198 while arguments:
199 if arguments[0] in validcommands:
200 curarg = [arguments[0]]
201 if arguments[0] == 'rm':
202 if len(arguments) > 1 and arguments[1] == '--nosearchdirs':
203 del arguments[1]
204 else:
205 curarg.append('--searchdirs')
206 else:
207 if not curarg and arguments[0] != 0:
208 sys.stderr.write(
209 'Error: Could not parse commands at "%s"\n'
210 % (arguments[0]))
211 sys.exit(1)
212 if str(arguments[0])[-1] in (',', ';', 0):
213 curarg.append(arguments[0][0:-1])
214 arguments[0] = ','
215 if arguments[0] in (',', ';', 0) and curarg:
216 # TV-TODO: syntax check for #args etc.
217 if options['debug']:
218 print (
219 'D: Successfully parsed command "%s"'
220 % (' '.join(curarg)))
221 commands.append(' '.join(curarg))
222 curarg = []
223 else:
224 # TV-TODO: maybe syntax check the arguments here
225 curarg.append(arguments[0])
226 del arguments[0]
227 if not commands:
228 sys.stderr.write("Error: no arguments given, see dcut -h\n")
229 sys.exit(1)
230 return commands
233 def write_commands(commands, options, config, tempdir):
234 if options['filetocreate']:
235 filename = options['filetocreate']
236 else:
237 translationorig = (
238 ''.join(map(chr, range(256)))
239 + string.ascii_letters + string.digits)
240 translationdest = 256 * '_' + string.ascii_letters + string.digits
241 uploadpartforname = string.translate(
242 options['uploader'],
243 string.maketrans(translationorig, translationdest))
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 load_dput(options):
369 file_path = os.path.join(app_library_path, "dput.py")
370 if options['debug']:
371 print "D: loading {path}".format(path=file_path)
372 module = imp.load_source("dput", file_path)
373 return module
376 def dcut():
377 options, arguments = getoptions()
378 dput = load_dput(options)
379 # dput read_configs sets dput.config
380 if options['debug']:
381 print 'D: calling dput.read_configs'
382 dput.read_configs(options['config'], options['debug'])
383 config = dput.config
384 if (
385 not options['host']
386 and config.has_option('DEFAULT', 'default_host_main')):
387 options['host'] = config.get('DEFAULT', 'default_host_main')
388 if options['debug']:
389 print 'D: Using host "%s" (default_host_main)' % (options['host'])
390 if not options['host']:
391 options['host'] = 'ftp-master'
392 if options['debug']:
393 print 'D: Using host "%s" (hardcoded)' % (options['host'])
394 tempdir = None
395 filename = None
396 try:
397 if not (options['filetoupload'] or options['filetocreate']):
398 tempdir = tempfile.mkdtemp(prefix=progname + '.')
399 if not options['filetocreate']:
400 if not options['host']:
401 print "Error: No host specified and no default found in config"
402 sys.exit(1)
403 if not config.has_section(options['host']):
404 print "No host %s found in config" % (options['host'])
405 sys.exit(1)
406 else:
407 if config.has_option(options['host'], 'allow_dcut'):
408 dcut_allowed = config.getboolean(
409 options['host'], 'allow_dcut')
410 else:
411 dcut_allowed = config.getboolean('DEFAULT', 'allow_dcut')
412 if not dcut_allowed:
413 print 'Error: dcut is not supported for this upload queue.'
414 sys.exit(1)
415 if options['filetoupload']:
416 if arguments:
417 print (
418 'Error: cannot take commands'
419 ' when uploading existing file,')
420 print ' "%s" found' % (' '.join(arguments))
421 sys.exit(1)
422 commands = None
423 filename = options['filetoupload']
424 if not filename.endswith(".commands"):
425 print 'Error: I\'m insisting on the .commands extension, which'
426 print ' "%s" doesnt seem to have.' % filename
427 # TV-TODO: check file to be readable?
428 elif options['changes']:
429 parse_changes = dput.parse_changes
430 removecommands = create_commands(options, config, parse_changes)
431 filename = write_commands(removecommands, options, config, tempdir)
432 else:
433 commands = parse_queuecommands(arguments, options, config)
434 filename = write_commands(commands, options, config, tempdir)
435 if not options['filetocreate']:
436 dput.import_upload_functions()
437 upload_methods = dput.upload_methods
438 upload_stolen_from_dput_main(
439 options['host'], upload_methods, config,
440 options['debug'], options['simulate'],
441 [filename], options['passive'])
442 finally:
443 # we use sys.exit, so we need to clean up here
444 if tempdir:
445 # file is temporary iff in tempdir
446 for filename in files_to_remove:
447 os.unlink(filename)
448 os.rmdir(tempdir)
451 def create_commands(options, config, parse_changes):
452 """ Get the removal commands from a package changes file.
454 Parse the specified ‘foo.changes’ file and returns commands to
455 remove files named in it.
458 changes_file = options['changes']
459 if options['debug']:
460 print "D: Parsing changes file (%s) for files to remove" % changes_file
461 try:
462 chg_fd = open(changes_file, 'r')
463 except IOError:
464 print "Can't open changes file: %s" % changes_file
465 sys.exit(1)
466 the_changes = parse_changes(chg_fd)
467 chg_fd.close
468 removecommands = ['rm --searchdirs ' + os.path.basename(changes_file)]
469 for file in the_changes.dict['files'].split('\n'):
470 # filename only
471 fn = string.split(file)[4]
472 rm = 'rm --searchdirs ' + fn
473 if options['debug']:
474 print "D: Will remove %s with '%s'" % (fn, rm)
475 removecommands.append(rm)
476 return removecommands
479 if __name__ == "__main__":
480 try:
481 dcut()
482 except dputhelper.DputException as e:
483 sys.stderr.write("%s\n" % e)
484 sys.exit(1)
487 # Local variables:
488 # coding: utf-8
489 # mode: python
490 # End:
491 # vim: fileencoding=utf-8 filetype=python :