Merge ~cjwatson/launchpad:generate-copy-archive-signing-keys into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: ddb45f5bb1c2197a2f05ced4652577bb1c893def
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:generate-copy-archive-signing-keys
Merge into: launchpad:master
Diff against target: 322 lines (+103/-24)
6 files modified
lib/lp/archivepublisher/archivegpgsigningkey.py (+12/-3)
lib/lp/archivepublisher/tests/archive-signing.txt (+35/-7)
lib/lp/soyuz/interfaces/archive.py (+4/-2)
lib/lp/soyuz/model/archive.py (+2/-2)
lib/lp/soyuz/scripts/ppakeygenerator.py (+11/-5)
lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py (+39/-5)
Reviewer Review Type Date Requested Status
Ioana Lasc (community) Approve
Review via email: mp+413406@code.launchpad.net

Commit message

Extend ppa-generate-keys to generate keys for copy archives too

Description of the change

It's currently awkward to run builds that use the output of test rebuilds, since they're only signed if we go through some manual steps to arrange that. Since the copy archive publisher has access to the signing service, having it generate signing keys is straightforward enough, so let's just do that.

This requires a separate invocation with the `--copy-archives` option (mainly since copy archives are published on a different machine so the signing keys need to be authorized for use by that machine).

Once we have this running automatically on production, we should change `Archive.can_be_published` to have a guard for copy archives similar to the one it has for PPAs: it should only publish production copy archives that have had a signing key generated for them.

