Merge ppa-dev-tools:set-command-private-ppa into ppa-dev-tools:main

Proposed by Bryce Harrington
Status: Merged
Merge reported by: Bryce Harrington
Merged at revision: a116288136ae712e726d5e8f7830b64ca77830ba
Proposed branch: ppa-dev-tools:set-command-private-ppa
Merge into: ppa-dev-tools:main
Diff against target: 395 lines (+173/-31)
7 files modified
ppa/ppa.py (+24/-0)
ppa/ppa_group.py (+3/-1)
scripts/ppa (+43/-21)
tests/helpers.py (+5/-2)
tests/smoketest_ppa_set (+70/-0)
tests/test_ppa_group.py (+10/-0)
tests/test_scripts_ppa.py (+18/-7)
Reviewer Review Type Date Requested Status
Lena Voytek (community) Approve
Canonical Server packageset reviewers Pending
PpaDevTools Developers Pending
Canonical Server Reporter Pending
Review via email: mp+447032@code.launchpad.net

Description of the change

Implements support for creating private PPAs and toggling PPAs public vs. private.

Creating private PPAs requires special authorization that I don't seem to have, with the result that I've not been able to verify that this actually works correctly "live". However, I've included checks for some of the authorization errors I've hit, and implemented a slew of unit tests. I'll try to find someone with access to do the live verification, but apart from that I think this code is ready for review and landing.

As usual, to run the testsuite, do:

    $ make check

or

    $ pytest-3

And the smoketest has been updated to include toggling --private:

    $ ./tests/smoketest_ppa_set

However as mentioned, unless you have authorization to modify privacy on PPAs that will merely generate errors.

To post a comment you must log in.
Revision history for this message
Lena Voytek (lvoytek) wrote :

LGTM! just one note for one of the help strings

review: Approve
Revision history for this message
Bryce Harrington (bryce) wrote :

Thanks Lena, I've squashed that change in, updated this branch, and landed to main.

stirling: ~/src/PpaDevTools/ppa-dev-tools-commands-set$ git merge --ff-only set-command-private-ppa
Updating 4fa781a..0f7a075
Fast-forward
 ppa/ppa.py | 24 ++++++++++++++++++++++++
 ppa/ppa_group.py | 4 +++-
 scripts/ppa | 64 +++++++++++++++++++++++++++++++++++++++++++---------------------
 tests/helpers.py | 7 +++++--
 tests/smoketest_ppa_set | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 tests/test_ppa_group.py | 10 ++++++++++
 tests/test_scripts_ppa.py | 25 ++++++++++++++++++-------
 7 files changed, 173 insertions(+), 31 deletions(-)
 create mode 100755 tests/smoketest_ppa_set
