diff --git a/buildd b/buildd index 22607a6..e2ef86b 100755 --- a/buildd +++ b/buildd @@ -31,8 +31,7 @@ from optparse import OptionParser from urllib import urlencode # ubuntu-dev-tools modules. -sys.path.append("/usr/share/ubuntu-dev-tools/") -import common +from ubuntutools import common # Usage. usage = "%prog \n\n" diff --git a/debian/changelog b/debian/changelog index fde29b0..06192e0 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,64 @@ -ubuntu-dev-tools (0.52) UNRELEASED; urgency=low +ubuntu-dev-tools (0.58) UNRELEASED; urgency=low + + * Changes go here. + + -- Jonathan Davies Sat, 17 Jan 2009 21:04:55 +0000 + +ubuntu-dev-tools (0.57) jaunty; urgency=low + + * requestsync: Skip existing bug check if no credentials are + found (LP: #318120). + + -- Jonathan Davies Sat, 17 Jan 2009 22:02:39 +0000 + +ubuntu-dev-tools (0.56) jaunty; urgency=low + + * manage-credentials: Tighted security by making credentials files and + folder world unreadable. + * common.py: Improved no credentials found error message to show which + consumer token is needed. + * requestsync: Catch credentials error to hide traceback. + * Moved common.py to ubuntutools/ subdirectory to avoid possible conflicts + in Python packaging and fixed all imports as necessary. + * debian/ubuntu-dev-tools.install: Removed common.py entry. + + -- Jonathan Davies Sat, 17 Jan 2009 11:32:33 +0000 + +ubuntu-dev-tools (0.55) jaunty; urgency=low + + * manage-credentials: Use common.py's mkdir function to create as many + subdirectories as necessary for the credentials directory (LP: #317317). + + -- Jonathan Davies Thu, 15 Jan 2009 12:33:31 +0000 + +ubuntu-dev-tools (0.54) jaunty; urgency=low + + * manage-credentials: + - Save credentials to ~/.cache/lp_credentials/ by + default. + - Set service option default to edge. + * doc/manage-credentials.1: Update as necessary for the above. + * common.py: + - When credentials are not found, ask user to see + manage-credentials manpage. + - Load all token files for the consumer specified in the above + directory as necessary. + + -- Jonathan Davies Wed, 14 Jan 2009 19:39:35 +0000 + +ubuntu-dev-tools (0.53) jaunty; urgency=low + + [ Siegfried-Angel Gevatter Pujals ] + * debian/copyright: + - Add information about manage-credentials. + + [ Daniel Holbach ] + * debian/control: replace 'sb-release' with lsb-release, make package + installable again. + + -- Daniel Holbach Wed, 14 Jan 2009 16:27:34 +0100 + +ubuntu-dev-tools (0.52) jaunty; urgency=low [ Siegfried-Angel Gevatter Pujals ] * pbuilder-dist.new: @@ -32,8 +92,23 @@ ubuntu-dev-tools (0.52) UNRELEASED; urgency=low 'status'. * requestsync: If package is new, check the Ubuntu Archive team's bug list for possible duplicate requests. + * doc/manage-credentials.1: Written up. + * doc/requestsync.1: Changed documentation to launchpadlib related-stuff. - -- Siegfried-Angel Gevatter Pujals Sat, 10 Jan 2009 14:35:14 +0100 + [ Luca Falavigna ] + * requestsync: + - Catch AssertionError exception if rmadison returns with an error. + + [ Markus Korn ] + * Added manage-credentials, a tool to create (and manage) credentials + which are used to access launchpad via the API. + * Ported: hugdaylist, massfile, grab-attachment and requestsync to + launchpadlib. + * Other misc. fixes and tweaks. + * Install common.py to correct location with py_modules and remove + hardcoded path from files. + + -- Jonathan Davies Wed, 14 Jan 2009 13:21:35 +0000 ubuntu-dev-tools (0.51) jaunty; urgency=low diff --git a/debian/control b/debian/control index 28f4424..15d01f2 100644 --- a/debian/control +++ b/debian/control @@ -14,8 +14,8 @@ Package: ubuntu-dev-tools Architecture: all Section: devel Depends: ${python:Depends}, binutils, devscripts, sudo, python-debian, - python-launchpad-bugs (>= 0.2.25), dctrl-tools, lsb-release, diffstat, - dpkg-dev, ${misc:Depends} + python-launchpad-bugs (>= 0.2.25), python-launchpadlib, dctrl-tools, + lsb-release, diffstat, dpkg-dev, ${misc:Depends} Recommends: bzr, pbuilder | cowdancer | sbuild, reportbug (>= 3.39ubuntu1), ca-certificates, genisoimage, perl-modules, libwww-perl Conflicts: devscripts (<< 2.10.7ubuntu5) diff --git a/debian/copyright b/debian/copyright index fb5d85e..db79056 100644 --- a/debian/copyright +++ b/debian/copyright @@ -13,6 +13,7 @@ Upstream Authors: Jordan Mantha Kees Cook Luke Yelavich + Markus Korn Martin Pitt Matt Zimmerman Michael Bienia @@ -30,14 +31,15 @@ Copyright: (C) 2006-2007, Albin Tonnerre (C) 2006-2007, Daniel Holbach (C) 2006-2007, Luke Yelavich + (C) 2009, Markus Korn (C) 2007, Martin Pitt (C) 2006-2007, Michael Bienia (C) 2006-2008, Kees Cook (C) 2006-2007, Pete Savage - (C) 2007-2008, Siegfried-A. Gevatter + (C) 2007-2009, Siegfried-A. Gevatter (C) 2007, Terence Simpson (C) 2008, Iain Lane - (C) 2008, Jonathan Davies + (C) 2008-2009, Jonathan Davies (C) 2008, Nathan handler Licenses: @@ -58,9 +60,10 @@ licensed under the GNU General Public License, version 2: On Debian and Ubuntu systems, the complete text of the GNU General Public License v2 can be found in `/usr/share/common-licenses/GPL-2'. -dch-repeat, get-branches, get-build-deps, massfile, mk-sbuild-lv, ppaput, -pull-debian-debdiff, pull-debian-source, pull-lp-source, suspicious-source and what-patch are -licensed under the GNU General Public License, version 3: +dch-repeat, get-branches, get-build-deps, manage-credentials, massfile, +mk-sbuild-lv, ppaput, pull-debian-debdiff, pull-debian-source, pull-lp-source, +suspicious-source and what-patch are licensed under the GNU General Public +License, version 3: 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 @@ -76,5 +79,6 @@ License v3 can be found in `/usr/share/common-licenses/GPL-3'. The following of the scripts can be used, at your option, regarding any later version of the previously specified license: 404main, dch-repeat, dgetlp, -get-build-deps, mk-sbuild-lv, pull-debian-debdiff, pull-debian-source, pull-lp-source, -reverse-build-depends, suspicious-source, what-patch. +get-build-deps, manage-credentials, mk-sbuild-lv, pull-debian-debdiff, +pull-debian-source, pull-lp-source, reverse-build-depends, suspicious-source, +what-patch. diff --git a/debian/ubuntu-dev-tools.install b/debian/ubuntu-dev-tools.install index 5b36808..40fadd6 100644 --- a/debian/ubuntu-dev-tools.install +++ b/debian/ubuntu-dev-tools.install @@ -1,2 +1 @@ bash_completion/* etc/bash_completion.d/ -common.py usr/share/ubuntu-dev-tools/ diff --git a/doc/manage-credentials.1 b/doc/manage-credentials.1 new file mode 100644 index 0000000..922fcba --- /dev/null +++ b/doc/manage-credentials.1 @@ -0,0 +1,76 @@ +.TH MANAGE-CREDENTIALS "1" "13 January 2009" "ubuntu-dev-tools" +.SH NAME +manage-credentials \- a tool to create (and manage) credentials which +are used to access launchpad via the API. +.SH SYNOPSIS +.B manage-credentials create -c [--email --password ] [--service ] +.br +.B manage-credentials \-h +.SH DESCRIPTION +\fBmanage-credentials\fR is a tool to create (and manage) credentials which +are used to access Launchpad via the API. + +.PP +Currently this tool can be used +to create a token with or without using the web UI. In the future, once +related methods are available through the API, this tool can also be used +to manage tokens in launchpad and on the users local machine. + +.SH OPTIONS +Listed below are the command line options for requestsync: +.TP +.B \-h +Display a help message and exit. +.TP +.B \-c \-\-consumer +.TP +.B \-e \-\-email +Your email address as registered on Launchpad. +.TP +.B \-p \-\-password +Your Launchpad password. +.TP +.B \-s \-\-service +If we should use the edge or staging root of the Launchpad API. +.TP +.B \-\-cache +Where to store the cache. +.TP +.B \-o +Which file we should save the credentials to. By default +\fBmanage-credentials\fR writes the credentials tokens to the +~/.cache/lp_credentials/ directory. +.TP +.B \-l \-\-level +A number representing the access-level you wish to give to the new +Launchpad token. 0 is unauthorized, 1 is read public data, 2; write public data, +3; read private data and 4; write private data. + +.SH EXAMPLE USAGE +There are currently two ways of using \fBmanage-credentials\fR to get +Launchpad tokens. +.TP +1) manage-credentials create -c CONSUMER --level 2 + +.TP +This way shall open your webbrowser with a Launchpad login page. + +.TP +2) manage-credentials create -c CONSUMER --level 2 --password BOO --email me@example.com + +.TP +This is a hack, but it works and does not require a webbrowser . + +.TP +If you intend to use manage-credentials for Ubuntu development (such as +the ubuntu-dev-tools package). Please by sure to run the following: + +.TP +manage-credentials create -c ubuntu-dev-tools -l 2 + +.SH AUTHOR +.B manage-credentials +was written by Markus Korn and this manual page was written by +Jonathan Davies . +.PP +Both are released under the GNU General Public License, version 3. diff --git a/doc/requestsync.1 b/doc/requestsync.1 index 27e47d6..ef8db6b 100644 --- a/doc/requestsync.1 +++ b/doc/requestsync.1 @@ -15,10 +15,10 @@ The changelog entry is then downloaded from packages.debian.org. If the sync request is being filed per email (default), a prompt for your GPG passphrase follows so that it can sign the mail and send it off to Launchpad. -Alternatively a sync request can be filed directly using the launchpadbugs -python module (option \fB\-\-lp\fR). +Alternatively a sync request can be filed directly using the launchpadlib +Python module (option \fB\-\-lp\fR). \fBrequestsync\fR falls back to mail the sync request if submitting using -the launchpadbugs module fails. +the launchpadlib module fails. .PP \fBrequestsync\fR checks if you have the permissions to request the sync from @@ -29,10 +29,8 @@ If you are not a member of the appropriate team, the script will subscribe the necessary team with approval rights to the bug report for you. .PP -\fBrequestsync\fR uses a cookie file stored at \fI~/.lpcookie.txt\fR to -authenticate with Launchpad. -This cookie is created on run from the Mozilla Firefox cookie file at -\fI~/.mozilla/*/*/cookies.sqlite\fR. +\fBrequestsync\fR uses launchpadlib authentication to file its requests. Please +see manage-credentials(1) for more information. .SH OPTIONS Listed below are the command line options for requestsync: @@ -55,7 +53,7 @@ configuration (for example: \fI$HOME/.bashrc\fR). This is only used if the sync request is mailed to Launchpad. .TP .B \-\-lp -Use the launchpadbugs python module (packaged as python\-launchpad\-bugs) to +Use the launchpadlib Python module (packaged as python\-launchpadlib) to file the sync request in Launchpad. .TP .B \-s diff --git a/grab-attachments b/grab-attachments index 109a119..ab4dd96 100755 --- a/grab-attachments +++ b/grab-attachments @@ -20,8 +20,7 @@ import os import sys -import urllib -import launchpadbugs.connector as Connector +from ubuntutools.common import get_launchpad USAGE = "grab-attachments " @@ -33,17 +32,21 @@ def main(): if sys.argv[1] in ["--help", "-h"]: print USAGE sys.exit(0) - Bug = Connector.ConnectBug(method="Text") + launchpad = get_launchpad("ubuntu-dev-tools") for arg in sys.argv[1:]: try: number = int(arg) except: print >> sys.stderr, "'%s' is not a valid bug number." % arg sys.exit(1) - b = Bug(number) + b = launchpad.bugs[number] for a in b.attachments: - filename = os.path.join(os.getcwd(), a.url.split("/")[-1]) - urllib.urlretrieve(a.url, filename) + f = a.data.open() + filename = os.path.join(os.getcwd(), f.filename) + local_file = open(filename, "w") + local_file.write(f.read()) + f.close() + local_file.close() if __name__ == '__main__': main() diff --git a/hugdaylist b/hugdaylist index 4498bd7..55a44a8 100755 --- a/hugdaylist +++ b/hugdaylist @@ -35,14 +35,7 @@ import string import sys from optparse import OptionParser -try: - import launchpadbugs.connector as Connector - BugList = Connector.ConnectBugList() - Bug = Connector.ConnectBug(method="Text") -except ImportError: - print >> sys.stderr, \ - "python-launchpad-bugs (>= 0.2.25) needs to be installed to use hugdaylist." - sys.exit(1) +from ubuntutools.common import get_launchpad, translate_web_api, translate_api_web def check_args(): howmany = -1 @@ -74,26 +67,40 @@ def check_args(): return (howmany, url) -def filter_unsolved(b): - bug = Bug(int(b)) +def filter_unsolved(task): + # TODO: don't use this filter here, only check status and assignee of + # the given task # Filter out special types of bugs: # - https://wiki.ubuntu.com/Bugs/HowToTriage#Special%20types%20of%20bugs - return filter(lambda a: a.status != 'Fix Committed' and \ - (a.assignee in ['motu','desktop-bugs'] or \ - not a.assignee), bug.infotable) and \ - 'ubuntu-main-sponsors' not in [str(s) for s in bug.subscribers] and \ - 'ubuntu-universe-sponsors' not in [str(s) for s in bug.subscribers] and \ - 'ubuntu-archive' not in [str(s) for s in bug.subscribers] + subscriptions = set(s.person.name for s in task.bug.subscriptions) #this is expensive, parse name out of self_link instead? + if (task.status != "Fix Committed" and + (not task.assignee or task.assignee.name in ['motu','desktop-bugs']) and + 'ubuntu-main-sponsors' not in subscriptions and + 'ubuntu-universe-sponsors' not in subscriptions and + 'ubuntu-archive' not in subscriptions): + return True + return False def main(): (howmany, url) = check_args() - - try: - bl = BugList(url) - except: - print >> sys.stderr, "The URL at '%s' does not appear to have a bug " \ - "list." % url + if len(url.split("?", 1)) == 2: + # search options not supported, because there is no mapping web ui options <-> API options + print >> sys.stderr, "Options in url are not supported, url: %s" %url sys.exit(1) + + launchpad = get_launchpad("ubuntu-dev-tools") + api_url = translate_web_api(url, launchpad) + try: + product = launchpad.load(api_url) + except Exception, e: + x = getattr(e, "response", {}) + if response.get("status", None) == "404": + print >> sys.stderr, "The URL at '%s' does not appear to be a valid url to a product" %url + sys.exit(1) + else: + raise + + bl = product.searchTasks() l = filter(filter_unsolved, bl) @@ -112,13 +119,14 @@ def main(): || Bug || Subject || Triager ||""" for i in list(l)[:howmany]: + bug = i.bug print '|| [%s %s] || %s || ||' % \ - (i.url, i.bugnumber, i.summary) + (translate_api_web(bug.self_link), bug.id, bug.title) if __name__ == '__main__': try: - main() + main() except KeyboardInterrupt: print >> sys.stderr, "Aborted." sys.exit(1) diff --git a/manage-credentials b/manage-credentials new file mode 100755 index 0000000..75f2c86 --- /dev/null +++ b/manage-credentials @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2009 Markus Korn +# +# ################################################################## +# +# 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; either version 3 +# of the License, or (at your option) any later version. +# +# 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 for more details. +# +# ################################################################## + +import os +import sys +from optparse import OptionParser, make_option +from ubuntutools.common import Credentials, Launchpad, translate_service +from ubuntutools.common import LEVEL, translate_api_web, approve_application +from ubuntutools.common import mkdir + +class CmdOptions(OptionParser): + + USAGE = ( + "\t%prog create -c [--email --password ] [--service ]\n" + ) + + OPTIONS = ( + make_option("-c", "--consumer", action="store", type="string", + dest="consumer", default=None), + make_option("-e", "--email", action="store", type="string", + dest="email", default=None), + make_option("-p", "--password", action="store", type="string", + dest="password", default=None), + make_option("-s", "--service", action="store", type="string", + dest="service", default="edge"), + make_option("--cache", action="store", type="string", + dest="cache", default=None), + make_option("-o", action="store", type="string", + dest="output", default=None), + make_option("-l", "--level", action="store", type="int", + dest="level", default=0, + help="integer representing the access-level (default: 0), mapping: %s" %LEVEL), + ) + + TOOLS = { + "create": ( ("consumer",), + ("email", "password", "service", "cache", "output", + "level")), + "list": (tuple(), ("service", )), + } + + def __init__(self): + OptionParser.__init__(self, option_list=self.OPTIONS) + self.set_usage(self.USAGE) + + def parse_args(self, args=None, values=None): + options, args = OptionParser.parse_args(self, args, values) + given_options = set(i for i, k in self.defaults.iteritems() if not getattr(options, i) == k) + + if not args: + self.error("Please define a sub-tool you would like to use") + if not len(args) == 1: + self.error("Only one sub-tool allowed") + else: + tool = args.pop() + if not tool in self.TOOLS: + self.error("Unknown tool '%s'" %tool) + needed_options = set(self.TOOLS[tool][0]) - given_options + if needed_options: + self.error("Please define the following options: %s" %", ".join(needed_options)) + optional_options = given_options - set(sum(self.TOOLS[tool], ())) + if optional_options: + self.error("The following options are not allowed for this tool: %s" %", ".join(optional_options)) + options.service = translate_service(options.service) + if options.level in LEVEL: + options.level = LEVEL[options.level] + elif options.level.upper() in LEVEL.values(): + options.level = options.level.upper() + else: + self.error("Unknown access-level '%s', level must be in %s" %(options.level, self.LEVEL)) + return tool, options + +def create_credentials(options): + if options.password and options.email: + # use hack + credentials = Credentials(options.consumer) + credentials = approve_application(credentials, options.email, + options.password, options.level, + translate_api_web(options.service), None) + else: + launchpad = Launchpad.get_token_and_login(options.consumer, + options.service, options.cache) + credentials = launchpad.credentials + + if options.output: + filepath = options.output + else: + credentialsDir = os.path.expanduser("~/.cache/lp_credentials") + if not os.path.isdir(credentialsDir): + mkdir(credentialsDir) + os.chmod(credentialsDir, 0700) + filepath = os.path.expanduser("%s/%s-%s.txt" % \ + (credentialsDir, options.consumer, str(options.level).lower())) + + f = open(filepath, "w") + # Make credentials file non-world readable. + os.chmod(filepath, 0600) + credentials.save(f) + f.close() + + print "Credentials sucessfully written to %s." % filepath + + return + +def list_tokens(options): + print "Not implemented yet." + print "To get a list of your tokens, please visit %speople/+me/+oauth-tokens" %translate_api_web(options.service) + return 1 + +def main(): + cmdoptions = CmdOptions() + tool, options = cmdoptions.parse_args() + if tool == "create": + return create_credentials(options) + elif tool == "list": + return list_tokens(options) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/massfile b/massfile index bdb0e00..1e34af7 100755 --- a/massfile +++ b/massfile @@ -27,12 +27,7 @@ import email import subprocess import glob -import launchpadbugs.connector as Connector - -sys.path.append('/usr/share/ubuntu-dev-tools/') -import common - -cookie = common.prepareLaunchpadCookie() +from ubuntutools.common import get_launchpad, translate_api_web, translate_web_api def read_config(): instructions_file = open("instructions") @@ -57,10 +52,6 @@ def read_list(): listfile.close() return pack_list -def file_bug(): - Bug = Connector.ConnectBug() - Bug.authentication = cookie - def check_configfiles(): result = True @@ -87,43 +78,62 @@ def check_configfiles(): def file_bug(config): - Bug = Connector.ConnectBug() - - Bug.authentication = cookie + launchpad = get_launchpad("ubuntu-dev-tools") try: summary = config["subject"].replace("$pack", config["sourcepackage"]) description = config["text"].replace("$pack", config["sourcepackage"]) - bug = Bug.New(product={"name": config["sourcepackage"], - "target": "ubuntu"}, - summary=summary, - description=description) - print "Successfully filed bug %s: https://launchpad.net/bugs/%s" % \ - (bug.bugnumber, bug.bugnumber) - for sub in config["subscribers"].split(","): - if sub.strip("\n").strip(): - bug.subscribers.add(sub.strip("\n").strip()) - for tag in config["tags"].split(","): - if tag.strip("\n").strip(): - bug.tags.append(tag.strip("\n").strip()) - bug.assignee = config["assignee"] + + product_url = "%subuntu/+source/%s" %(launchpad._root_uri, config["sourcepackage"]) + tags = filter(None, map(lambda t: t.strip("\n").strip(), config["tags"].split(","))) + bug = launchpad.bugs.createBug(description=description, title=summary, + target=product_url, tags=tags) + + print "Successfully filed bug %i: %s" %(bug.id, translate_api_web(bug.self_link)) + + subscribers = filter(None, map(lambda t: t.strip("\n").strip(), config["subscribers"].split(","))) + for sub in subscribers: + subscribe_url = "%s~%s" %(launchpad._root_uri, sub) + bug.subscribe(person=subscribe_url) + + #newly created bugreports have one task + task = bug.bug_tasks[0] + if config["status"]: - bug.status = config["status"].capitalize() + status = config["status"].capitalize() else: - bug.status = "Confirmed" - bug.commit() + status = "Confirmed" + task.transitionToStatus(status=status) + + assignee = config["assignee"] + if assignee: + assignee_url = "%s~%s" %(launchpad._root_uri, assignee) + bug.transitionToAssignee(assignee=assignee_url) except: "Bug for '%s' was not filed." % config["sourcepackage"] def read_buglist(url): - BugList = Connector.ConnectBugList() + if not url: + return set() + + if len(url.split("?", 1)) == 2: + # search options not supported, because there is no mapping web ui options <-> API options + print >> sys.stderr, "Options in url are not supported, url: %s" %url + sys.exit(1) + + launchpad = get_launchpad("ubuntu-dev-tools") packages = set() - if url: - buglist = BugList(url) - for bug in buglist.bugs: - packages.add(bug.sourcepackage) + api_url = translate_web_api(url, launchpad) + # workaround LP #303414 + # if this is fixed it should simply be: buglist = launchpad.load(api_url) + api_url = api_url.split("?", 1)[0] + project = launchpad.load(api_url) + buglist = project.searchTasks() + + for bug in buglist: + packages.add(bug.bug_target_name) return packages diff --git a/pull-lp-source b/pull-lp-source index e9fc787..2aa2cab 100755 --- a/pull-lp-source +++ b/pull-lp-source @@ -33,8 +33,7 @@ import urllib2 from optparse import OptionParser # Ubuntu-dev-tools modules. -sys.path.append("/usr/share/ubuntu-dev-tools") -import common +from ubuntutools import common class BackportFromLP: diff --git a/requestsync b/requestsync index dac4367..82c56cb 100755 --- a/requestsync +++ b/requestsync @@ -9,6 +9,7 @@ # Daniel Hahler # Iain Lane # Jonathan Davies +# Markus Korn (python-launchpadlib support) # # ################################################################## # @@ -34,9 +35,7 @@ from debian_bundle.changelog import Version from optparse import OptionParser from time import sleep -# Use functions from ubuntu-dev-tools to create Launchpad cookie file. -sys.path.append('/usr/share/ubuntu-dev-tools/') -import common +from ubuntutools import common launchpad_cookiefile = common.prepareLaunchpadCookie() @@ -54,6 +53,11 @@ def checkNeedsSponsorship(component): The prepareLaunchpadCookie function above shall ensure that a cookie file exists first. """ + # TODO: use launchpadlib here + # Once LP: #313233 has been fixed this can be implemented by either: + # >>> me = launchpad.me + # >>> me.inTeam() #or + # >>> me in urlopener = common.setupLaunchpadUrlOpener(launchpad_cookiefile) # Check where the package is and assign the appropriate variables. @@ -85,52 +89,47 @@ def checkNeedsSponsorship(component): # Is a team member, no sponsorship required. return False -def checkExistingReports(package, isNewPackage): +def checkExistingReports(package): """ Check existing bug reports on Launchpad for a possible sync request. If found ask for confirmation on filing a request. """ - # Determine if the package is new or not. - if isNewPackage: - # Package not in Ubuntu, check Ubuntu Archive Team's bug page for - # possible duplicate reports. - bugListUrl = "https://bugs.launchpad.net/~ubuntu-archive" - else: - # Package in Ubuntu, check the package's bug list for duplicate reports. - bugListUrl = "https://bugs.launchpad.net/ubuntu/+source/%s" % package + + launchpad = None try: - import launchpadbugs.connector as Connector - except: - # Failed to import launchpadbugs - skip check. - print >> sys.stderr, "Unable to import launchpadbugs. Is " \ - "python-launchpad-bugs installed?" + launchpad = common.get_launchpad("ubuntu-dev-tools") + 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 check at:" - print "-", bugListUrl - return + "manually see if there are any at:" + print "- https://bugs.launchpad.net/ubuntu/+source/%s" % package + print "" + return False - # Connect to the bug list. - bugList = Connector.ConnectBugList() - - # Fetch data from Launchpad. - pkgBugList = bugList(bugListUrl) - - if len(pkgBugList) == 0: - return # No bugs found. + # Fetch the package's bug list from Launchpad. + pkg = launchpad.distributions["ubuntu"].getSourcePackage(name=package) + pkgBugList = pkg.searchTasks() # Search bug list for other sync requests. matchingBugs = [bug for bug in pkgBugList if "Please sync %s" % - package in bug.summary] + package in bug.title] if len(matchingBugs) == 0: return # No sync bugs found. - print "The following bugs could possibly be duplicate sync request(s) on Launchpad:" + print "The following bugs could be possible duplicate sync bug(s) on Launchpad:" for bug in matchingBugs: - print " *", bug.summary - print " -", bug.url + print " *", bug.title + print " -", common.translate_api_web(bug.self_link) print "Please check the above URLs to verify this before filing a " \ "possible duplicate report." @@ -317,7 +316,7 @@ def mail_bug(source_package, subscribe, status, bugtitle, bugtext, keyid = None) (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 " \ + print "the '--lp' flag to file a sync request with the launchpadlib " \ "module." return False @@ -343,24 +342,27 @@ def mail_bug(source_package, subscribe, status, bugtitle, bugtext, keyid = None) return True def post_bug(source_package, subscribe, status, bugtitle, bugtext): - '''Use python-launchpad-bugs to submit the sync request. + '''Use python-launchpadlib to submit the sync request. Return True if successfully posted, otherwise False.''' import glob, os.path try: - import launchpadbugs.connector - from launchpadbugs.lpconstants import HTTPCONNECTION + launchpad = common.get_launchpad("ubuntu-dev-tools") except ImportError: - print >> sys.stderr, 'Importing launchpadbugs failed. Is python-launchpad-bugs installed?' + 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 = {'name': source_package, 'target': 'ubuntu'} + product_url = "%subuntu/+source/%s" %(launchpad._root_uri, source_package) else: # new source package - product = {'name': 'ubuntu'} - + product_url = "%subuntu" %launchpad._root_uri + in_confirm_loop = True while in_confirm_loop: print 'Summary:\n%s\n\nDescription:\n%s' % (bugtitle, bugtext) @@ -379,32 +381,17 @@ def post_bug(source_package, subscribe, status, bugtitle, bugtext): print "Invalid answer." # Create bug - Bug = launchpadbugs.connector.ConnectBug() - # Force the usage of stable Launchpad. - Bug.set_connection_mode(HTTPCONNECTION.MODE.STABLE) - - # Use our cookie file for authentication. - Bug.authentication = launchpad_cookiefile - print "Using LP cookie at: %s for authentication." % launchpad_cookiefile - - # Submit bug report. - bug = Bug.New(product = product, summary = bugtitle, description = bugtext) - sleep(2) # Wait in case of slow Launchpad. - - try: - bug.importance = 'Wishlist' - except IOError, s: - print "Warning: setting importance failed: %s" % s + bug = launchpad.bugs.createBug(description=bugtext, title=bugtitle, target=product_url) - bug.status = status - bug.subscriptions.add(subscribe) - sleep(1) # Wait. - - bug.commit() - sleep(1) # Wait. - - print 'Sync request filed as bug #%i: https://launchpad.net/bugs/%i' % (bug.bugnumber, bug.bugnumber) + #newly created bugreports have one task + task = bug.bug_tasks[0] + task.transitionToImportance(importance='Wishlist') + task.transitionToStatus(status=status) + + subscribe_url = "%s~%s" %(launchpad._root_uri, subscribe) + bug.subscribe(person=subscribe_url) + print 'Sync request filed as bug #%i: %s' % (bug.id, common.translate_api_web(bug.self_link)) return True def edit_report(subject, body, changes_required=False): @@ -477,7 +464,7 @@ if __name__ == '__main__': 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 launchpadbugs module for filing " \ + help = "Specify whether to use the launchpadlib module for filing " \ "report.") optParser.add_option("-s", action = "store_true", dest = "sponsor", default = False, @@ -524,8 +511,8 @@ if __name__ == '__main__': # -s flag not specified - check if we do need sponsorship. if not sponsorship: sponsorship = checkNeedsSponsorship(component) - # Check for existing sync requests. - checkExistingReports(srcpkg, newsource) + # Check for existing package reports. + if not newsource: checkExistingReports(srcpkg) # Generate bug report. subscribe = 'ubuntu-archive' diff --git a/setup.py b/setup.py index d1a5242..0f68626 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ setup(name='ubuntu-dev-tools', 'get-build-deps', 'grab-attachments', 'hugdaylist', + 'manage-credentials', 'massfile', 'mk-sbuild-lv', 'pbuilder-dist', @@ -37,7 +38,7 @@ setup(name='ubuntu-dev-tools', 'suspicious-source', 'ubuntu-iso', 'update-maintainer', - 'what-patch', + 'what-patch', ], packages=['ubuntutools'], ) diff --git a/common.py b/ubuntutools/common.py similarity index 67% rename from common.py rename to ubuntutools/common.py index f147441..1ab9094 100644 --- a/common.py +++ b/ubuntutools/common.py @@ -31,6 +31,16 @@ import re import subprocess import sys import urllib2 +import urlparse +import urllib +try: + import httplib2 + from launchpadlib.credentials import Credentials + from launchpadlib.launchpad import Launchpad, STAGING_SERVICE_ROOT, EDGE_SERVICE_ROOT + from launchpadlib.errors import HTTPError +except ImportError: + Credentials = None + Launchpad = None # Clear https_proxy env var as it's not supported in urllib/urllib2; see # LP #122551 @@ -265,3 +275,120 @@ def packageComponent(package, release): component = rel.split('/')[1] return component.strip() + + +def find_credentials(consumer, files, level=None): + """ search for credentials matching 'consumer' in path for given access level. """ + if Credentials is None: + raise ImportError + + for f in files: + cred = Credentials() + try: + cred.load(open(f)) + except: + continue + if cred.consumer.key == consumer: + return cred + + raise IOError("No credentials found for '%s', please see the " \ + "manage-credentials manpage for help on how to create " \ + "one for this consumer." % consumer) + +def get_credentials(consumer, cred_file=None, level=None): + files = list() + + if cred_file: + files.append(cred_file) + + if "LPCREDENTIALS" in os.environ: + files.append(os.environ["LPCREDENTIALS"]) + + files.append(os.path.join(os.getcwd(), "lp_credentials.txt")) + + # Add all files which have our consumer name to file listing. + for x in glob.glob(os.path.expanduser("~/.cache/lp_credentials/%s*.txt" % \ + consumer)): + files.append(x) + + return find_credentials(consumer, files, level) + +def get_launchpad(consumer, server=EDGE_SERVICE_ROOT, cache=None, + cred_file=None, level=None): + credentials = get_credentials(consumer, cred_file, level) + cache = cache or os.environ.get("LPCACHE", None) + return Launchpad(credentials, server, cache) + +def query_to_dict(query_string): + result = dict() + options = filter(None, query_string.split("&")) + for opt in options: + key, value = opt.split("=") + result.setdefault(key, set()).add(value) + return result + +def translate_web_api(url, launchpad): + scheme, netloc, path, query, fragment = urlparse.urlsplit(url) + query = query_to_dict(query) + if not (("edge" in netloc and "edge" in str(launchpad._root_uri)) + or ("staging" in netloc and "staging" in str(launchpad._root_uri))): + raise ValueError("url conflict (url: %s, root: %s" %(url, launchpad._root_uri)) + if path.endswith("/+bugs"): + path = path[:-6] + if "ws.op" in query: + raise ValueError("Invalid web url, url: %s" %url) + query["ws.op"] = "searchTasks" + scheme, netloc, api_path, _, _ = urlparse.urlsplit(str(launchpad._root_uri)) + query = urllib.urlencode(query) + url = urlparse.urlunsplit((scheme, netloc, api_path + path.lstrip("/"), query, fragment)) + return url + +def translate_api_web(self_url): + return self_url.replace("api.", "").replace("beta/", "") + +LEVEL = { + 0: "UNAUTHORIZED", + 1: "READ_PUBLIC", + 2: "WRITE_PUBLIC", + 3: "READ_PRIVATE", + 4: "WRITE_PRIVATE" +} + +def approve_application(credentials, email, password, level, web_root, + context): + authorization_url = credentials.get_request_token(context, web_root) + if level in LEVEL: + level = 'field.actions.%s' %LEVEL[level] + elif level in LEVEL.values(): + level = 'field.actions.%s' %level + elif str(level).startswith("field.actions") and str(level).split(".")[-1] in LEVEL: + pass + else: + raise ValueError("Unknown access level '%s'" %level) + + params = {level: 1, + "oauth_token": credentials._request_token.key, + "lp.context": context or ""} + + lp_creds = ":".join((email, password)) + basic_auth = "Basic %s" %(lp_creds.encode('base64')) + headers = {'Authorization': basic_auth} + response, content = httplib2.Http().request(authorization_url, + method="POST", body=urllib.urlencode(params), headers=headers) + if int(response["status"]) != 200: + if not 300 <= int(response["status"]) <= 400: # this means redirection + raise HTTPError(response, content) + credentials.exchange_request_token_for_access_token(web_root) + return credentials + +def translate_service(service): + _service = service.lower() + if _service in (STAGING_SERVICE_ROOT, EDGE_SERVICE_ROOT): + return _service + elif _service == "edge": + return EDGE_SERVICE_ROOT + elif _service == "staging": + return STAGING_SERVICE_ROOT + else: + raise ValueError("unknown service '%s'" %service) +