To post a comment you must log in.
Revision history for this message
Ioana Lasc (ilasc) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/lib/lp/archivepublisher/archivegpgsigningkey.py b/lib/lp/archivepublisher/archivegpgsigningkey.py
index c32bd9a..c1ada20 100644
--- a/lib/lp/archivepublisher/archivegpgsigningkey.py
+++ b/lib/lp/archivepublisher/archivegpgsigningkey.py
@@ -251,7 +251,9 @@ class ArchiveGPGSigningKey(SignableArchive):
251 # Always generate signing keys for the default PPA, even if it251 # Always generate signing keys for the default PPA, even if it
252 # was not specifically requested. The default PPA signing key252 # was not specifically requested. The default PPA signing key
253 # is then propagated to the context named-ppa.253 # is then propagated to the context named-ppa.
254 default_ppa = self.archive.owner.archive254 default_ppa = (
255 self.archive.owner.archive if self.archive.is_ppa
256 else self.archive)
255 if self.archive != default_ppa:257 if self.archive != default_ppa:
256 def propagate_key(_):258 def propagate_key(_):
257 self.archive.signing_key_owner = default_ppa.signing_key_owner259 self.archive.signing_key_owner = default_ppa.signing_key_owner
@@ -274,8 +276,15 @@ class ArchiveGPGSigningKey(SignableArchive):
274 propagate_key(None)276 propagate_key(None)
275 return277 return
276278
277 key_displayname = (279 # XXX cjwatson 2021-12-17: If we need key generation for other
278 "Launchpad PPA for %s" % self.archive.owner.displayname)280 # archive purposes (PRIMARY/PARTNER) then we should extend this, and
281 # perhaps push it down to a property of the archive.
282 if self.archive.is_copy:
283 key_displayname = (
284 "Launchpad copy archive %s" % self.archive.reference)
285 else:
286 key_displayname = (
287 "Launchpad PPA for %s" % self.archive.owner.displayname)
279 if getFeatureFlag(PUBLISHER_GPG_USES_SIGNING_SERVICE):288 if getFeatureFlag(PUBLISHER_GPG_USES_SIGNING_SERVICE):
280 try:289 try:
281 signing_key = getUtility(ISigningKeySet).generate(290 signing_key = getUtility(ISigningKeySet).generate(
diff --git a/lib/lp/archivepublisher/tests/archive-signing.txt b/lib/lp/archivepublisher/tests/archive-signing.txt
index f142a01..ced9aa5 100644
--- a/lib/lp/archivepublisher/tests/archive-signing.txt
+++ b/lib/lp/archivepublisher/tests/archive-signing.txt
@@ -31,7 +31,7 @@ We will set up and use the test-keyserver.
31Querying 'pending signing key' PPAs31Querying 'pending signing key' PPAs
32-----------------------------------32-----------------------------------
3333
34`IArchiveSet.getPPAsPendingSigningKey` allows call-sites to query for34`IArchiveSet.getArchivesPendingSigningKey` allows call-sites to query for
35PPA pending signing key generation.35PPA pending signing key generation.
3636
37 >>> from lp.registry.interfaces.person import IPersonSet37 >>> from lp.registry.interfaces.person import IPersonSet
@@ -40,14 +40,14 @@ PPA pending signing key generation.
40Only PPAs with at least one source publication are considered.40Only PPAs with at least one source publication are considered.
4141
42 >>> archive_set = getUtility(IArchiveSet)42 >>> archive_set = getUtility(IArchiveSet)
43 >>> for ppa in archive_set.getPPAsPendingSigningKey():43 >>> for ppa in archive_set.getArchivesPendingSigningKey():
44 ... print(ppa.displayname)44 ... print(ppa.displayname)
45 PPA for Celso Providelo45 PPA for Celso Providelo
46 PPA for Mark Shuttleworth46 PPA for Mark Shuttleworth
4747
48The PPA for 'No Privileges' user exists, is enabled and has no48The PPA for 'No Privileges' user exists, is enabled and has no
49signing, but it also does not contain any source publications, that's49signing, but it also does not contain any source publications, that's
50why it's skipped in the getPPAsPendingSigningKey() results.50why it's skipped in the getArchivesPendingSigningKey() results.
5151
52 >>> cprov = getUtility(IPersonSet).getByName('cprov')52 >>> cprov = getUtility(IPersonSet).getByName('cprov')
53 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')53 >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
@@ -71,7 +71,7 @@ we copy an arbitrary source into it.
71 >>> copied_sources = a_source.copyTo(71 >>> copied_sources = a_source.copyTo(
72 ... a_source.distroseries, a_source.pocket, no_priv.archive)72 ... a_source.distroseries, a_source.pocket, no_priv.archive)
7373
74 >>> for ppa in archive_set.getPPAsPendingSigningKey():74 >>> for ppa in archive_set.getArchivesPendingSigningKey():
75 ... print(ppa.displayname)75 ... print(ppa.displayname)
76 PPA for Celso Providelo76 PPA for Celso Providelo
77 PPA for Mark Shuttleworth77 PPA for Mark Shuttleworth
@@ -81,7 +81,7 @@ Disabled PPAs are excluded from the 'PendingSigningKey' pool:
8181
82 >>> no_priv.archive.disable()82 >>> no_priv.archive.disable()
8383
84 >>> for ppa in archive_set.getPPAsPendingSigningKey():84 >>> for ppa in archive_set.getArchivesPendingSigningKey():
85 ... print(ppa.displayname)85 ... print(ppa.displayname)
86 PPA for Celso Providelo86 PPA for Celso Providelo
87 PPA for Mark Shuttleworth87 PPA for Mark Shuttleworth
@@ -111,10 +111,34 @@ And use it as the Mark's PPA signing key.
111111
112It will exclude Mark's PPA from the 'PendingSigningKey' pool as well.112It will exclude Mark's PPA from the 'PendingSigningKey' pool as well.
113113
114 >>> for ppa in archive_set.getPPAsPendingSigningKey():114 >>> for ppa in archive_set.getArchivesPendingSigningKey():
115 ... print(ppa.displayname)115 ... print(ppa.displayname)
116 PPA for Celso Providelo116 PPA for Celso Providelo
117117
118We can also query for copy archives.
119
120 >>> from lp.soyuz.enums import ArchivePurpose
121 >>> rebuild_archive = factory.makeArchive(
122 ... distribution=cprov.archive.distribution, name='test-rebuild',
123 ... displayname='Test rebuild', purpose=ArchivePurpose.COPY)
124 >>> _ = a_source.copyTo(
125 ... a_source.distroseries, a_source.pocket, rebuild_archive)
126 >>> for archive in archive_set.getArchivesPendingSigningKey(
127 ... purpose=ArchivePurpose.COPY):
128 ... print(archive.displayname)
129 Test rebuild
130
131Set up a signing key for the new test rebuild archive, and after that it no
132longer shows up as pending signing key generation.
133
134 >>> rebuild_archive.signing_key_owner = a_key.owner
135 >>> rebuild_archive.signing_key_fingerprint = a_key.fingerprint
136 >>> for archive in archive_set.getArchivesPendingSigningKey(
137 ... purpose=ArchivePurpose.COPY):
138 ... print(archive.displayname)
139 >>> rebuild_archive.signing_key_owner = None
140 >>> rebuild_archive.signing_key_fingerprint = None
141
118142
119Generating a PPA signing key143Generating a PPA signing key
120----------------------------144----------------------------
@@ -270,7 +294,6 @@ As documented in archive.txt, when a named-ppa is created it is
270already configured to used the same signing-key created for the294already configured to used the same signing-key created for the
271default PPA. We will create a named-ppa for Celso.295default PPA. We will create a named-ppa for Celso.
272296
273 >>> from lp.soyuz.enums import ArchivePurpose
274 >>> named_ppa = getUtility(IArchiveSet).new(297 >>> named_ppa = getUtility(IArchiveSet).new(
275 ... owner=cprov, purpose=ArchivePurpose.PPA, name='boing')298 ... owner=cprov, purpose=ArchivePurpose.PPA, name='boing')
276299
@@ -359,6 +382,11 @@ named after the user, even if the default PPA name is something different.
359 >>> print(named_ppa.signing_key.fingerprint)382 >>> print(named_ppa.signing_key.fingerprint)
360 447DBF38C4F9C4ED752246B77D88913717B05A8F383 447DBF38C4F9C4ED752246B77D88913717B05A8F
361384
385Keys generated for copy archives use a different naming scheme.
386
387 >>> IArchiveGPGSigningKey(rebuild_archive).generateSigningKey()
388 Generating: Launchpad copy archive ubuntu/test-rebuild
389
362Restore the original functionality of GPGHandler.390Restore the original functionality of GPGHandler.
363391
364 >>> naked_gpghandler.generateKey = real_key_generator392 >>> naked_gpghandler.generateKey = real_key_generator
diff --git a/lib/lp/soyuz/interfaces/archive.py b/lib/lp/soyuz/interfaces/archive.py
index d01ebdf..a3de895 100644
--- a/lib/lp/soyuz/interfaces/archive.py
+++ b/lib/lp/soyuz/interfaces/archive.py
@@ -2416,10 +2416,12 @@ class IArchiveSet(Interface):
2416 def getPPADistributionsForUser(user):2416 def getPPADistributionsForUser(user):
2417 """Return the `Distribution`s of all PPAs for the given user."""2417 """Return the `Distribution`s of all PPAs for the given user."""
24182418
2419 def getPPAsPendingSigningKey():2419 def getArchivesPendingSigningKey(purpose=ArchivePurpose.PPA):
2420 """Return all PPAs pending signing key generation.2420 """Return all archives with `purpose` pending signing key generation.
24212421
2422 The result is ordered by archive creation date.2422 The result is ordered by archive creation date.
2423
2424 :param purpose: Only return archives with this `ArchivePurpose`.
2423 """2425 """
24242426
2425 def getLatestPPASourcePublicationsForDistribution(distribution):2427 def getLatestPPASourcePublicationsForDistribution(distribution):
diff --git a/lib/lp/soyuz/model/archive.py b/lib/lp/soyuz/model/archive.py
index ae95cf9..50ed7aa 100644
--- a/lib/lp/soyuz/model/archive.py
+++ b/lib/lp/soyuz/model/archive.py
@@ -2819,7 +2819,7 @@ class ArchiveSet:
2819 self._getPPAsForUserClause(user))2819 self._getPPAsForUserClause(user))
2820 return result.config(distinct=True)2820 return result.config(distinct=True)
28212821
2822 def getPPAsPendingSigningKey(self):2822 def getArchivesPendingSigningKey(self, purpose=ArchivePurpose.PPA):
2823 """See `IArchiveSet`."""2823 """See `IArchiveSet`."""
2824 origin = (2824 origin = (
2825 Archive,2825 Archive,
@@ -2828,7 +2828,7 @@ class ArchiveSet:
2828 results = IStore(Archive).using(*origin).find(2828 results = IStore(Archive).using(*origin).find(
2829 Archive,2829 Archive,
2830 Archive.signing_key_fingerprint == None,2830 Archive.signing_key_fingerprint == None,
2831 Archive.purpose == ArchivePurpose.PPA, Archive._enabled == True)2831 Archive.purpose == purpose, Archive._enabled == True)
2832 results.order_by(Archive.date_created)2832 results.order_by(Archive.date_created)
2833 return results.config(distinct=True)2833 return results.config(distinct=True)
28342834
diff --git a/lib/lp/soyuz/scripts/ppakeygenerator.py b/lib/lp/soyuz/scripts/ppakeygenerator.py
index 88e1d84..1b650f1 100644
--- a/lib/lp/soyuz/scripts/ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/ppakeygenerator.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the1# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__all__ = [4__all__ = [
@@ -14,6 +14,7 @@ from lp.services.scripts.base import (
14 LaunchpadCronScript,14 LaunchpadCronScript,
15 LaunchpadScriptFailure,15 LaunchpadScriptFailure,
16 )16 )
17from lp.soyuz.enums import ArchivePurpose
17from lp.soyuz.interfaces.archive import IArchiveSet18from lp.soyuz.interfaces.archive import IArchiveSet
1819
1920
@@ -26,6 +27,9 @@ class PPAKeyGenerator(LaunchpadCronScript):
26 self.parser.add_option(27 self.parser.add_option(
27 "-A", "--archive",28 "-A", "--archive",
28 help="The reference of the archive whose key should be generated.")29 help="The reference of the archive whose key should be generated.")
30 self.parser.add_option(
31 "--copy-archives", action="store_true", default=False,
32 help="Run only over COPY archives.")
2933
30 def generateKey(self, archive):34 def generateKey(self, archive):
31 """Generate a signing key for the given archive."""35 """Generate a signing key for the given archive."""
@@ -38,9 +42,9 @@ class PPAKeyGenerator(LaunchpadCronScript):
3842
39 def main(self):43 def main(self):
40 """Generate signing keys for the selected PPAs."""44 """Generate signing keys for the selected PPAs."""
45 archive_set = getUtility(IArchiveSet)
41 if self.options.archive is not None:46 if self.options.archive is not None:
42 archive = getUtility(IArchiveSet).getByReference(47 archive = archive_set.getByReference(self.options.archive)
43 self.options.archive)
44 if archive is None:48 if archive is None:
45 raise LaunchpadScriptFailure(49 raise LaunchpadScriptFailure(
46 "No archive named '%s' could be found."50 "No archive named '%s' could be found."
@@ -51,9 +55,11 @@ class PPAKeyGenerator(LaunchpadCronScript):
51 % (archive.reference, archive.displayname,55 % (archive.reference, archive.displayname,
52 archive.signing_key_fingerprint))56 archive.signing_key_fingerprint))
53 archives = [archive]57 archives = [archive]
58 elif self.options.copy_archives:
59 archives = list(archive_set.getArchivesPendingSigningKey(
60 purpose=ArchivePurpose.COPY))
54 else:61 else:
55 archive_set = getUtility(IArchiveSet)62 archives = list(archive_set.getArchivesPendingSigningKey())
56 archives = list(archive_set.getPPAsPendingSigningKey())
5763
58 for archive in archives:64 for archive in archives:
59 self.generateKey(archive)65 self.generateKey(archive)
diff --git a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
index 182f64a..0ad8a7d 100644
--- a/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
+++ b/lib/lp/soyuz/scripts/tests/test_ppakeygenerator.py
@@ -1,4 +1,4 @@
1# Copyright 2009-2019 Canonical Ltd. This software is licensed under the1# Copyright 2009-2021 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""`PPAKeyGenerator` script class tests."""4"""`PPAKeyGenerator` script class tests."""
@@ -10,14 +10,15 @@ from lp.registry.interfaces.gpg import IGPGKeySet
10from lp.registry.interfaces.person import IPersonSet10from lp.registry.interfaces.person import IPersonSet
11from lp.services.propertycache import get_property_cache11from lp.services.propertycache import get_property_cache
12from lp.services.scripts.base import LaunchpadScriptFailure12from lp.services.scripts.base import LaunchpadScriptFailure
13from lp.soyuz.enums import ArchivePurpose
13from lp.soyuz.interfaces.archive import IArchiveSet14from lp.soyuz.interfaces.archive import IArchiveSet
14from lp.soyuz.scripts.ppakeygenerator import PPAKeyGenerator15from lp.soyuz.scripts.ppakeygenerator import PPAKeyGenerator
15from lp.testing import TestCase16from lp.testing import TestCaseWithFactory
16from lp.testing.faketransaction import FakeTransaction17from lp.testing.faketransaction import FakeTransaction
17from lp.testing.layers import LaunchpadZopelessLayer18from lp.testing.layers import LaunchpadZopelessLayer
1819
1920
20class TestPPAKeyGenerator(TestCase):21class TestPPAKeyGenerator(TestCaseWithFactory):
21 layer = LaunchpadZopelessLayer22 layer = LaunchpadZopelessLayer
2223
23 def _fixArchiveForKeyGeneration(self, archive):24 def _fixArchiveForKeyGeneration(self, archive):
@@ -29,7 +30,8 @@ class TestPPAKeyGenerator(TestCase):
29 ubuntutest = getUtility(IDistributionSet).getByName('ubuntutest')30 ubuntutest = getUtility(IDistributionSet).getByName('ubuntutest')
30 archive.distribution = ubuntutest31 archive.distribution = ubuntutest
3132
32 def _getKeyGenerator(self, archive_reference=None, txn=None):33 def _getKeyGenerator(self, archive_reference=None, copy_archives=False,
34 txn=None):
33 """Return a `PPAKeyGenerator` instance.35 """Return a `PPAKeyGenerator` instance.
3436
35 Monkey-patch the script object with a fake transaction manager37 Monkey-patch the script object with a fake transaction manager
@@ -40,6 +42,8 @@ class TestPPAKeyGenerator(TestCase):
4042
41 if archive_reference is not None:43 if archive_reference is not None:
42 test_args.extend(['-A', archive_reference])44 test_args.extend(['-A', archive_reference])
45 if copy_archives:
46 test_args.append('--copy-archives')
4347
44 key_generator = PPAKeyGenerator(48 key_generator = PPAKeyGenerator(
45 name='ppa-generate-keys', test_args=test_args)49 name='ppa-generate-keys', test_args=test_args)
@@ -109,7 +113,7 @@ class TestPPAKeyGenerator(TestCase):
109 The 'signing_key' for all 'pending-signing-key' PPAs are generated113 The 'signing_key' for all 'pending-signing-key' PPAs are generated
110 and the transaction is committed once for each PPA.114 and the transaction is committed once for each PPA.
111 """115 """
112 archives = list(getUtility(IArchiveSet).getPPAsPendingSigningKey())116 archives = list(getUtility(IArchiveSet).getArchivesPendingSigningKey())
113117
114 for archive in archives:118 for archive in archives:
115 self._fixArchiveForKeyGeneration(archive)119 self._fixArchiveForKeyGeneration(archive)
@@ -123,3 +127,33 @@ class TestPPAKeyGenerator(TestCase):
123 self.assertIsNotNone(archive.signing_key_fingerprint)127 self.assertIsNotNone(archive.signing_key_fingerprint)
124128
125 self.assertEqual(txn.commit_count, len(archives))129 self.assertEqual(txn.commit_count, len(archives))
130
131 def testGenerateKeyForAllCopyArchives(self):
132 """Signing key generation for all PPAs.
133
134 The 'signing_key' for all 'pending-signing-key' PPAs are generated
135 and the transaction is committed once for each PPA.
136 """
137 for _ in range(3):
138 rebuild = self.factory.makeArchive(
139 distribution=getUtility(IDistributionSet).getByName(
140 'ubuntutest'),
141 purpose=ArchivePurpose.COPY)
142 self.factory.makeSourcePackagePublishingHistory(archive=rebuild)
143
144 archives = list(getUtility(IArchiveSet).getArchivesPendingSigningKey(
145 purpose=ArchivePurpose.COPY))
146 self.assertNotEqual([], archives)
147
148 for archive in archives:
149 self._fixArchiveForKeyGeneration(archive)
150 self.assertIsNone(archive.signing_key_fingerprint)
151
152 txn = FakeTransaction()
153 key_generator = self._getKeyGenerator(copy_archives=True, txn=txn)
154 key_generator.main()
155
156 for archive in archives:
157 self.assertIsNotNone(archive.signing_key_fingerprint)
158
159 self.assertEqual(txn.commit_count, len(archives))

Subscribers

People subscribed via source and target branches

to status/vote changes: