2 # -*- coding: utf-8; -*-
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. """
36 from .helper
import dputhelper
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.
53 Use maintainer information in "Uploader:" field.
55 Use this keyid for signing.
56 -O file Write commands to file.
57 -U file Upload specified commands file (presently no checks).
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.)
64 validcommands
= ('rm', 'cancel', 'reschedule')
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:]):
76 print "D: %s %s" % (progname
, version
)
78 # check environment for maintainer
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'])
89 options
['uploader'] = os
.environ
['DEBEMAIL']
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'])
99 options
['uploader'] = os
.environ
['EMAIL']
101 print "D: Uploader from env: %s" % (options
['uploader'])
104 print "D: Guessing uploader"
105 pwrec
= pwd
.getpwuid(os
.getuid())
107 s
= open('/etc/mailname').read().strip()
112 print "D: Guessing uploader: /etc/mailname was a failure"
113 s
= os
.popen('/bin/hostname --fqdn').read().strip()
115 options
['uploader'] = (
116 '%s <%s@%s>' % (pwrec
[4].split(',')[0], pwrec
[0], s
))
118 print "D: Guessed uploader: %s" % (options
['uploader'])
121 print "D: Couldn't guess uploader"
122 # parse command line arguments
123 (opts
, arguments
) = dputhelper
.getopt(
125 'c:dDhsvm:k:PU:O:i:', [
127 'help', 'simulate', 'version', 'host=',
128 'maintainteraddress=', 'keyid=',
129 'passive', 'upload=', 'output=', 'input='
132 for (option
, arg
) in opts
:
134 print 'D: processing arg "%s", option "%s"' % (option
, arg
)
135 if option
in ('-h', '--help'):
138 elif option
in ('-v', '--version'):
139 print progname
, version
141 elif option
in ('-d', '--debug'):
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
163 "%s internal error: Option %s, argument %s unknown\n"
164 % (progname
, option
, arg
))
167 if not options
['host'] and arguments
and arguments
[0] not in validcommands
:
168 options
['host'] = arguments
[0]
170 print 'D: first argument "%s" treated as host' % (options
['host'])
173 # we don't create command files without uploader
175 not options
['uploader']
176 and (options
['filetoupload'] or options
['changes'])):
178 "%s error: command file cannot be created"
179 " without maintainer email\n"
182 '%s please set $DEBEMAIL, $EMAIL'
183 ' or use the "-m" option\n'
184 % (len(progname
) * ' '))
187 return options
, arguments
190 def parse_queuecommands(arguments
, options
, config
):
192 # want to consume a copy of arguments
193 arguments
= 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':
203 curarg
.append('--searchdirs')
205 if not curarg
and arguments
[0] != 0:
207 'Error: Could not parse commands at "%s"\n'
210 if str(arguments
[0])[-1] in (',', ';', 0):
211 curarg
.append(arguments
[0][0:-1])
213 if arguments
[0] in (',', ';', 0) and curarg
:
214 # TV-TODO: syntax check for #args etc.
217 'D: Successfully parsed command "%s"'
218 % (' '.join(curarg
)))
219 commands
.append(' '.join(curarg
))
222 # TV-TODO: maybe syntax check the arguments here
223 curarg
.append(arguments
[0])
226 sys
.stderr
.write("Error: no arguments given, see dcut -h\n")
231 def write_commands(commands
, options
, config
, tempdir
):
232 if options
['filetocreate']:
233 filename
= options
['filetocreate']
236 ''.join(map(chr, range(256)))
237 + string
.ascii_letters
+ string
.digits
)
238 translationdest
= 256 * '_' + string
.ascii_letters
+ string
.digits
240 ord(orig_char
): ord(dest_char
)
241 for (orig_char
, dest_char
)
242 in zip(translationorig
, translationdest
)}
243 uploadpartforname
= options
['uploader'].translate(translationmap
)
245 progname
+ '.%s.%d.%d.commands' %
246 (uploadpartforname
, int(time
.time()), os
.getpid()))
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
)))
254 debsign_cmdline
= ['debsign']
255 debsign_cmdline
.append('-m%s' % options
['uploader'])
257 debsign_cmdline
.append('-k%s' % options
['keyid'])
258 debsign_cmdline
.append('%s' % filename
)
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")
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
273 print "D: Default Method: %s" % config
.get('DEFAULT', 'method')
274 if config
.get('DEFAULT', 'method') not in upload_methods
:
276 "Unknown upload method: %s\n"
277 % config
.get('DEFAULT', 'method'))
280 print "D: Host Method: %s" % config
.get(host
, 'method')
281 if config
.get(host
, 'method') not in upload_methods
:
283 "Unknown upload method: %s\n" % config
.get(host
, 'method'))
286 # Inspect the Config and set appropriate upload method
287 if not config
.get(host
, 'method'):
288 method
= config
.get('DEFAULT', 'method')
290 method
= config
.get(host
, 'method')
292 # Check now the login and redefine it if needed
294 config
.has_option(host
, 'login')
295 and config
.get(host
, 'login') != 'username'):
296 login
= config
.get(host
, 'login')
298 config
.has_option('DEFAULT', 'login')
299 and config
.get('DEFAULT', 'login') != 'username'):
300 login
= config
.get('DEFAULT', 'login')
302 # Try to get the login from the enviroment
303 if 'USER' in os
.environ
:
304 login
= os
.environ
['USER']
306 print "$USER not set, will use login information."
307 # Else use the current username
308 login
= pwd
.getpwuid(os
.getuid())[0]
310 print "D: User-ID: %s" % os
.getuid()
313 "D: Neither host %s nor default login used. Using %s"
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':
322 fqdn
= config
.get(host
, 'fqdn')
323 incoming
= config
.get(host
, 'incoming')
324 # Do the actual upload
327 print "D: FQDN: %s" % fqdn
328 print "D: Login: %s" % login
329 print "D: Incoming: %s" % incoming
331 ftp_mode
= config
.getboolean(host
, 'passive_ftp')
332 if ftp_passive_mode
== 1:
336 if ftp_passive_mode
== 1:
337 print "D: Using passive ftp"
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
= [
350 config
.get(host
, 'ssh_config_options').split('\n'))
352 upload_methods
[method
](
353 fqdn
, login
, incoming
,
354 files_to_upload
, debug
, scp_compress
, ssh_config_options
)
356 upload_methods
[method
](
357 fqdn
, login
, incoming
,
358 files_to_upload
, debug
, 0)
359 # Or just simulate it.
361 for file in files_to_upload
:
363 "Uploading with %s: %s to %s:%s\n"
364 % (method
, file, fqdn
, incoming
))
365 os
.system("cat %s" % file)
369 options
, arguments
= getoptions()
370 # dput read_configs sets dput.config
372 print 'D: calling dput.read_configs'
373 dput
.read_configs(options
['config'], options
['debug'])
377 and config
.has_option('DEFAULT', 'default_host_main')):
378 options
['host'] = config
.get('DEFAULT', 'default_host_main')
380 print 'D: Using host "%s" (default_host_main)' % (options
['host'])
381 if not options
['host']:
382 options
['host'] = 'ftp-master'
384 print 'D: Using host "%s" (hardcoded)' % (options
['host'])
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"
394 if not config
.has_section(options
['host']):
395 print "No host %s found in config" % (options
['host'])
398 if config
.has_option(options
['host'], 'allow_dcut'):
399 dcut_allowed
= config
.getboolean(
400 options
['host'], 'allow_dcut')
402 dcut_allowed
= config
.getboolean('DEFAULT', 'allow_dcut')
404 print 'Error: dcut is not supported for this upload queue.'
406 if options
['filetoupload']:
409 'Error: cannot take commands'
410 ' when uploading existing file,')
411 print ' "%s" found' % (' '.join(arguments
))
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
)
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'])
434 # we use sys.exit, so we need to clean up here
436 # file is temporary iff in tempdir
437 for filename
in files_to_remove
:
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']
451 print "D: Parsing changes file (%s) for files to remove" % changes_file
453 chg_fd
= open(changes_file
, 'r')
455 print "Can't open changes file: %s" % changes_file
457 the_changes
= parse_changes(chg_fd
)
459 removecommands
= ['rm --searchdirs ' + os
.path
.basename(changes_file
)]
460 for file in the_changes
['files'].strip().split('\n'):
463 rm
= 'rm --searchdirs ' + fn
465 print "D: Will remove %s with '%s'" % (fn
, rm
)
466 removecommands
.append(rm
)
467 return removecommands
470 if __name__
== "__main__":
473 except dputhelper
.DputException
as e
:
474 sys
.stderr
.write("%s\n" % e
)
482 # vim: fileencoding=utf-8 filetype=python :