#!/usr/bin/python # -*- coding: utf-8 -*- # # (C) 2007 Canonical Ltd., Steve Kowalik # Authors: # Martin Pitt # Steve Kowalik # Michael Bienia # Daniel Hahler # Iain Lane # Jonathan Davies # Markus Korn (python-launchpadlib support) # # ################################################################## # # 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 os import subprocess import sys from debian_bundle.changelog import Version from optparse import OptionParser # ubuntu-dev-tools modules. import ubuntutools.lp.libsupport as lp_libsupport import ubuntutools.lp.udtexceptions as udtexceptions from ubuntutools.lp.lpapicache import Launchpad, LpApiWrapper, Distribution, PersonTeam # https_proxy fix import ubuntutools.common import ubuntutools.packages from ubuntutools.requestsync.mail import * from ubuntutools.requestsync.common import * def checkNeedsSponsorship(srcpkg): """ Check that the user has the appropriate permissions by checking what Launchpad returns when we check if someone can upload a package or not. 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. """ if not LpApiWrapper.canUploadPackage(srcpkg): print "You are not able to upload this package directly to Ubuntu.\n" \ "Your sync request shall require an approval by a member of " \ "the appropriate sponsorship team, who shall be subscribed to " \ "this bug report.\n" \ "This must be done before it can be processed by a member of " \ "the Ubuntu Archive team." print "If the above is correct please press Enter." raw_input_exit_on_ctrlc() # Abort if necessary. return True # Sponsorship required. # Is a team member, no sponsorship required. return False def checkExistingReports(package): """ Check existing bug reports on Launchpad for a possible sync request. If found ask for confirmation on filing a request. """ launchpad = None try: launchpad = Launchpad.login() except ImportError: print >> sys.stderr, 'Importing launchpadlib failed. Is ' \ 'python-launchpadlib installed?' except IOError, msg: # No credentials found. print msg # Failed to get Launchpad credentials. if launchpad is None: print >> sys.stderr, "Skipping existing report check, you should "\ "manually see if there are any at:" print "- https://bugs.launchpad.net/ubuntu/+source/%s" % package print "" return False # Fetch the package's bug list from Launchpad. pkg = Distribution('ubuntu').getSourcePackage(name=package) pkgBugList = pkg.searchTasks() # Search bug list for other sync requests. matchingBugs = [bug for bug in pkgBugList if "Sync %s" % package in bug.title or "Please sync %s" % package in bug.title] if len(matchingBugs) == 0: return # No sync bugs found. print "The following bugs could be possible duplicate sync bug(s) on Launchpad:" for bug in matchingBugs: print " *", bug.title print " -", lp_libsupport.translate_api_web(bug.self_link) print "Please check the above URLs to verify this before filing a " \ "possible duplicate report." print "Press Ctrl-C to stop filing the bug report now, otherwise " \ "please press enter." raw_input_exit_on_ctrlc() def cur_version_component(sourcepkg, release): try: src = getUbuntuSrcPkg(sourcepkg, release) return (src.getVersion(), src.getComponent()) except udtexceptions.PackageNotFoundException: print "%s doesn't appear to exist in %s, specify -n for a package not in Ubuntu." % (sourcepkg, release) sys.exit(1) def get_email_address(): '''Get the DEBEMAIL environment variable or give an error.''' myemailaddr = os.getenv('DEBEMAIL') if not myemailaddr: myemailaddr = os.getenv('EMAIL') if not myemailaddr: print >> sys.stderr, 'The environment variable DEBEMAIL or ' +\ 'EMAIL 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 done\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 To: %s Subject: %s Content-Type: text/plain; charset=UTF-8 %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 launchpadlib " \ "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-launchpadlib to submit the sync request. Return True if successfully posted, otherwise False.''' import glob, os.path try: launchpad = Launchpad.login() except ImportError: print >> sys.stderr, 'Importing launchpadlib failed. Is python-launchpadlib installed?' return False except IOError, msg: # No credentials found. print msg sys.exit(1) if source_package: product_url = "%subuntu/+source/%s" %(launchpad._root_uri, source_package) else: # new source package product_url = "%subuntu" %launchpad._root_uri 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 = launchpad.bugs.createBug(description=bugtext, title=bugtitle, target=product_url) #newly created bugreports have one task task = bug.bug_tasks[0] # Only members of ubuntu-bugcontrol can set importance if PersonTeam.getMe().isLpTeamMember('ubuntu-bugcontrol'): task.importance = 'Wishlist' task.status = status task.lp_save() subscribe_url = "%s~%s" %(launchpad._root_uri, subscribe) bug.subscribe(person=subscribe_url) print 'Sync request filed as bug #%i: %s' % (bug.id, lp_libsupport.translate_api_web(bug.self_link)) 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__': # Our usage options. usage = "Usage: %prog [-d distro] [-k keyid] [-n] [--lp] [-s] [-e] " usage += " [ [base version]]" optParser = OptionParser(usage) optParser.add_option("-d", type = "string", dest = "dist", default = "unstable", help = "Debian distribution to sync from.") optParser.add_option("-k", type = "string", dest = "keyid", default = None, help = "GnuPG key ID to use for signing report.") optParser.add_option("-n", action = "store_true", dest = "newpkg", default = False, help = "Whether package to sync is a new package in Ubuntu.") optParser.add_option("--lp", action = "store_true", dest = "lpbugs", default = False, help = "Specify whether to use the launchpadlib module for filing " \ "report.") optParser.add_option("-s", action = "store_true", dest = "sponsor", default = False, help = "Force sponsorship requirement (shall be autodetected if not " \ "specified).") optParser.add_option("-e", action = "store_true", dest = "ffe", default = False, help = "Use this after FeatureFreeze for non-bug fix syncs, changes " \ "default subscription to the appropriate release team.") (options, args) = optParser.parse_args() newsource = options.newpkg sponsorship = options.sponsor keyid = options.keyid use_lp_bugs = options.lpbugs need_interaction = False distro = options.dist ffe = options.ffe if not use_lp_bugs and not get_email_address(): sys.exit(1) if len(args) == 0: optParser.print_help() sys.exit(1) if len(args) not in (2, 3): # no release specified, assume development release release = Distribution('ubuntu').getDevelopmentSeries().name print >> sys.stderr, ("Source package / target release missing - assuming %s " % release) else: release = args[1] srcpkg = args[0] force_base_ver = None # Base version specified. if len(args) == 3: force_base_ver = args[2] (cur_ver, component) = ('0', 'universe') # Let's assume universe # Find Ubuntu release's package version. if not newsource: (cur_ver, component) = cur_version_component(srcpkg, release) debsrcpkg = getDebianSrcPkg(srcpkg, distro) debiancomponent = debsrcpkg.getComponent() # Find Debian release's package version. deb_version = debsrcpkg.getVersion() # Debian and Ubuntu versions are the same - stop. if deb_version == cur_ver: print 'The versions in Debian and Ubuntu are the same already (%s). Aborting.' % (deb_version,) sys.exit(1) # -s flag not specified - check if we do need sponsorship. if not sponsorship: sponsorship = checkNeedsSponsorship(srcpkg) # Check for existing package reports. if not newsource and use_lp_bugs: checkExistingReports(srcpkg) # Generate bug report. subscribe = 'ubuntu-archive' status = 'confirmed' if sponsorship == True: status = 'new' if component in ['main', 'restricted']: subscribe = 'ubuntu-main-sponsors' else: subscribe = 'ubuntu-universe-sponsors' if ffe == True: status = 'new' if component in ['main', 'restricted']: subscribe = 'ubuntu-release' else: subscribe = 'motu-release' pkg_to_sync = '%s %s (%s) from Debian %s (%s).' % (srcpkg, deb_version, component, distro, debiancomponent) title = "Sync %s" % pkg_to_sync if ffe == True: title = "FFe: " + title report = "Please sync %s\n\n" % pkg_to_sync 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' if ffe == True: need_interaction = True print 'To approve FeatureFreeze exception, you need to state ' print 'the reason why you feel it is necessary.' 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 FeatureFreeze exception:\n' + \ '>>> ENTER_EXPLANATION_HERE <<<\n\n' # Check if they have a per-package upload permission. if LpApiWrapper.isPerPackageUploader(srcpkg): report += 'Note that I have per-package upload permissions for %s.\n\n' % srcpkg 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) changelog = getDebianChangelog(debsrcpkg, Version(base_ver)) if not changelog: sys.exit(1) report += changelog + '\n' if need_interaction: (title, 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)