diff --git a/debian/changelog b/debian/changelog index c72bd45..d6b53ca 100644 --- a/debian/changelog +++ b/debian/changelog @@ -22,10 +22,11 @@ ubuntu-dev-tools (0.137) UNRELEASED; urgency=low - Do the report boiler-plate checking in a script that wraps an editor, so that we only edit the report once, after checking for duplicates. - rm the tmpdir with a little more force (shutil.rmtree) (LP: #899399) + * New Tool: ubuntu-upload-permission: Query upload permissions (LP: #876554) [ Andreas Moog ] * sponsor-patch: Check permission to unsubscribe sponsors-team (LP: #896884) - * grep-merges: We already require a UTF-8 enabled terminal, so encode + * grep-merges: We already require a UTF-8 enabled terminal, so encode package and uploader name in UTF-8 (LP: #694388) * requestsync: Give user option to retry in case of temporary error (LP: #850360) diff --git a/debian/control b/debian/control index e15fd97..7255fb1 100644 --- a/debian/control +++ b/debian/control @@ -68,6 +68,7 @@ Description: useful tools for Ubuntu developers . - 404main - used to check what components a package's deps are in, for doing a main inclusion report for example. + - backportpackage - helper to test package backports - bitesize - add the 'bitesize' tag to a bug and comment that you are willing to help fix it. - check-mir - check support status of build/binary dependencies @@ -105,4 +106,6 @@ Description: useful tools for Ubuntu developers - ubuntu-build - give commands to the Launchpad build daemons from the command line. - ubuntu-iso - output information of an Ubuntu ISO image. + - ubuntu-upload-permission - query / list the upload permissions for a + package. - update-maintainer - script to update maintainer field in ubuntu packages. diff --git a/debian/copyright b/debian/copyright index 98ffaf6..c4de0f3 100644 --- a/debian/copyright +++ b/debian/copyright @@ -150,6 +150,7 @@ Files: doc/pull-debian-debdiff.1 doc/reverse-depends.1 doc/sponsor-patch.1 doc/ubuntu-dev-tools.5 + doc/ubuntu-upload-permission.1 doc/update-maintainer.1 enforced-editing-wrapper pull-debian-debdiff @@ -158,6 +159,7 @@ Files: doc/pull-debian-debdiff.1 reverse-depends sponsor-patch test-data/* + ubuntu-upload-permission ubuntutools/archive.py ubuntutools/builder.py ubuntutools/config.py diff --git a/doc/ubuntu-upload-permission.1 b/doc/ubuntu-upload-permission.1 new file mode 100644 index 0000000..4d97b03 --- /dev/null +++ b/doc/ubuntu-upload-permission.1 @@ -0,0 +1,60 @@ +.\" Copyright (C) 2011, Stefano Rivera +.\" +.\" Permission to use, copy, modify, and/or distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +.\" REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +.\" AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +.\" INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +.\" LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +.\" OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +.\" PERFORMANCE OF THIS SOFTWARE. +.TH ubuntu\-upload\-permission 1 "November 2011" ubuntu\-dev\-tools + +.SH NAME +ubuntu\-upload\-permission \- Query upload rights and (optionally) list +the people and teams with upload rights for a package + +.SH SYNOPSIS +.B ubuntu\-upload\-permission \fR[\fIoptions\fR] \fIpackage + +.SH DESCRIPTION +\fBubuntu\-upload\-permission\fR checks if the user has upload +permissions for \fIpackage\fR. +If the \fB\-\-list\-uploaders\fR option is provided, all the people and +teams that do have upload rights for \fIpackage\fR will be listed. + +.SH OPTIONS +.TP +\fB\-r\fR \fIRELEASE\fR, \fB\-\-release\fR=\fIRELEASE\fR +Query permissions in \fIRELEASE\fR. +Default: current development release. +.TP +\fB\-a\fR, \fB\-\-list\-uploaders\fR +List all the people and teams who have upload rights for \fIpackage\fR. +.TP +\fB\-t\fR, \fB\-\-list\-team\-members\fR +List all the members of every team with rights. (Implies +\fB\-\-list\-uploaders\fR) +.TP +\fB\-h\fR, \fB\-\-help\fR +Display a help message and exit + +.SH EXIT STATUS +.TP +.B 0 +You have the necessary upload rights. +.TP +.B 1 +You don't have the necessary upload rights. +.TP +.B 2 +There was an error. + +.SH AUTHORS +\fBubuntu\-upload\-permission\fR and this manpage were written by +Stefano Rivera . +.PP +Both are released under the terms of the ISC License. diff --git a/setup.py b/setup.py index 558b8e1..6c895e5 100755 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ scripts = ['404main', 'syncpackage', 'ubuntu-build', 'ubuntu-iso', + 'ubuntu-upload-permission', 'update-maintainer', ] diff --git a/ubuntu-upload-permission b/ubuntu-upload-permission new file mode 100755 index 0000000..2df06bd --- /dev/null +++ b/ubuntu-upload-permission @@ -0,0 +1,145 @@ +#!/usr/bin/python +# +# Copyright (C) 2011, Stefano Rivera +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import optparse +import sys + +from devscripts.logger import Logger + +from ubuntutools.lp.lpapicache import (Launchpad, Distribution, PersonTeam, + Packageset, PackageNotFoundException, + SeriesNotFoundException) +from ubuntutools.misc import split_release_pocket + + +def parse_arguments(): + '''Parse arguments and return (options, package)''' + parser = optparse.OptionParser('%prog [options] package') + parser.add_option('-r', '--release', default=None, metavar='RELEASE', + help='Use RELEASE, rather than the current development ' + 'release') + parser.add_option('-a', '--list-uploaders', + default=False, action='store_true', + help='List all the people/teams with upload rights') + parser.add_option('-t', '--list-team-members', + default=False, action='store_true', + help='List all team members of teams with upload rights ' + '(implies --list-uploaders)') + options, args = parser.parse_args() + + if len(args) != 1: + parser.error("One (and only one) package must be specified") + package = args[0] + + if options.list_team_members: + options.list_uploaders = True + + return (options, package) + + +def main(): + '''Query upload permissions''' + options, package = parse_arguments() + # Need to be logged in to see uploaders: + Launchpad.login() + + ubuntu = Distribution('ubuntu') + archive = ubuntu.getArchive() + if options.release is None: + options.release = ubuntu.getDevelopmentSeries().name + try: + release, pocket = split_release_pocket(options.release) + series = ubuntu.getSeries(release) + except SeriesNotFoundException, e: + Logger.error(str(e)) + sys.exit(2) + + try: + spph = archive.getSourcePackage(package) + except PackageNotFoundException, e: + Logger.error(str(e)) + sys.exit(2) + component = spph.getComponent() + if (options.list_uploaders and (pocket != 'Release' or series.status in + ('Experimental', 'Active Development', 'Pre-release Freeze'))): + + component_uploader = archive.getUploadersForComponent( + component_name=component)[0] + print "All upload permissions for %s:" % package + print + print "Component (%s)" % component + print "============" + ("=" * len(component)) + print_uploaders([component_uploader], options.list_team_members) + + packagesets = sorted(Packageset.setsIncludingSource( + distroseries=series, + sourcepackagename=package)) + if packagesets: + print + print "Packagesets" + print "===========" + for packageset in packagesets: + print + print "%s:" % packageset.name + print_uploaders(archive.getUploadersForPackageset( + packageset=packageset), options.list_team_members) + + ppu_uploaders = archive.getUploadersForPackage( + source_package_name=package) + if ppu_uploaders: + print + print "Per-Package-Uploaders" + print "=====================" + print + print_uploaders(ppu_uploaders, options.list_team_members) + print + + if PersonTeam.me.canUploadPackage(archive, series, package, component, + pocket): + print "You can upload %s to %s." % (package, options.release) + else: + print ("You can not upload %s to %s, yourself." + % (package, options.release)) + if (series.status in ('Current Stable Release', 'Supported', 'Obsolete') + and pocket == 'Release'): + print ("%s is in the '%s' state. " + "You may want to query the %s-proposed pocket." + % (release, series.status, release)) + else: + print ("But you can still contribute to it via the sponsorship " + "process: https://wiki.ubuntu.com/SponsorshipProcess") + if not options.list_uploaders: + print ("To see who has the necessary upload rights, " + "use the --list-uploaders option.") + sys.exit(1) + + +def print_uploaders(uploaders, expand_teams=False, prefix=''): + """Given a list of uploaders, pretty-print them all + Each line is prefixed with prefix. + If expand_teams is set, recurse, adding more spaces to prefix on each + recursion. + """ + for uploader in sorted(uploaders, key=lambda p: p.display_name): + print ("%s* %s (%s)%s" + % (prefix, uploader.display_name, uploader.name, + ' [team]' if uploader.is_team else '')) + if expand_teams and uploader.is_team: + print_uploaders(uploader.participants, True, prefix=prefix + ' ') + + +if __name__ == '__main__': + main() diff --git a/ubuntutools/lp/lpapicache.py b/ubuntutools/lp/lpapicache.py index f284d09..5906c48 100644 --- a/ubuntutools/lp/lpapicache.py +++ b/ubuntutools/lp/lpapicache.py @@ -54,6 +54,7 @@ __all__ = [ _POCKETS = ('Release', 'Security', 'Updates', 'Proposed', 'Backports') + class _Launchpad(object): '''Singleton for LP API access.''' @@ -116,7 +117,7 @@ class BaseWrapper(object): A base class from which other wrapper classes are derived. ''' __metaclass__ = MetaWrapper - resource_type = None # it's a base class after all + resource_type = None # it's a base class after all def __new__(cls, data): if (isinstance(data, basestring) and @@ -278,11 +279,11 @@ class Archive(BaseWrapper): resource_type = 'archive' def __init__(self, *args): - # Don't share between different Archives - if '_binpkgs' not in self.__dict__: - self._binpkgs = dict() - if '_srcpkgs' not in self.__dict__: - self._srcpkgs = dict() + self._binpkgs = {} + self._srcpkgs = {} + self._pkg_uploaders = {} + self._pkgset_uploaders = {} + self._component_uploaders = {} def getSourcePackage(self, name, series=None, pocket=None): ''' @@ -400,6 +401,45 @@ class Archive(BaseWrapper): include_binaries=include_binaries ) + def getUploadersForComponent(self, component_name): + '''Get the list of PersonTeams who can upload packages in the + specified component. + [Note: the permission records, themselves, aren't exposed] + ''' + if component_name not in self._component_uploaders: + self._component_uploaders[component_name] = sorted(set( + PersonTeam(permission.person_link) + for permission in self._lpobject.getUploadersForComponent( + component_name=component_name + ))) + return self._component_uploaders[component_name] + + def getUploadersForPackage(self, source_package_name): + '''Get the list of PersonTeams who can upload source_package_name) + [Note: the permission records, themselves, aren't exposed] + ''' + if source_package_name not in self._pkg_uploaders: + self._pkg_uploaders[source_package_name] = sorted(set( + PersonTeam(permission.person_link) + for permission in self._lpobject.getUploadersForPackage( + source_package_name=source_package_name + ))) + return self._pkg_uploaders[source_package_name] + + def getUploadersForPackageset(self, packageset, direct_permissions=False): + '''Get the list of PersonTeams who can upload packages in packageset + [Note: the permission records, themselves, aren't exposed] + ''' + key = (packageset, direct_permissions) + if key not in self._pkgset_uploaders: + self._pkgset_uploaders[key] = sorted(set( + PersonTeam(permission.person_link) + for permission in self._lpobject.getUploadersForPackageset( + packageset=packageset._lpobject, + direct_permissions=direct_permissions, + ))) + return self._pkgset_uploaders[key] + class SourcePackagePublishingHistory(BaseWrapper): ''' @@ -585,6 +625,7 @@ class MetaPersonTeam(MetaWrapper): raise return cls._me + class PersonTeam(BaseWrapper): ''' Wrapper class around a LP person or team object. @@ -687,7 +728,7 @@ class Build(BaseWrapper): def rescore(self, score): if self.can_be_rescored: - self().rescore(score = score) + self().rescore(score=score) return True return False @@ -703,3 +744,36 @@ class DistributionSourcePackage(BaseWrapper): Caching class for distribution_source_package objects. ''' resource_type = 'distribution_source_package' + + +class Packageset(BaseWrapper): + ''' + Caching class for packageset objects. + ''' + resource_type = 'packageset' + _lp_packagesets = None + _source_sets = {} + + @classmethod + def setsIncludingSource(cls, sourcepackagename, distroseries=None, + direct_inclusion=False): + '''Get the package sets including sourcepackagename''' + + if cls._lp_packagesets is None: + cls._lp_packagesets = Launchpad.packagesets + + key = (sourcepackagename, distroseries, direct_inclusion) + if key not in cls._source_sets: + params = { + 'sourcepackagename': sourcepackagename, + 'direct_inclusion': direct_inclusion, + } + if distroseries is not None: + params['distroseries'] = distroseries._lpobject + + cls._source_sets[key] = [ + Packageset(packageset) for packageset + in cls._lp_packagesets.setsIncludingSource(**params) + ] + + return cls._source_sets[key]