diff --git a/buildd b/buildd index 1b55f32..31eb182 100755 --- a/buildd +++ b/buildd @@ -199,7 +199,7 @@ else: # filter out duplicate and invalid architectures archs.intersection_update(valid_archs) -release = options.series # if None it falls back to the current development series +release = options.series or Distribution('ubuntu').getDevelopmentSeries().name pocket = 'Release' if release and '-' in release: # split release and pocket diff --git a/debian/changelog b/debian/changelog index ba79c1c..18df071 100644 --- a/debian/changelog +++ b/debian/changelog @@ -14,6 +14,12 @@ ubuntu-dev-tools (0.76) UNRELEASED; urgency=low [ Michael Bienia ] * Drop python-launchpad-bugs from Depends. * buildd: Add a --batch mode for batch retrying/rescoring of packages. + * requestsync: + - Use UBU* environment variables before the DEB* ones (lp: #400133) + - Split requestsync into a "mail" module and a "lpapi" module and use + the LP API only when --lp was used. In "mail" mode requestsync has + to ask some more questions for parts it can't find out without LP API. + (lp: #406659, #416955) [ Iain Lane ] * requestsync: @@ -39,7 +45,7 @@ ubuntu-dev-tools (0.76) UNRELEASED; urgency=low - debian/rules: set DEB_PYTHON_SYSTEM to pysupport. - ubuntu-dev-tools.preinst: remove stale pycentral files on upgrades. - -- Luca Falavigna Fri, 21 Aug 2009 17:30:05 +0200 + -- Michael Bienia Tue, 25 Aug 2009 13:03:50 +0200 ubuntu-dev-tools (0.75) karmic; urgency=low diff --git a/requestsync b/requestsync index ebb86e8..19f7285 100755 --- a/requestsync +++ b/requestsync @@ -26,571 +26,196 @@ # # ################################################################## -import os -import subprocess import sys -import urllib -import urllib2 -from debian_bundle.changelog import Version from optparse import OptionParser -from time import sleep +from debian_bundle.changelog import Version -# 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 +# ubuntu-dev-tools modules +from ubuntutools.lp import udtexceptions +from ubuntutools.requestsync.common import * # https_proxy fix import ubuntutools.common -import ubuntutools.packages - -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 = Distribution('ubuntu').getArchive().getSourcePackage(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 cur_deb_version(sourcepkg, distro): - '''Return the current debian version of a package in a Debian distro.''' - out = ubuntutools.packages.checkIsInDebian(sourcepkg, distro) - if not out: - print "%s doesn't appear to exist in Debian." % sourcepkg - sys.exit(0) - - # 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, debver): - '''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] - - # Get the debian/changelog file from packages.debian.org. - try: - pkgurl = 'http://packages.debian.org/changelogs/pool/%s/%s/%s/%s_%s/changelog.txt' % (component, subdir, sourcepkg, sourcepkg, debver) - debianChangelogPage = urllib2.urlopen(pkgurl) - except urllib2.HTTPError, error: - print >> sys.stderr, "Unable to connect to packages.debian.org. " \ - "Received a %s." % error.code - sys.exit(1) - - for l in debianChangelogPage: - 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 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) + # Our usage options. + usage = 'Usage: %prog [-d distro] [-k keyid] [-n] [--lp] [-s] [-e] ' \ + ' [ [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.") + 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 (only used when emailing the sync request).') + 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 = 'lpapi', default = False, + help = 'Specify whether to use the LP API for filing the sync request (recommended).') + optParser.add_option('-s', action = 'store_true', + dest = 'sponsorship', default = False, + help = 'Force sponsorship') + 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() + (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 len(args): + optParser.print_help() + sys.exit(1) - if not use_lp_bugs and not get_email_address(): - sys.exit(1) + # import the needed requestsync module + if options.lpapi: + from ubuntutools.requestsync.lp import * + from ubuntutools.lp.lpapicache import Distribution, PersonTeam + else: + from ubuntutools.requestsync.mail import * + if not getEmailAddress(): + sys.exit(1) - if len(args) == 0: - optParser.print_help() - sys.exit(1) + newsource = options.newpkg + sponsorship = options.sponsorship + distro = options.dist + ffe = options.ffe + lpapi = options.lpapi + need_interaction = False + force_base_version = None + srcpkg = args[0] - 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] + if len(args) == 1: + if lpapi: + release = Distribution('ubuntu').getDevelopmentSeries().name + print >> sys.stderr, 'W: Target release missing - assuming %s' % release + else: + print >> sys.stderr, 'E: Source package or target release missing. Exiting.' + sys.exit(1) + elif len(args) == 2: + release = args[1] + elif len(args) == 3: + release = args[1] + force_base_version = Version(args[2]) + else: + print >> sys.stderr, 'E: Too many arguments.' + optParser.print_help() + sys.exit(1) - srcpkg = args[0] - force_base_ver = None + # Get the current Ubuntu source package + try: + ubuntu_srcpkg = getUbuntuSrcPkg(srcpkg, release) + ubuntu_version = Version(ubuntu_srcpkg.getVersion()) + ubuntu_component = ubuntu_srcpkg.getComponent() + newsource = False # override the -n flag + except udtexceptions.PackageNotFoundException: + ubuntu_srcpkg = None + ubuntu_version = Version(0) + ubuntu_component = 'universe' # let's assume universe + if not newsource: + print "'%s' doesn't exist in 'Ubuntu %s'.\nDo you want to sync a new package?" % \ + (srcpkg, release) + raw_input_exit_on_ctrlc('Press [Enter] to continue or [Ctrl-C] to abort. ') + newsource = True - # Base version specified. - if len(args) == 3: force_base_ver = args[2] + # Get the requested Debian source package + try: + debian_srcpkg = getDebianSrcPkg(srcpkg, distro) + debian_version = Version(debian_srcpkg.getVersion()) + debian_component = debian_srcpkg.getComponent() + except udtexceptions.PackageNotFoundException, e: + print >> sys.stderr, "E: %s" % e + sys.exit(1) - (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) + # Debian and Ubuntu versions are the same - stop + if ubuntu_version == debian_version: + print >> sys.stderr, \ + 'E: The versions in Debian and Ubuntu are the same already (%s). Aborting.' % ubuntu_version + sys.exit(1) - debiancomponent = debian_component(srcpkg, distro) - # Find Debian release's package version. - deb_version = cur_deb_version(srcpkg, distro) + # -s flag not specified - check if we do need sponsorship + if not sponsorship: + sponsorship = needSponsorship(srcpkg, ubuntu_component) - # 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) + # Check for existing package reports + if not newsource: + checkExistingReports(srcpkg) - # -s flag not specified - check if we do need sponsorship. - if (not sponsorship): - if (not newsource): - sponsorship = checkNeedsSponsorship(srcpkg) - else: - sponsorship = not PersonTeam.getMe().isLpTeamMember('motu') # assume going to universe + # Generate bug report + pkg_to_sync = '%s %s (%s) from Debian %s (%s)' % \ + (srcpkg, debian_version, ubuntu_component, distro, debian_component) + title = "Sync %s" % pkg_to_sync + if ffe: + title = "FFe: " + title + report = "Please sync %s\n\n" % pkg_to_sync - # Check for existing package reports. - if (not newsource) and use_lp_bugs: checkExistingReports(srcpkg) + if 'ubuntu' in str(ubuntu_version): + need_interaction = True - # 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' + print 'Changes have been made to the package in Ubuntu.\n' \ + 'Please edit the report and give an explanation.\n' \ + 'Not saving the report file will abort the request.' + report += 'Explanation of the Ubuntu delta and why it can be dropped:\n' \ + '>>> ENTER_EXPLANATION_HERE <<<\n\n' + if ffe: + need_interaction = True - 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 'To approve FeatureFreeze exception, you need to state\n' \ + 'the reason why you feel it is necessary.\n' \ + 'Not saving the report file will abort the request.' + report += 'Explanation of FeatureFreeze exception:\n' \ + '>>> ENTER_EXPLANATION_HERE <<<\n\n' - 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 need_interaction: + raw_input_exit_on_ctrlc('Press [Enter] to continue. Press [Ctrl-C] to abort now. ') - if ffe == True: - need_interaction = True + # Check if they have a per-package upload permission. + if lpapi: + ubuntu_archive = Distribution('ubuntu').getArchive() + if PersonTeam.getMe().isPerPackageUploader(ubuntu_archive, srcpkg): + report += 'Note that I have per-package upload permissions for %s.\n\n' % srcpkg - 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' + base_version = force_base_version or ubuntu_version + if newsource: + report += 'All changelog entries:\n\n' + else: + report += 'Changelog entries since current %s version %s:\n\n' % (release, ubuntu_version) + changelog = getDebianChangelog(debian_srcpkg, base_version) + if not changelog: + sys.exit(1) + report += changelog - # 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 + (title, report) = edit_report(title, report, changes_required = need_interaction) - uidx = base_ver.find('build') - if uidx > 0: - base_ver = base_ver[:uidx] + # bug status and bug subscriber + status = 'confirmed' + subscribe = 'ubuntu-archive' + if sponsorship: + status = 'new' + if ubuntu_component in ('main', 'restricted'): + subscribe = 'ubuntu-main-sponsors' + else: + subscribe = 'ubuntu-universe-sponsors' + if ffe: + status = 'new' + if ubuntu_component in ('main', 'restricted'): + subscribe = 'ubuntu-release' + else: + subscribe = 'motu-release' - if force_base_ver: - base_ver = force_base_ver - - if not newsource: report += 'Changelog since current %s version %s:\n\n' % (release, cur_ver) - report += debian_changelog(srcpkg, debiancomponent, base_ver, deb_version) + '\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) + srcpkg = not newsource and srcpkg or None + if lpapi: + # Map status to the values expected by LP API + mapping = {'new': 'New', 'confirmed': 'Confirmed'} + # Post sync request using LP API + postBug(srcpkg, subscribe, mapping[status], title, report) + else: + # Mail sync request + mailBug(srcpkg, subscribe, status, title, report, options.keyid) diff --git a/ubuntutools/lp/lpapicache.py b/ubuntutools/lp/lpapicache.py index fbf0009..e1c6896 100644 --- a/ubuntutools/lp/lpapicache.py +++ b/ubuntutools/lp/lpapicache.py @@ -24,6 +24,7 @@ #import httplib2 #httplib2.debuglevel = 1 +import sys import libsupport from launchpadlib.errors import HTTPError from launchpadlib.resource import Entry @@ -38,7 +39,11 @@ class Launchpad(object): Enforce a login through the LP API. ''' if not self.__lp: - self.__lp = libsupport.get_launchpad('ubuntu-dev-tools') + try: + self.__lp = libsupport.get_launchpad('ubuntu-dev-tools') + except IOError, error: + print >> sys.stderr, 'E: %s' % error + sys.exit(1) return self def __getattr__(self, attr): @@ -50,56 +55,6 @@ class Launchpad(object): return self Launchpad = Launchpad() -# Almost deprecated, better use the specific classes like Distribution -# or PersonTeam directly -class LpApiWrapper(object): - ''' - Wrapper around some common used LP API functions used in - ubuntu-dev-tools. - ''' - - @classmethod - def canUploadPackage(cls, srcpkg, series = None): - ''' - Check if the currently authenticated LP user has upload rights - for package either through component upload rights or - per-package upload rights. - - 'package' can either be a SourcePackagePublishingHistory object - or a string and an Ubuntu series. If 'package' doesn't exist - yet in Ubuntu assume 'universe' for component. - ''' - component = 'universe' - archive = Distribution('ubuntu').getArchive() - - if isinstance(srcpkg, SourcePackagePublishingHistory): - package = srcpkg.getPackageName() - component = srcpkg.getComponent() - else: - if not series: - series = Distribution('ubuntu').getDevelopmentSeries() - try: - srcpkg = archive.getSourcePackage(srcpkg, series) - package = srcpkg.getPackageName() - component = srcpkg.getComponent() - except PackageNotFoundException: - package = None - - return PersonTeam.getMe().canUploadPackage(archive, package, component) - - # TODO: check if this is still needed after ArchiveReorg (or at all) - @classmethod - def isPerPackageUploader(cls, package, series = None): - ''' - Check if the user has PerPackageUpload rights for package. - ''' - if isinstance(package, SourcePackagePublishingHistory): - package = package.getPackageName() - - archive = Distribution('ubuntu').getArchive() - - return PersonTeam.getMe().canUploadPackage(archive, package, None) - class MetaWrapper(type): ''' @@ -458,9 +413,9 @@ class PersonTeam(BaseWrapper): ''' if not isinstance(archive, Archive): raise TypeError("'%r' is not an Archive object." % archive) - if not isinstance(package, (str, None)): + if package and not isinstance(package, str): raise TypeError('A source package name expected.') - if not isinstance(component, (str, None)): + if component and not isinstance(component, str): raise TypeError('A component name expected.') if not package and not component: raise ValueError('Either a source package name or a component has to be specified.') @@ -494,10 +449,8 @@ class PersonTeam(BaseWrapper): ''' if isinstance(package, SourcePackagePublishingHistory): pkg = package.getPackageName() - comp = package.getComponent() else: pkg = package - compon return self.canUploadPackage(archive, pkg, None) diff --git a/ubuntutools/requestsync/__init__.py b/ubuntutools/requestsync/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ubuntutools/requestsync/common.py b/ubuntutools/requestsync/common.py new file mode 100644 index 0000000..2cb66b9 --- /dev/null +++ b/ubuntutools/requestsync/common.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# common.py - common methods used by requestsync +# +# Copyright © 2009 Michael Bienia +# +# This module may contain code written by other authors/contributors to +# the main requestsync script. See there for their names. +# +# 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. +# +# Please see the /usr/share/common-licenses/GPL-2 file for the full text +# of the GNU General Public License license. + +import os +import sys +import urllib2 +import re +import tempfile +import subprocess +from debian_bundle.changelog import Changelog + +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 '\nAbort requested. No sync request filed.' + sys.exit(1) + +def getDebianChangelog(srcpkg, version): + ''' + Return the new changelog entries upto 'version'. + ''' + pkgname = srcpkg.getPackageName() + pkgversion = srcpkg.getVersion() + component = srcpkg.getComponent() + if pkgname.startswith('lib'): + subdir = 'lib%s' % pkgname[3] + else: + subdir = pkgname[0] + # Strip epoch from version + if ':' in pkgversion: + pkgversion = pkgversion[pkgversion.find(':')+1:] + + # Get the debian changelog file from packages.debian.org + try: + changelog = urllib2.urlopen( + 'http://packages.debian.org/changelogs/pool/%s/%s/%s/%s_%s/changelog.txt' % \ + (component, subdir, pkgname, pkgname, pkgversion)) + except urllib2.HTTPError, error: + print >> sys.stderr, 'Unable to connect to packages.debian.org: %s' % error + return None + + new_entries = '' + changelog = Changelog(changelog.read()) + for block in changelog._blocks: + if block.version > version: + new_entries += str(block) + + return new_entries + +def edit_report(subject, body, changes_required = False): + ''' + Ask if the user wants to edit a report (consisting of subject and body) + in sensible-editor. + + If changes_required is True then the file has to be edited before we + can proceed. + + Returns (new_subject, new_body). + ''' + + editing_finished = False + while not editing_finished: + report = 'Summary (one line):\n%s\n\nDescription:\n%s' % (subject, body) + + if not changes_required: + print 'Currently the report looks as follows:\n%s' % report + while True: + val = raw_input_exit_on_ctrlc('Do you want to edit the report [y/N]? ') + if val.lower() in ('y', 'yes'): + break + elif val.lower() in ('n', 'no', ''): + editing_finished = True + break + else: + print 'Invalid answer.' + + if not editing_finished: + # Create tempfile and remember mtime + report_file = tempfile.NamedTemporaryFile(prefix='requestsync_') + report_file.write(report) + report_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: + if mtime_before == os.stat(report_file.name).st_mtime: + print 'The report has not been changed, but you have to explain why ' \ + 'the Ubuntu changes can be dropped.' + raw_input_exit_on_ctrlc('Press [Enter] to retry or [Control-C] to abort. ') + else: + changes_required = False + + report_file.seek(0) + report = report_file.read() + report_file.close() + + # Undecorate report again + (subject, body) = report.split("\nDescription:\n", 1) + # Remove prefix and whitespace from subject + subject = re.sub('^Summary \(one line\):\s*', '', subject, 1).strip() + + return (subject, body) diff --git a/ubuntutools/requestsync/lp.py b/ubuntutools/requestsync/lp.py new file mode 100644 index 0000000..7e8f3b3 --- /dev/null +++ b/ubuntutools/requestsync/lp.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# +# lp.py - methods used by requestsync while interacting +# directly with Launchpad +# +# Copyright © 2009 Michael Bienia +# +# This module may contain code written by other authors/contributors to +# the main requestsync script. See there for their names. +# +# 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. +# +# Please see the /usr/share/common-licenses/GPL-2 file for the full text +# of the GNU General Public License license. + +from .common import raw_input_exit_on_ctrlc +from ..lp.lpapicache import Launchpad, Distribution, PersonTeam, DistributionSourcePackage +from ..lp.udtexceptions import * +from ..lp.libsupport import translate_api_web + +def getDebianSrcPkg(name, release): + debian = Distribution('debian') + debian_archive = debian.getArchive() + + # Map 'unstable' to 'sid' as LP doesn't know 'unstable' but only 'sid' + if release == 'unstable': + release = 'sid' + + return debian_archive.getSourcePackage(name, release) + +def getUbuntuSrcPkg(name, release): + ubuntu = Distribution('ubuntu') + ubuntu_archive = ubuntu.getArchive() + + return ubuntu_archive.getSourcePackage(name, release) + +def needSponsorship(name, component): + ''' + Check if the user has upload permissions for either the package + itself or the component + ''' + archive = Distribution('ubuntu').getArchive() + + need_sponsor = not PersonTeam.getMe().canUploadPackage(archive, name, component) + if need_sponsor: + print '''You are not able to upload this package directly to Ubuntu. +Your sync request shall require an approval by a member of the appropriate +sponsorship team, who shall be subscribed to this bug report. +This must be done before it can be processed by a member of the Ubuntu Archive +team.''' + raw_input_exit_on_ctrlc('If the above is correct please press [Enter] ') + + return need_sponsor + +def checkExistingReports(srcpkg): + ''' + Check existing bug reports on Launchpad for a possible sync request. + + If found ask for confirmation on filing a request. + ''' + + # Fetch the package's bug list from Launchpad + pkg = Distribution('ubuntu').getSourcePackage(name = srcpkg) + pkgBugList = pkg.getBugTasks() + + # Search bug list for other sync requests. + for bug in pkgBugList: + # check for Sync or sync and the package name + if not bug.is_complete and 'ync %s' % srcpkg in bug.title: + print 'The following bug could be a possible duplicate sync bug on Launchpad:' + print ' * %s (%s)' % \ + (bug.title, translate_api_web(bug.self_link)) + print 'Please check the above URL to verify this before continuing.' + raw_input_exit_on_ctrlc('Press [Enter] to continue or [Ctrl-C] to abort. ') + +def postBug(srcpkg, subscribe, status, bugtitle, bugtext): + ''' + Use the LP API to file the sync request. + ''' + + print 'The final report is:\nSummary: %s\nDescription:\n%s\n' % (bugtitle, bugtext) + raw_input_exit_on_ctrlc('Press [Enter] to continue or [Ctrl-C] to abort. ') + + if srcpkg: + bug_target = DistributionSourcePackage( + '%subuntu/+source/%s' % (Launchpad._root_uri, srcpkg)) + else: + # new source package + bug_target = Distribution('ubuntu') + + # create bug + bug = Launchpad.bugs.createBug(title = bugtitle, description = bugtext, target = bug_target()) + + # newly created bugreports have only 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() + + bug.subscribe(person = PersonTeam(subscribe)()) + + print 'Sync request filed as bug #%i: %s' % (bug.id, + translate_api_web(bug.self_link)) diff --git a/ubuntutools/requestsync/mail.py b/ubuntutools/requestsync/mail.py new file mode 100644 index 0000000..8e7d41a --- /dev/null +++ b/ubuntutools/requestsync/mail.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# +# mail.py - methods used by requestsync when used in "mail" mode +# +# Copyright © 2009 Michael Bienia +# +# This module may contain code written by other authors/contributors to +# the main requestsync script. See there for their names. +# +# 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. +# +# Please see the /usr/share/common-licenses/GPL-2 file for the full text +# of the GNU General Public License license. + +import os +import sys +import subprocess +import smtplib +import socket +from .common import raw_input_exit_on_ctrlc +from ..lp.udtexceptions import PackageNotFoundException + +__all__ = [ + 'getDebianSrcPkg', + 'getUbuntuSrcPkg', + 'getEmailAddress', + 'needSponsorship', + 'checkExistingReports', + 'mailBug', + ] + +class SourcePackagePublishingHistory(object): + ''' + Simulate a SourcePackagePublishingHistory class from the LP API caching + module. + ''' + def __init__(self, name, version, component): + self.name = name + self.version = version + self.component = component + + def getPackageName(self): + return self.name + + def getVersion(self): + return self.version + + def getComponent(self): + return self.component + +def rmadison(distro, package, release): + rmadison_cmd = subprocess.Popen( + ['rmadison', '-u', distro, '-a', 'source', '-s', release, package], + stdout = subprocess.PIPE) + + rmadison_out = rmadison_cmd.communicate()[0] + assert (rmadison_cmd.returncode == 0) + + # Work-around for a bug in Debians madison.php script not returning + # only the source line + for line in rmadison_out.splitlines(): + if line.find('source') > 0: + return map(lambda x: x.strip(), line.split('|')) + + return None + +def getSrcPkg(distro, name, release): + out = rmadison(distro, name, release) + if not out: + raise PackageNotFoundException( + "'%s' doesn't appear to exist in %s '%s'" % \ + (name, distro.capitalize(), release)) + + version = out[1] + component = 'main' + raw_comp = out[2].split('/') + if len(raw_comp) == 2: + component = raw_comp[1] + + return SourcePackagePublishingHistory(name, version, component) + +def getDebianSrcPkg(name, release): + return getSrcPkg('debian', name, release) + +def getUbuntuSrcPkg(name, release): + return getSrcPkg('ubuntu', name, release) + +def getEmailAddress(): + ''' + Get the From email address from the UBUMAIL, DEBEMAIL or EMAIL + environment variable or give an error. + ''' + myemailaddr = os.getenv('UBUMAIL') or os.getenv('DEBEMAIL') or os.getenv('EMAIL') + if not myemailaddr: + print >> sys.stderr, 'E: The environment variable UBUMAIL, ' \ + 'DEBEMAIL or EMAIL needs to be set to let this script ' \ + 'mail the sync request.' + return myemailaddr + +def needSponsorship(name, component): + ''' + Ask the user if he has upload permissions for the package or the + component. + ''' + + while True: + print "Do you have upload permissions for the '%s' component " \ + "or the package '%s'?" % (component, name) + val = raw_input_exit_on_ctrlc("If in doubt answer 'n'. [y/N]? ") + if val.lower() in ('y', 'yes'): + return False + elif val.lower() in ('n', 'no', ''): + return True + else: + print 'Invalid answer' + +def checkExistingReports(srcpkg): + ''' + Point the user to the URL to manually check for duplicate bug reports. + ''' + print 'Please check on https://bugs.launchpad.net/ubuntu/+source/%s/+bugs\n' \ + 'for duplicate sync requests before continuing.' % srcpkg + raw_input_exit_on_ctrlc('Press [Enter] to continue or [Ctrl-C] to abort. ') + +def mailBug(srcpkg, subscribe, status, bugtitle, bugtext, keyid = None): + ''' + Submit the sync request per email. + ''' + + to = 'new@bugs.launchpad.net' + + # getEmailAddress() can't fail here as the main code in requestsync + # already checks its return value + myemailaddr = getEmailAddress() + + # generate mailbody + if srcpkg: + mailbody = ' affects ubuntu/%s\n' % srcpkg + else: + mailbody = ' affects ubuntu\n' + mailbody += '''\ + status %s + importance wishlist + subscribe %s + done + +%s''' % (status, subscribe, bugtext) + + # prepare sign command + gpg_command = None + for cmd in ('gpg', 'gpg2', 'gnome-gpg'): + if os.access('/usr/bin/%s' % cmd, os.X_OK): + gpg_command = [cmd] + assert gpg_command # TODO: catch exception and produce error message + + gpg_command.append('--clearsign') + if keyid: + gpg_command.extend(('-u', keyid)) + + # sign the mail body + 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) + + print 'The final report is:\n%s' % mail + raw_input_exit_on_ctrlc('Press [Enter] to continue or [Ctrl-C] to abort. ') + + # get server address and port + mailserver_host = os.getenv('UBUSMTP') or os.getenv('DEBSMTP') or 'fiordland.ubuntu.com' + mailserver_port = os.getenv('UBUSMTP_PORT') or os.getenv('DEBSMTP_PORT') or 25 + + # connect to the server + try: + print 'Connecting to %s:%s ...' % (mailserver_host, mailserver_port) + s = smtplib.SMTP(mailserver_host, mailserver_port) + except socket.error, s: + print >> sys.stderr, 'E: Could not connect to %s:%s: %s (%i)' % \ + (mailserver_host, mailserver_port, s[1], s[0]) + return + + # authenticate to the server + mailserver_user = os.getenv('UBUSMTP_USER') or os.getenv('DEBSMTP_USER') + mailserver_pass = os.getenv('UBUSMTP_PASS') or os.getenv('DEBSMTP_PASS') + if mailserver_user and mailserver_pass: + try: + s.login(mailserver_user, mailserver_pass) + except smtplib.SMTPAuthenticationError: + print >> sys.stderr, 'E: Error authenticating to the server: invalid username and password.' + s.quit() + return + except: + print >> sys.stderr, 'E: Unknown SMTP error.' + s.quit() + return + + s.sendmail(myemailaddr, to, mail) + s.quit() + print 'Sync request mailed.'