stirling: ~/src/PpaDevTools/ppa-dev-tools-commands-set$ git push
Enumerating objects: 57, done.
Counting objects: 100% (57/57), done.
Delta compression using up to 12 threads
Compressing objects: 100% (40/40), done.
Writing objects: 100% (47/47), 6.94 KiB | 6.94 MiB/s, done.
Total 47 (delta 29), reused 0 (delta 0), pack-reused 0
To git+ssh://git.launchpad.net/ppa-dev-tools
   4fa781a..0f7a075 main -> main

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/ppa/ppa.py b/ppa/ppa.py
index 2dd46eb..c4ce1fe 100755
--- a/ppa/ppa.py
+++ b/ppa/ppa.py
@@ -198,6 +198,30 @@ class Ppa:
198198
199 @property199 @property
200 @lru_cache200 @lru_cache
201 def is_private(self) -> bool:
202 """Indicates if the PPA is private or public.
203
204 :rtype: bool
205 :returns: True if the archive is private, False if public.
206 """
207 return self.archive.private
208
209 def set_private(self, private: bool):
210 """Attempts to configure the PPA as private.
211
212 Note that PPAs can't be changed to private if they ever had any
213 sources published, or if the owning person or team is not
214 permitted to hold private PPAs.
215
216 :param bool private: Whether the PPA should be private or public.
217 """
218 if private is None:
219 return
220 self.archive.private = private
221 self.archive.lp_save()
222
223 @property
224 @lru_cache
201 def publish(self):225 def publish(self):
202 return self.archive.publish226 return self.archive.publish
203227
diff --git a/ppa/ppa_group.py b/ppa/ppa_group.py
index c9747a5..3061100 100755
--- a/ppa/ppa_group.py
+++ b/ppa/ppa_group.py
@@ -88,13 +88,14 @@ class PpaGroup:
88 """88 """
89 return self.service.people[self.name]89 return self.service.people[self.name]
9090
91 def create(self, ppa_name='ppa', ppa_description=None):91 def create(self, ppa_name='ppa', ppa_description=None, private=False):
92 """Registers a new PPA with Launchpad.92 """Registers a new PPA with Launchpad.
9393
94 If a description is not provided a default one will be generated.94 If a description is not provided a default one will be generated.
9595
96 :param str ppa_name: Name for the PPA to create.96 :param str ppa_name: Name for the PPA to create.
97 :param str ppa_description: Text description of the PPA.97 :param str ppa_description: Text description of the PPA.
98 :param bool private: Limit access to PPA to only subscribed users and owner.
98 :rtype: Ppa99 :rtype: Ppa
99 :returns: A Ppa object that describes the created PPA.100 :returns: A Ppa object that describes the created PPA.
100101
@@ -103,6 +104,7 @@ class PpaGroup:
103 ppa_settings = {104 ppa_settings = {
104 'description': ppa_description,105 'description': ppa_description,
105 'displayname': ppa_name,106 'displayname': ppa_name,
107 'private': private
106 }108 }
107109
108 try:110 try:
diff --git a/scripts/ppa b/scripts/ppa
index 67dec11..ea624e0 100755
--- a/scripts/ppa
+++ b/scripts/ppa
@@ -55,7 +55,7 @@ from inspect import currentframe
55from textwrap import indent55from textwrap import indent
56from typing import Any56from typing import Any
57from distro_info import UbuntuDistroInfo57from distro_info import UbuntuDistroInfo
58from lazr.restfulclient.errors import BadRequest58from lazr.restfulclient.errors import BadRequest, Unauthorized
5959
60try:60try:
61 from ruamel import yaml61 from ruamel import yaml
@@ -193,33 +193,37 @@ def add_basic_config_options(parser: argparse.ArgumentParser) -> None:
193 help="A short description of the archive. URLs will be rendered as links. (See also 'ppa desc'.)"193 help="A short description of the archive. URLs will be rendered as links. (See also 'ppa desc'.)"
194 )194 )
195195
196 # Enable/Disable uploading196 # Dependencies
197 parser.add_argument(197 parser.add_argument(
198 '--enable',198 '--ppa-dependencies', '--ppa-depends',
199 dest="set_enabled",199 dest="ppa_dependencies", action='store',
200 action='store_true',200 help="The set of other PPAs this PPA should use for satisfying build dependencies."
201 help="Accept and build packages uploaded to the PPA."
202 )201 )
202
203 # Public/Private access
203 parser.add_argument(204 parser.add_argument(
204 '--disable',205 '--private', '-P',
205 dest="set_disabled",206 dest="private",
206 action='store_true',207 action='store_true',
207 help="Do not accept or build packages uploaded to the PPA."208 help=(
209 "Restrict access to the PPA to its owner and subscribers. " +
210 "This can only be changed if the archive has never had any " +
211 "sources published and the owner/group has permission to do so."
212 )
208 )213 )
209
210 # Dependencies
211 parser.add_argument(214 parser.add_argument(
212 '--ppa-dependencies', '--ppa-depends',215 '--public',
213 dest="ppa_dependencies", action='store',216 dest="private",
214 help="The set of other PPAs this PPA should use for satisfying build dependencies."217 action='store_false',
218 help="Allow access to the PPA to anyone."
215 )219 )
216220
221 # Publishing
217 parser.add_argument(222 parser.add_argument(
218 '--publish',223 '--publish',
219 dest="publish", action='store_true',224 dest="publish", action='store_true',
220 help=("Allow built packages in the PPA to be made available for download.")225 help=("Allow built packages in the PPA to be made available for download.")
221 )226 )
222
223 parser.add_argument(227 parser.add_argument(
224 '--no-publish',228 '--no-publish',
225 dest="publish", action='store_false',229 dest="publish", action='store_false',
@@ -304,7 +308,7 @@ def create_arg_parser() -> argparse.ArgumentParser:
304 set_parser = subparser.add_parser(308 set_parser = subparser.add_parser(
305 'set',309 'set',
306 argument_default=argparse.SUPPRESS,310 argument_default=argparse.SUPPRESS,
307 help='set help',311 help='Applies one or more configuration changes to the PPA.',
308 prog=progname,312 prog=progname,
309 )313 )
310 add_global_options(set_parser)314 add_global_options(set_parser)
@@ -429,7 +433,7 @@ def create_config(lp: Lp, args: argparse.Namespace) -> dict[str, Any]:
429 config = dict(DEFAULT_CONFIG)433 config = dict(DEFAULT_CONFIG)
430434
431 # Map all non-empty elements from argparse Namespace into config dict435 # Map all non-empty elements from argparse Namespace into config dict
432 config.update({k: v for k, v in vars(args).items() if v})436 config.update({k: v for k, v in vars(args).items() if v is not None})
433437
434 # Use defaults for any remaining parameters not yet configured438 # Use defaults for any remaining parameters not yet configured
435 for k, v in DEFAULT_CONFIG.items():439 for k, v in DEFAULT_CONFIG.items():
@@ -498,8 +502,6 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
498 warn("Could not determine owning person or team LP username")502 warn("Could not determine owning person or team LP username")
499 return os.EX_USAGE503 return os.EX_USAGE
500504
501 publish = config.get('publish', None)
502
503 architectures = config.get('architectures', ARCHES_PPA_ALL)505 architectures = config.get('architectures', ARCHES_PPA_ALL)
504 if type(architectures) is str:506 if type(architectures) is str:
505 architectures = unpack_to_dict(architectures).keys()507 architectures = unpack_to_dict(architectures).keys()
@@ -507,8 +509,12 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
507 try:509 try:
508 if not config.get('dry_run', False):510 if not config.get('dry_run', False):
509 ppa_group = PpaGroup(service=lp, name=owner_name)511 ppa_group = PpaGroup(service=lp, name=owner_name)
510 the_ppa = ppa_group.create(ppa_name, ppa_description=description)512 the_ppa = ppa_group.create(
511 the_ppa.set_publish(publish)513 ppa_name,
514 ppa_description=description,
515 private=config.get('private', False)
516 )
517 the_ppa.set_publish(config.get('publish', None))
512 if architectures:518 if architectures:
513 the_ppa.set_architectures(architectures)519 the_ppa.set_architectures(architectures)
514 arch_str = ', '.join(the_ppa.architectures)520 arch_str = ', '.join(the_ppa.architectures)
@@ -534,6 +540,12 @@ def command_create(lp: Lp, config: dict[str, str]) -> int:
534 print(" sudo add-apt-repository -yus {}".format(the_ppa.address))540 print(" sudo add-apt-repository -yus {}".format(the_ppa.address))
535 print(" sudo apt-get install <package(s)>")541 print(" sudo apt-get install <package(s)>")
536 return os.EX_OK542 return os.EX_OK
543 except Unauthorized as e:
544 error(f"Insufficient authorization to create '{ppa_name}' under ownership of '{owner_name}'.")
545 return os.EX_NOPERM
546 except KeyError as e:
547 error(f"No such person or team '{owner_name}'")
548 return os.EX_NOUSER
537 except PpaAlreadyExists as e:549 except PpaAlreadyExists as e:
538 warn(o2str(e.message))550 warn(o2str(e.message))
539 return 3551 return 3
@@ -678,9 +690,18 @@ def command_set(lp: Lp, config: dict[str, str]) -> int:
678 if 'publish' in config:690 if 'publish' in config:
679 the_ppa.archive.publish = config.get('publish')691 the_ppa.archive.publish = config.get('publish')
680692
693 the_ppa.set_private(config.get('private', None))
694
681 return the_ppa.archive.lp_save()695 return the_ppa.archive.lp_save()
696 except Unauthorized as e:
697 if b'private' in e.content:
698 error(f"Insufficient authorization to change privacy for PPA '{the_ppa.name}'.")
699 else:
700 error(f"Insufficient authorization to modify PPA '{the_ppa.name}'.")
701 return os.EX_NOPERM
682 except PpaDoesNotExist as e:702 except PpaDoesNotExist as e:
683 print(e)703 print(e)
704 return 1
684 except ValueError as e:705 except ValueError as e:
685 print(f"Error: {e}")706 print(f"Error: {e}")
686 return os.EX_USAGE707 return os.EX_USAGE
@@ -819,6 +840,7 @@ def command_wait(lp: Lp, config: dict[str, str]) -> int:
819 return os.EX_OK840 return os.EX_OK
820 except PpaDoesNotExist as e:841 except PpaDoesNotExist as e:
821 print(e)842 print(e)
843 return 1
822 except ValueError as e:844 except ValueError as e:
823 print(f"Error: {e}")845 print(f"Error: {e}")
824 return os.EX_USAGE846 return os.EX_USAGE
diff --git a/tests/helpers.py b/tests/helpers.py
index c607d3c..99d5e38 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -44,8 +44,9 @@ class ArchiveMock:
44 self.displayname = name44 self.displayname = name
45 self.description = description45 self.description = description
46 self.owner = owner46 self.owner = owner
47 self.publish = True47 self.private = False
48 self.processors = [ProcessorMock(proc_name) for proc_name in ARCHES_PPA_DEFAULT]48 self.processors = [ProcessorMock(proc_name) for proc_name in ARCHES_PPA_DEFAULT]
49 self.publish = True
49 self.published_sources = []50 self.published_sources = []
5051
51 def setProcessors(self, processors):52 def setProcessors(self, processors):
@@ -64,12 +65,14 @@ class PersonMock:
64 self.name = name65 self.name = name
65 self._ppas = []66 self._ppas = []
6667
67 def createPPA(self, name, description, displayname):68 def createPPA(self, name, description, displayname, private=None):
68 for ppa in self._ppas:69 for ppa in self._ppas:
69 if ppa.name == name:70 if ppa.name == name:
70 raise PpaAlreadyExists(name)71 raise PpaAlreadyExists(name)
71 ppa = Ppa(name, self.name, description)72 ppa = Ppa(name, self.name, description)
72 Ppa.archive = ArchiveMock(ppa.name, ppa.description, self)73 Ppa.archive = ArchiveMock(ppa.name, ppa.description, self)
74 if isinstance(private, bool):
75 Ppa.archive.private = private
73 self._ppas.append(ppa)76 self._ppas.append(ppa)
74 return True77 return True
7578
diff --git a/tests/smoketest_ppa_set b/tests/smoketest_ppa_set
76new file mode 10075579new file mode 100755
index 0000000..7724d27
--- /dev/null
+++ b/tests/smoketest_ppa_set
@@ -0,0 +1,70 @@
1#!/bin/bash
2
3testsdir=$(dirname "$(readlink -f "${0}")")
4progdir="${testsdir}/../scripts"
5script="${progdir}/ppa"
6ppa_name="ppa-test-set-${RANDOM}"
7
8[ -e "${script}" ] || {
9 echo "Could not locate ppa command '${script}'."
10 exit 1
11}
12
13ppa_create() {
14 echo -n "* Creating ${ppa_name} .."
15 if "${script}" create "${ppa_name}"; then
16 echo ". OK"
17 else
18 echo "! ERROR"
19 exit 1
20 fi
21}
22
23ppa_destroy() {
24 echo -n "* Destroying ${ppa_name} .."
25 if "${script}" destroy "${ppa_name}"; then
26 echo ". OK"
27 else
28 echo "! ERROR"
29 exit 1
30 fi
31}
32
33ppa_status() {
34 "${script}" status "${ppa_name}"
35}
36
37ppa_set() {
38 echo -n "* Setting ${1} .."
39 if "${script}" set "${ppa_name}" "${@}"; then
40 echo ". OK"
41 else
42 echo "! ERROR"
43 fi
44}
45
46echo "### Default configuration ###"
47ppa_create && \
48 trap "ppa_destroy" EXIT
49ppa_status
50
51echo
52echo "### Set parameters ###"
53ppa_set --displayname "parameters-set"
54ppa_set --description "Updated PPA with various settings changed"
55ppa_set --all-architectures
56ppa_set --no-publish
57ppa_status
58
59echo
60echo "### Restore to default parameters ###"
61ppa_set --displayname "${ppa_name}"
62ppa_set --description ""
63ppa_set --default-architectures
64ppa_set --publish
65ppa_status
66
67echo
68echo "### Private PPA ###"
69ppa_set --private
70ppa_set --public
diff --git a/tests/test_ppa_group.py b/tests/test_ppa_group.py
index f53799f..6539c3a 100644
--- a/tests/test_ppa_group.py
+++ b/tests/test_ppa_group.py
@@ -69,6 +69,16 @@ def test_create_with_owner():
69 assert ppa.address == 'ppa:test_owner_name/ppa_test_name'69 assert ppa.address == 'ppa:test_owner_name/ppa_test_name'
7070
7171
72def test_create_private():
73 """Check creating a private PPA."""
74 lp = LpServiceMock()
75 ppa_group = PpaGroup(service=lp, name='me')
76 ppa = ppa_group.create('private_ppa', private=True)
77 assert ppa is not None
78 assert ppa.address == 'ppa:me/private_ppa'
79 assert ppa.is_private is True
80
81
72def test_list_ppas():82def test_list_ppas():
73 """Check listing the PPAs for a PPA group."""83 """Check listing the PPAs for a PPA group."""
74 test_ppa_list = ['a', 'b', 'c', 'd']84 test_ppa_list = ['a', 'b', 'c', 'd']
diff --git a/tests/test_scripts_ppa.py b/tests/test_scripts_ppa.py
index f82a9e4..93988d0 100644
--- a/tests/test_scripts_ppa.py
+++ b/tests/test_scripts_ppa.py
@@ -232,13 +232,16 @@ def test_create_arg_parser_basic_config(command):
232 assert args.description == 'x'232 assert args.description == 'x'
233 args.description = None233 args.description = None
234234
235 # Check --enable / --disable235 # Check --public / -P|--private
236 args = parser.parse_args([command, 'test-ppa', '--enable'])236 args = parser.parse_args([command, 'test-ppa', '--public'])
237 assert args.set_enabled is True237 assert args.private is False
238 args.set_enabled = False238 args.public = None
239 args = parser.parse_args([command, 'test-ppa', '--disable'])239 args = parser.parse_args([command, 'test-ppa', '--private'])
240 assert args.set_disabled is True240 assert args.private is True
241 args.set_disabled = False241 args.private = None
242 args = parser.parse_args([command, 'test-ppa', '-P'])
243 assert args.private is True
244 args.private = None
242245
243 # Check --ppa-dependencies <PPA[,...]>246 # Check --ppa-dependencies <PPA[,...]>
244 args = parser.parse_args([command, 'test-ppa', '--ppa-dependencies', 'a,b,c'])247 args = parser.parse_args([command, 'test-ppa', '--ppa-dependencies', 'a,b,c'])
@@ -431,6 +434,8 @@ def test_create_arg_parser_tests():
431 (['create', 'aa/bb'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),434 (['create', 'aa/bb'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
432 (['create', 'bb', '--owner', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),435 (['create', 'bb', '--owner', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
433 (['create', 'bb', '--owner-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),436 (['create', 'bb', '--owner-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
437 (['create', 'bb', '--private'], {'command': 'create', 'private': True}),
438 (['create', 'bb', '--public'], {'command': 'create', 'private': False}),
434 (['create', 'bb', '--team', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),439 (['create', 'bb', '--team', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
435 (['create', 'bb', '--team-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),440 (['create', 'bb', '--team-name', 'aa'], {'command': 'create', 'owner_name': 'aa', 'ppa_name': 'bb'}),
436 ])441 ])
@@ -489,6 +494,8 @@ def test_create_config_from_args_error(args, expected_exception):
489 (None, {'ppa_name': 'a'}, {'displayname': 'a'}),494 (None, {'ppa_name': 'a'}, {'displayname': 'a'}),
490 (None, {'publish': True}, {'publish': True}),495 (None, {'publish': True}, {'publish': True}),
491 (None, {'publish': False}, {'publish': False}),496 (None, {'publish': False}, {'publish': False}),
497 (None, {'private': True}, {'private': True}),
498 (None, {'private': False}, {'private': False}),
492])499])
493def test_command_create(fake_config, monkeypatch, stdin, params, expected_ppa_config):500def test_command_create(fake_config, monkeypatch, stdin, params, expected_ppa_config):
494 '''Checks create command produces a PPA with expected configuration.'''501 '''Checks create command produces a PPA with expected configuration.'''
@@ -603,6 +610,10 @@ def test_command_not_exists(fake_config):
603 ({}, {'publish': True}),610 ({}, {'publish': True}),
604 ({'publish': False}, {'publish': False}),611 ({'publish': False}, {'publish': False}),
605 ({'publish': True}, {'publish': True}),612 ({'publish': True}, {'publish': True}),
613
614 ({}, {'private': False}),
615 ({'private': False}, {'private': False}),
616 ({'private': True}, {'private': True}),
606])617])
607def test_command_set(fake_config, params, expected_ppa_config):618def test_command_set(fake_config, params, expected_ppa_config):
608 '''Checks that the set command properly requests PPA configuration changes.'''619 '''Checks that the set command properly requests PPA configuration changes.'''

Subscribers

People subscribed via source and target branches

to all changes: