#!/usr/bin/python # -*- coding: utf-8 -*- # # (C) 2007 Canonical Ltd., Steve Kowalik # Authors: # Martin Pitt <martin.pitt@ubuntu.com> # Steve Kowalik <stevenk@ubuntu.com> # Michael Bienia <geser@ubuntu.com> (python-launchpad-bugs support) # Daniel Hahler <ubuntu@thequod.de> # Iain Lane <iain@orangesquash.org.uk> # # License: GPLv2, see /usr/share/common-licenses/GPL-2 import os, sys, urllib, subprocess, getopt from debian_bundle.changelog import Version # Set this to the path of your Launchpad cookie file, when using # python-launchpad-bugs support (--lp). # The following will be tried automatically, if unset (first match gets used): # 1. ~/.lpcookie.txt # 2. ~/.mozilla/*/*/cookies.sqlite # 3. ~/.mozilla/*/*/cookies.txt launchpad_cookiefile = None def cur_version_component(sourcepkg, release): '''Determine current package version in ubuntu.''' madison = subprocess.Popen(['rmadison', '-u', 'ubuntu', '-a', 'source', \ '-s', release, sourcepkg], stdout=subprocess.PIPE) out = madison.communicate()[0] assert (madison.returncode == 0) for l in out.splitlines(): (pkg, version, rel, builds) = l.split('|') component = 'main' if rel.find('/') != -1: component = rel.split('/')[1] return (version.strip(), component.strip()) print "%s doesn't appear to exist in %s, specify -n for a package not in Ubuntu." % (sourcepkg, release) sys.exit(1) def cur_deb_version(sourcepkg): '''Return the current debian version of a package in unstable.''' madison = subprocess.Popen(['rmadison', '-u', 'debian', '-a', 'source', \ '-s', 'unstable', sourcepkg], \ stdout=subprocess.PIPE) out = madison.communicate()[0] assert (madison.returncode == 0) try: assert out except AssertionError: print "%s doesn't appear to exist in Debian." % sourcepkg sys.exit(1) # Work-around for a bug in Debians madison.php script not returning # only the source line for line in out.splitlines(): if line.find('source') > 0: out = line return out.split('|')[1].rstrip('[]''').strip() def debian_changelog(sourcepkg, component, version): '''Return the Debian changelog from the latest up to the given version (exclusive).''' base_version = Version(version) ch = '' subdir = sourcepkg[0] if sourcepkg.startswith('lib'): subdir = 'lib%s' % sourcepkg[3] for l in urllib.urlopen('http://packages.debian.org/changelogs/pool/%s/%s/%s/current/changelog.txt' % (component, subdir, sourcepkg)): if l.startswith(sourcepkg): ch_version = l[ l.find("(")+1 : l.find(")") ] if Version(ch_version) <= base_version: break ch += l return ch def debian_component(sourcepkg): '''Return the Debian component for the source package.''' madison = subprocess.Popen(['rmadison', '-u', 'debian', '-a', 'source', '-s', 'unstable', \ sourcepkg], stdout=subprocess.PIPE) out = madison.communicate()[0] assert (madison.returncode == 0) try: assert out except AssertionError: print "%s doesn't appear to exist in Debian." % sourcepkg sys.exit(1) raw_comp = out.split('|')[2].split('/') component = 'main' if len(raw_comp) == 2: component = raw_comp[1].strip() return component def raw_input_exit_on_ctrlc(*args, **kwargs): """A wrapper around raw_input() to exit with a normalized message on Control-C""" try: return raw_input(*args, **kwargs) except KeyboardInterrupt: print 'Abort requested. No sync request filed.' sys.exit(1) def usage(): print '''Usage: requestsync [-h|-n|-s|-k <keyid>|--lp] <source package> <target release> [basever] In some cases, the base version (fork point from Debian) cannot be determined automatically, and you'll get a complete Debian changelog. Specify the correct base version of the package in Ubuntu. Options: -h print this help -n new source package (doesn't exist yet in Ubuntu) -s request sponsoring (through ubuntu-{main,universe}-sponsors) -k <keyid> sign email with <keyid> (only used when submitting per email) --lp use python-launchpad-bugs instead of email for bug submitting ''' sys.exit(1) def mail_bug(source_package, subscribe, status, bugtitle, bugtext, keyid = None): '''Submit the sync request per email. Return True if email successfully send, otherwise False.''' import smtplib import socket to = 'new@bugs.launchpad.net' myemailaddr = os.getenv('DEBEMAIL') if not myemailaddr: print >> sys.stderr, 'The environment variable DEBEMAIL needs to be set to make use of this script.' return False # generate initial mailbody mailbody = '' if source_package: mailbody += ' affects ubuntu/%s\n' % source_package else: mailbody += ' affects ubuntu\n' mailbody = mailbody + ' status %s\n importance wishlist\n subscribe %s\n\n%s' % (status, subscribe, bugtext) # prepare sign_command sign_command = 'gpg' for cmd in ('gpg2', 'gnome-gpg'): if os.access('/usr/bin/%s' % cmd, os.X_OK): sign_command = cmd gpg_command = [sign_command, '--clearsign'] if keyid: gpg_command.extend(('-u', keyid)) in_confirm_loop = True while in_confirm_loop: # sign it gpg = subprocess.Popen(gpg_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE) signed_report = gpg.communicate(mailbody)[0] assert gpg.returncode == 0 # generate email mail = 'From: %s\nTo: %s\nSubject: %s\n\n%s' % (myemailaddr, to, bugtitle, signed_report) # ask for confirmation and allow to edit: print mail print 'Do you want to edit the report before sending [y/N]? Press Control-C to abort.' while 1: val = raw_input_exit_on_ctrlc() if val.lower() in ('y', 'yes'): (bugtitle, mailbody) = edit_report(bugtitle, mailbody) break elif val.lower() in ('n', 'no', ''): in_confirm_loop = False break else: print "Invalid answer" # get server address mailserver = os.getenv('DEBSMTP') if mailserver: print 'Using custom SMTP server:', mailserver else: mailserver = 'fiordland.ubuntu.com' # get server port mailserver_port = os.getenv('DEBSMTP_PORT') if mailserver_port: print 'Using custom SMTP port:', mailserver_port else: mailserver_port = 25 # connect to the server try: s = smtplib.SMTP(mailserver, mailserver_port) except socket.error, s: print "ERROR: Could not connect to mailserver %s at port %s: %s (%i)" % \ (mailserver, mailserver_port, s[1], s[0]) return False # authenticate to the server mailserver_user = os.getenv('DEBSMTP_USER') mailserver_pass = os.getenv('DEBSMTP_PASS') if mailserver_user and mailserver_pass: try: s.login(mailserver_user, mailserver_pass) except smtplib.SMTPAuthenticationError: print 'Error authenticating to the server: invalid username and password.' s.quit() return False except: print 'Unknown SMTP error.' s.quit() return False s.sendmail(myemailaddr, to, mail) s.quit() print 'Sync request mailed.' return True def post_bug(source_package, subscribe, status, bugtitle, bugtext): '''Use python-launchpad-bugs to submit the sync request. Return True if successfully posted, otherwise False.''' import glob, os.path global launchpad_cookiefile try: import launchpadbugs.connector except ImportError: print >> sys.stderr, 'Importing launchpadbugs failed. Is python-launchpad-bugs installed?' return False # Search cookiefile (for authentication to lp) if launchpad_cookiefile == None: try_globs = ('~/.lpcookie.txt', '~/.mozilla/*/*/cookies.sqlite', '~/.mozilla/*/*/cookies.txt') for try_glob in try_globs: try: cookiefile = glob.glob(os.path.expanduser(try_glob))[0] except IndexError: continue # Found: launchpad_cookiefile = cookiefile print "Using cookie file at «%s».\n" % launchpad_cookiefile break if launchpad_cookiefile == None: print >> sys.stderr, 'Could not find cookie file for Launchpad (looked in %s)' % ", ".join(try_globs) print >> sys.stderr, 'You should be able to create a valid file by logging into Launchpad with Firefox' return False if source_package: product = {'name': source_package, 'target': 'ubuntu'} else: # new source package product = {'name': 'ubuntu'} in_confirm_loop = True while in_confirm_loop: print 'Summary:\n%s\n\nDescription:\n%s' % (bugtitle, bugtext) # ask for confirmation and allow to edit: print 'Do you want to edit the report before sending [y/N]? Press Control-C to abort.' while 1: val = raw_input_exit_on_ctrlc() if val.lower() in ('y', 'yes'): (bugtitle, bugtext) = edit_report(bugtitle, bugtext) break elif val.lower() in ('n', 'no', ''): in_confirm_loop = False break else: print "Invalid answer" # Create bug Bug = launchpadbugs.connector.ConnectBug() Bug.authentication = launchpad_cookiefile bug = Bug.New(product = product, summary = bugtitle, description = bugtext) try: bug.importance = 'Wishlist' except IOError, s: print "Warning: setting importance failed: %s" % s bug.status = status bug.subscriptions.add(subscribe) bug.commit() print 'Sync request filed as bug #%i: https://launchpad.net/bugs/%i' % (bug.bugnumber, bug.bugnumber) return True def edit_report(subject, body, changes_required=False): """Edit a report (consisting of subject and body) in sensible-editor. subject and body get decorated, before they are written to the temporary file and undecorated after editing again. If changes_required is True and the file has not been edited (according to its mtime), an error is written to STDERR and the program exits. Returns (new_subject, new_body). """ import re, string report = "Summary (one line):\n%s\n\nDescription:\n%s" % (subject, body) # Create tempfile and remember mtime import tempfile report_file = tempfile.NamedTemporaryFile( prefix='requestsync_' ) report_file.file.write(report) report_file.file.flush() mtime_before = os.stat( report_file.name ).st_mtime # Launch editor try: editor = subprocess.check_call( ['sensible-editor', report_file.name] ) except subprocess.CalledProcessError, e: print >> sys.stderr, 'Error calling sensible-editor: %s\nAborting.' % (e,) sys.exit(1) # Check if the tempfile has been changed if changes_required: report_file_info = os.stat( report_file.name ) if mtime_before == os.stat( report_file.name ).st_mtime: print >> sys.stderr, 'The temporary file %s has not been changed, but you have\nto explain why the Ubuntu changes can be dropped. Aborting. [Press ENTER]' % (report_file.name,) raw_input() sys.exit(1) report_file.file.seek(0) report = report_file.file.read() report_file.file.close() # Undecorate report again: (new_subject, new_body) = report.split("\nDescription:\n", 1) # Remove prefix and whitespace for subject: new_subject = string.rstrip( re.sub("\n", " ", re.sub("^Summary \(one line\):\s*", "", new_subject, 1)) ) return (new_subject, new_body) # # entry point # if __name__ == '__main__': newsource = False sponsorship = False keyid = None use_lp_bugs = False need_interaction = False try: opts, args = getopt.gnu_getopt(sys.argv[1:], 'hnsk:', ('lp')) except getopt.GetoptError: usage() for o, a in opts: if o == '-h': usage() if o == '-n': newsource = True if o == '-s': sponsorship = True if o == '-k': keyid = a if o == '--lp': use_lp_bugs = True if len(args) not in (2, 3): usage() (srcpkg, release) = args[:2] force_base_ver = None if len(args) == 3: force_base_ver = args[2] (cur_ver, component) = ('0', 'universe') # Let's assume universe if not newsource: (cur_ver, component) = cur_version_component(srcpkg, release) debiancomponent = debian_component(srcpkg) deb_version = cur_deb_version(srcpkg) if deb_version == cur_ver: print 'The versions in Debian and Ubuntu are the same already (%s). Aborting.' % (deb_version,) sys.exit(1) # generate bug report subscribe = 'ubuntu-archive' status = 'confirmed' if sponsorship: status = 'new' if component in ['main', 'restricted']: subscribe = 'ubuntu-main-sponsors' else: subscribe = 'ubuntu-universe-sponsors' report = 'Please sync %s %s (%s) from Debian unstable (%s).\n\n' % (srcpkg, deb_version, component, debiancomponent) title = report[:-2] base_ver = cur_ver uidx = base_ver.find('ubuntu') if uidx > 0: base_ver = base_ver[:uidx] need_interaction = True print 'Changes have been made to the package in Ubuntu.' print 'Please edit the report and give an explanation.' print 'Press ENTER to start your editor. Press Control-C to abort now.' print 'Not saving the report file will abort the request, too.' raw_input_exit_on_ctrlc() report += 'Explanation of the Ubuntu delta and why it can be dropped:\n' + \ '>>> ENTER_EXPLANATION_HERE <<<\n\n' uidx = base_ver.find('build') if uidx > 0: base_ver = base_ver[:uidx] if force_base_ver: base_ver = force_base_ver report += 'Changelog since current %s version %s:\n\n' % (release, cur_ver) report += debian_changelog(srcpkg, debiancomponent, base_ver) + '\n' if need_interaction: (_, bug_text) = edit_report(title, report, changes_required=True) # Post sync request using Launchpad interface: srcpkg = not newsource and srcpkg or None if use_lp_bugs: # Map status to the values expected by lp-bugs mapping = {'new': 'New', 'confirmed': 'Confirmed'} if post_bug(srcpkg, subscribe, mapping[status], title, bug_text): sys.exit(0) # Abort on error: print 'Something went wrong. No sync request filed.' sys.exit(1) # Mail sync request: if mail_bug(srcpkg, subscribe, status, title, bug_text, keyid): sys.exit(0) print 'Something went wrong. No sync request filed.' sys.exit(1)