#!/usr/bin/python # -*- coding: utf-8 -*- # # (C) 2007 Canonical Ltd., Steve Kowalik # Authors: # Martin Pitt # Steve Kowalik # Michael Bienia (python-launchpad-bugs support) # Daniel Hahler # Iain Lane # Jonathan Patrick Davies # # ################################################################## # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; version 2. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # See file /usr/share/common-licenses/GPL-2 for more details. # # ################################################################## import getopt import os import subprocess import sys import urllib import urllib2 from debian_bundle.changelog import Version # Use functions from ubuntu-dev-tools to create Launchpad cookie file. sys.path.append('/usr/share/ubuntu-dev-tools/') import common launchpad_cookiefile = common.prepareLaunchpadCookie() def checkNeedsSponsorship(component): """ Check that the user has the appropriate permissions by checking what Launchpad returns while authenticating with their cookie. If they are an indirect or direct member of the ~ubuntu-dev team on Launchpad - sponsorship is not required if the package is in the universe / multiverse component. If they are in the ~ubuntu-core-dev team, no sponsorship required. The prepareLaunchpadCookie function above shall ensure that a cookie file exists first. """ urlopener = common.setupLaunchpadUrlOpener(launchpad_cookiefile) # Check where the package is and assign the appropriate variables. if component in ['main', 'restricted']: team = "ubuntu-core-dev" sponsor = "ubuntu-main-sponsors" else: team = "ubuntu-dev" sponsor = "ubuntu-universe-sponsors" # Try and open up Launchpad. try: teamLPPage = urlopener.open('https://launchpad.net/~%s' % team).read() except urllib2.HTTPError: print >> sys.stderr, "Unable to connect to Launchpad." # Check if they are a member of the team. if "You are an indirect member of this team:" in teamLPPage or \ "You are a member of this team." in teamLPPage: return False # Sponsorship not required. # Check if they are not. if "You are not a member of this team" in teamLPPage: print "You are not a member (direct or indirect) of the '%s' " \ "team on Launchpad." % team print "Your sync request shall require an approval by a member of " \ "the '%s'\nteam, who shall be subscribed to this bug report." % sponsor print "This must be done before it can be processed by a member of " \ "the Ubuntu Archive team." print "Should the above be incorrect, please press Control-C now to " \ "stop this script now\nand check the cookie file at:", launchpad_cookiefile raw_input_exit_on_ctrlc() # Abort if necessary. return True # Sponsorship required. 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, distro): '''Return the current debian version of a package in a Debian distro.''' madison = subprocess.Popen(['rmadison', '-u', 'debian', '-a', 'source', \ '-s', distro, 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, distro): '''Return the Debian component for the source package.''' madison = subprocess.Popen(['rmadison', '-u', 'debian', '-a', 'source', '-s', distro, \ 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|-k |--lp] [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) -k sign email with (only used when submitting per email) --lp use python-launchpad-bugs instead of email for bug submitting ''' sys.exit(1) def get_email_address(): '''Get the DEBEMAIL environment variable or give an error.''' myemailaddr = os.getenv('DEBEMAIL') if not myemailaddr: print >> sys.stderr, 'The environment variable DEBEMAIL needs to be ' +\ ' set to make use of this script, unless you use option --lp.' return myemailaddr 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 = get_email_address() if not myemailaddr: 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 >> sys.stderr, "Could not connect to mailserver %s at port %s: %s (%i)" % \ (mailserver, mailserver_port, s[1], s[0]) print "The port %s may be firewalled. Please try using requestsync with" \ % mailserver_port print "the '--lp' flag to file a sync request with the launchpadbugs " \ "module." 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 try: import launchpadbugs.connector except ImportError: print >> sys.stderr, 'Importing launchpadbugs failed. Is python-launchpad-bugs installed?' return False print "Using cookie file at", launchpad_cookiefile 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 import 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 distro = 'unstable' try: opts, args = getopt.gnu_getopt(sys.argv[1:], 'hnd:k:', ('lp')) except getopt.GetoptError: usage() for o, a in opts: if o == '-h': usage() if o == '-n': newsource = True if o == '-k': keyid = a if o == '-d': distro = a if o == '--lp': use_lp_bugs = True if len(args) not in (2, 3): usage() if not use_lp_bugs and not get_email_address(): sys.exit(1) (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, distro) deb_version = cur_deb_version(srcpkg, distro) sponsorship = checkNeedsSponsorship(component) 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 %s (%s).\n\n' % (srcpkg, deb_version, component, distro, 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: (_, report) = 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, report): 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, report, keyid): sys.exit(0) print 'Something went wrong. No sync request filed.' sys.exit(1)