Merge lp:~jtv/launchpad/bug-532354 into lp:launchpad

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/bug-532354
Merge into: lp:launchpad
Prerequisite: lp:~jtv/launchpad/bug-527170
Diff against target: 508 lines (+266/-33)
5 files modified
configs/development/launchpad-lazr.conf (+1/-0)
lib/canonical/testing/layers.py (+1/-1)
utilities/make-lp-user (+116/-11)
utilities/soyuz-sampledata-setup.py (+127/-21)
utilities/start-dev-soyuz.sh (+21/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-532354
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+20749@code.launchpad.net

Commit message

Register ssh key for ppa-user; enable translation templates generation on dev systems.

Description of the change

= Bug 532354 =

To automate the setup for Soyuz and the build farm on local development systems, I wrote a script that creates a user, optionally uploads your real GPG key(s) for it, signs the Ubuntu code of conduct, etc.

One thing this script failed to do, however, was upload your ssh key(s). But utilities/make-lp-user does do that. So instead of re-inventing the wheel I'm now making the setup script call make-lp-user. And since it obviously belongs there next to the ssh key upload, I extended make-lp-user to take an optional email address argument. If you pass this option, the user that is created will have the email address you specify—as well as any GPG keys you have for that address. This may come in handy generally when you need to sign things like package uploads.

I also made some small improvements in options handling:
 * Don't read all the zcml unless and until you've parsed your options and know it's not just a "--help" invocation!
 * The dry-run option is gone. Can't really work now that one script invokes another.

The GPG-handling code is essentially unchanged, just moved. One small difference is that run_native_gpg figures out for itself that the command you want to run is gpg; you only pass the arguments to gpg.

Another seemingly unrelated change, but also needed for convenient buildfarm testing as far as my team is concerned, is that generation of translation templates from branches is enabled on development systems. (It's already enabled in test runs, but still disabled by default until we get the whole feature working).

Once this is all done, I'll be updating the UsingSoyuzLocally page on the dev wiki, removing tons of stuff that no longer needs to be done manually. I'll have to go over the new TryOutBuildSlave page as well.

No lint. To test (insofar as the utility scripts are ever tested),
{{{
./bin/test -vv -t sampledata-setup.txt
}}}

To Q/A, follow the updated instructions on UsingSoyuzLocally.

Jeroen

To post a comment you must log in.
Revision history for this message
Abel Deuring (adeuring) wrote :

(15:28:59) adeuring: jtv: parse_fingerprints() expects untranslated gpg output: the string "Key fingerprint". Is gpg invoked without the user's language settings? (I get "Schl.-Fingerabdruck" instead when I invoke gpg --fingerprint from a shell)
(15:29:56) jtv: adeuring: gah, good point. I of all people should know better. I'll set LC_ALL to C, since I'm manipulating the environment anyway.
(15:30:12) adeuring: jtv: thanks!
(15:30:21) jtv: Thanks for pointing that out.
(15:31:49) jtv: Fixed.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'configs/development/launchpad-lazr.conf'
2--- configs/development/launchpad-lazr.conf 2010-02-12 19:26:23 +0000
3+++ configs/development/launchpad-lazr.conf 2010-03-05 14:34:40 +0000
4@@ -214,6 +214,7 @@
5
6 [rosetta]
7 global_suggestions_enabled: True
8+generate_templates: True
9
10 [rosettabranches]
11 error_dir: /var/tmp/rosettabranches.test
12
13=== modified file 'lib/canonical/testing/layers.py'
14--- lib/canonical/testing/layers.py 2010-02-12 19:34:42 +0000
15+++ lib/canonical/testing/layers.py 2010-03-05 14:34:40 +0000
16@@ -97,7 +97,7 @@
17 confirm_dbrevision, confirm_dbrevision_on_startup)
18 from canonical.database.sqlbase import cursor, ZopelessTransactionManager
19 from canonical.launchpad.interfaces import IMailBox, IOpenLaunchBag
20-from canonical.launchpad.ftests import ANONYMOUS, login, logout, is_logged_in
21+from lp.testing import ANONYMOUS, login, logout, is_logged_in
22 import lp.services.mail.stub
23 from lp.services.mail.mailbox import TestMailBox
24 from canonical.launchpad.scripts import execute_zcml_for_scripts
25
26=== renamed file 'lib/lp/soyuz/doc/sampledata-cleanup.txt' => 'lib/lp/soyuz/doc/sampledata-setup.txt'
27=== modified file 'utilities/make-lp-user'
28--- utilities/make-lp-user 2009-10-17 14:06:03 +0000
29+++ utilities/make-lp-user 2010-03-05 14:34:40 +0000
30@@ -1,11 +1,11 @@
31 #!/usr/bin/python2.5
32 #
33-# Copyright 2009 Canonical Ltd. This software is licensed under the
34+# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
35 # GNU Affero General Public License version 3 (see the file LICENSE).
36
37 """Create a user for testing the local Launchpad.
38
39-Usage: make-lp-user <username> [<team1> <team2> ...]
40+Usage: make-lp-user <username> [<team1> <team2> ...] [-e email]
41
42 This script will create a usable Launchpad user in the development database to
43 help you test a locally running copy of Launchpad.
44@@ -21,6 +21,9 @@
45 In addition, this script will look in your ~/.ssh directory for public keys
46 and register them for the created user.
47
48+If you pass an email address, the new user will have this email address
49+as well as any GPG keys you have for it.
50+
51 The login details will be printed to stdout.
52
53 Please note that this script is for testing purposes only. Do NOT use it in
54@@ -30,10 +33,14 @@
55 import _pythonpath
56
57 import os
58+from optparse import OptionParser
59+import re
60+import subprocess
61 import sys
62-
63 import transaction
64
65+from storm.store import Store
66+
67 from zope.component import getUtility
68
69 from canonical.launchpad.interfaces import (
70@@ -42,7 +49,9 @@
71 SSHKeyType,
72 TeamMembershipStatus,
73 )
74+from canonical.launchpad.interfaces.gpghandler import IGPGHandler
75 from canonical.launchpad.scripts import execute_zcml_for_scripts
76+from lp.registry.interfaces.gpg import GPGKeyAlgorithm, IGPGKeySet
77 from lp.testing.factory import LaunchpadObjectFactory
78
79 # Shut up, pyflakes.
80@@ -53,7 +62,7 @@
81 factory = LaunchpadObjectFactory()
82
83
84-def make_person(username):
85+def make_person(username, email):
86 """Create and return a person with the given username.
87
88 The email address for the user will be <username>@example.com. The
89@@ -61,7 +70,6 @@
90
91 These details will be printed to stdout.
92 """
93- email = '%s@example.com' % username
94 person = factory.makePerson(
95 name=username, password=DEFAULT_PASSWORD, email=email)
96 print "username: %s" % (username,)
97@@ -121,18 +129,115 @@
98 print 'Registered SSH key: %s' % (guessed_filename,)
99
100
101+def parse_fingerprints(gpg_output):
102+ """Find key fingerprints in "gpg --fingerprint <email>" output."""
103+ line_prefix = re.compile('\s*Key fingerprint\s*=\s*')
104+ return [
105+ ''.join(re.sub(line_prefix, '', line).split())
106+ for line in gpg_output.splitlines()
107+ if line_prefix.match(line)
108+ ]
109+
110+
111+def run_native_gpg(arguments):
112+ """Run GPG using the user's real keyring."""
113+ # Need to override GNUPGHOME or we'll get a dummy GPG in a temp
114+ # directory, which won't find any keys.
115+ env = os.environ.copy()
116+ if 'GNUPGHOME' in env:
117+ del env['GNUPGHOME']
118+
119+ # Prevent translated gpg output from messing up our parsing.
120+ env['LC_ALL'] = 'C'
121+
122+ command_line = ['gpg'] + arguments
123+ pipe = subprocess.Popen(
124+ command_line, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
125+ stdout, stderr = pipe.communicate()
126+ if stderr != '':
127+ print stderr
128+ if pipe.returncode != 0:
129+ raise Exception('GPG error during "%s"' % ' '.join(command_line))
130+
131+ return stdout
132+
133+
134+def add_gpg_key(person, fingerprint):
135+ """Add the GPG key with the given fingerprint to `person`."""
136+ run_native_gpg([
137+ '--keyserver', 'keyserver.launchpad.dev',
138+ '--send-key', fingerprint
139+ ])
140+
141+ gpghandler = getUtility(IGPGHandler)
142+ key = gpghandler.retrieveKey(fingerprint)
143+
144+ gpgkeyset = getUtility(IGPGKeySet)
145+ if gpgkeyset.getByFingerprint(fingerprint) is not None:
146+ # We already have this key.
147+ return
148+
149+ algorithm = GPGKeyAlgorithm.items[key.algorithm]
150+ can_encrypt = True
151+ lpkey = gpgkeyset.new(
152+ person.id, key.keyid, fingerprint, key.keysize, algorithm,
153+ active=True, can_encrypt=can_encrypt)
154+ Store.of(person).add(lpkey)
155+
156+
157+def attach_gpg_keys(email, person):
158+ """Attach the GPG key(s) for `email` to `person`."""
159+ output = run_native_gpg(['--fingerprint', email])
160+
161+ fingerprints = parse_fingerprints(output)
162+ if len(fingerprints) == 0:
163+ print "No GPG key fingerprints found!"
164+ for fingerprint in fingerprints:
165+ add_gpg_key(person, fingerprint)
166+
167+
168+def parse_args(arguments):
169+ """Parse command-line arguments.
170+
171+ :return: options object. Among the options are username (a
172+ string) and optionally teams (a list).
173+ """
174+ parser = OptionParser(description="Create a local Launchpad user.")
175+ parser.add_option(
176+ '-e', '--email', action='store', dest='email', default=None,
177+ help="Email address; set to use real GPG key for this address.")
178+
179+ options, args = parser.parse_args(arguments)
180+ if len(args) == 0:
181+ print __doc__
182+ sys.exit(2)
183+
184+ options.username = args[0]
185+ options.teams = args[1:]
186+
187+ return options
188+
189+
190 def main(arguments):
191 """Run the script."""
192- if len(arguments) == 0:
193- print __doc__
194- return 2
195+ options = parse_args(arguments)
196+ if options.email is None:
197+ email = '%s@example.com' % options.username
198+ else:
199+ email = options.email
200+
201 execute_zcml_for_scripts()
202- username, teams = arguments[0], arguments[1:]
203 transaction.begin()
204- person = make_person(username)
205- add_person_to_teams(person, teams)
206+
207+ person = make_person(options.username, email)
208+ add_person_to_teams(person, options.teams)
209 add_ssh_public_keys(person)
210+
211+ if options.email is not None:
212+ attach_gpg_keys(options.email, person)
213+
214 transaction.commit()
215+
216 return 0
217
218
219
220=== renamed file 'utilities/soyuz-sampledata-cleanup.py' => 'utilities/soyuz-sampledata-setup.py'
221--- utilities/soyuz-sampledata-cleanup.py 2010-02-24 12:13:29 +0000
222+++ utilities/soyuz-sampledata-setup.py 2010-03-05 14:34:40 +0000
223@@ -13,6 +13,9 @@
224
225 DO NOT RUN ON PRODUCTION SYSTEMS. This script deletes lots of
226 Ubuntu-related data.
227+
228+This script creates a user "ppa-user" (email ppa-user@example.com,
229+password test) who is able to create PPAs.
230 """
231
232 __metaclass__ = type
233@@ -20,9 +23,12 @@
234 import _pythonpath
235
236 from optparse import OptionParser
237-from os import getenv
238 import re
239+import os
240+import subprocess
241 import sys
242+from textwrap import dedent
243+import transaction
244
245 from zope.component import getUtility
246 from zope.event import notify
247@@ -40,15 +46,24 @@
248 from canonical.launchpad.scripts import execute_zcml_for_scripts
249 from canonical.launchpad.scripts.logger import logger, logger_options
250 from canonical.launchpad.webapp.interfaces import (
251- IStoreSelector, MAIN_STORE, SLAVE_FLAVOR)
252+ IStoreSelector, MAIN_STORE, MASTER_FLAVOR, SLAVE_FLAVOR)
253
254+from lp.registry.interfaces.codeofconduct import ISignedCodeOfConductSet
255+from lp.registry.interfaces.person import IPersonSet
256 from lp.registry.interfaces.series import SeriesStatus
257+from lp.registry.model.codeofconduct import SignedCodeOfConduct
258 from lp.soyuz.interfaces.component import IComponentSet
259+from lp.soyuz.interfaces.processor import IProcessorFamilySet
260 from lp.soyuz.interfaces.section import ISectionSet
261 from lp.soyuz.interfaces.sourcepackageformat import (
262 ISourcePackageFormatSelectionSet, SourcePackageFormat)
263 from lp.soyuz.model.section import SectionSelection
264 from lp.soyuz.model.component import ComponentSelection
265+from lp.testing.factory import LaunchpadObjectFactory
266+
267+
268+user_name = 'ppa-user'
269+default_email = '%s@example.com' % user_name
270
271
272 class DoNotRunOnProduction(Exception):
273@@ -64,13 +79,18 @@
274 return max_id[0]
275
276
277+def get_store(flavor=MASTER_FLAVOR):
278+ """Obtain an ORM store."""
279+ return getUtility(IStoreSelector).get(MAIN_STORE, flavor)
280+
281+
282 def check_preconditions(options):
283 """Try to ensure that it's safe to run.
284
285 This script must not run on a production server, or anything
286 remotely like it.
287 """
288- store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)
289+ store = get_store(SLAVE_FLAVOR)
290
291 # Just a guess, but dev systems aren't likely to have ids this high
292 # in this table. Production data does.
293@@ -82,7 +102,7 @@
294 # For some configs it's just absolutely clear this script shouldn't
295 # run. Don't even accept --force there.
296 forbidden_configs = re.compile('(edge|lpnet|production)')
297- current_config = getenv('LPCONFIG', 'an unknown config')
298+ current_config = os.getenv('LPCONFIG', 'an unknown config')
299 if forbidden_configs.match(current_config):
300 raise DoNotRunOnProduction(
301 "I won't delete Ubuntu data on %s and you can't --force me."
302@@ -95,22 +115,25 @@
303 :return: (options, args, logger)
304 """
305 parser = OptionParser(
306- description="Delete existing Ubuntu releases and set up new ones.")
307+ description="Set up fresh Ubuntu series and %s identity." % user_name)
308 parser.add_option('-f', '--force', action='store_true', dest='force',
309 help="DANGEROUS: run even if the database looks production-like.")
310- parser.add_option('-n', '--dry-run', action='store_true', dest='dry_run',
311- help="Do not commit changes.")
312+ parser.add_option('-e', '--email', action='store', dest='email',
313+ default=default_email,
314+ help=(
315+ "Email address to use for %s. Should match your GPG key."
316+ % user_name))
317+
318 logger_options(parser)
319
320 options, args = parser.parse_args(arguments)
321+
322 return options, args, logger(options)
323
324
325-def get_person(name):
326+def get_person_set():
327 """Return `IPersonSet` utility."""
328- # Avoid circular import.
329- from lp.registry.interfaces.person import IPersonSet
330- return getUtility(IPersonSet).getByName(name)
331+ return getUtility(IPersonSet)
332
333
334 def retire_series(distribution):
335@@ -150,6 +173,20 @@
336 components = main restricted universe multiverse'''
337
338
339+def add_architecture(distroseries, architecture_name):
340+ """Add a DistroArchSeries for the given architecture to `distroseries`."""
341+ # Avoid circular import.
342+ from lp.soyuz.model.distroarchseries import DistroArchSeries
343+
344+ store = get_store(MASTER_FLAVOR)
345+ family = getUtility(IProcessorFamilySet).getByName(architecture_name)
346+ archseries = DistroArchSeries(
347+ distroseries=distroseries, processorfamily=family,
348+ owner=distroseries.owner, official=True,
349+ architecturetag=architecture_name)
350+ store.add(archseries)
351+
352+
353 def create_sections(distroseries):
354 """Set up some sections for `distroseries`."""
355 section_names = (
356@@ -253,7 +290,7 @@
357 # published binaries without corresponding sources.
358
359 log.info("Deleting all items in official archives...")
360- retire_distro_archives(distribution, get_person('name16'))
361+ retire_distro_archives(distribution, get_person_set().getByName('name16'))
362
363 # Disable publishing of all PPAs, as they probably have broken
364 # publishings too.
365@@ -271,7 +308,7 @@
366 utility.add(distroseries, format)
367
368
369-def populate(distribution, parent_series_name, uploader_name, log):
370+def populate(distribution, parent_series_name, uploader_name, options, log):
371 """Set up sample data on `distribution`."""
372 parent_series = distribution.getSeries(parent_series_name)
373
374@@ -281,15 +318,73 @@
375
376 log.info("Configuring sections...")
377 create_sections(parent_series)
378+ add_architecture(parent_series, 'amd64')
379
380 log.info("Configuring components and permissions...")
381- create_components(parent_series, get_person(uploader_name))
382+ uploader = get_person_set().getByName(uploader_name)
383+ create_components(parent_series, uploader)
384
385 set_source_package_format(parent_series)
386
387 create_sample_series(parent_series, log)
388
389
390+def sign_code_of_conduct(person, log):
391+ """Sign Ubuntu Code of Conduct for `person`, if necessary."""
392+ if person.is_ubuntu_coc_signer:
393+ # Already signed.
394+ return
395+
396+ log.info("Signing Ubuntu code of conduct.")
397+ signedcocset = getUtility(ISignedCodeOfConductSet)
398+ person_id = person.id
399+ if signedcocset.searchByUser(person_id).count() == 0:
400+ fake_gpg_key = LaunchpadObjectFactory().makeGPGKey(person)
401+ Store.of(person).add(SignedCodeOfConduct(
402+ owner=person, signingkey=fake_gpg_key,
403+ signedcode="Normally a signed CoC would go here.", active=True))
404+
405+
406+def create_ppa_user(username, options, approver, log):
407+ """Create new user, with password "test," and sign code of conduct."""
408+ person = get_person_set().getByName(username)
409+ if person is None:
410+ have_email = (options.email != default_email)
411+ command_line = [
412+ 'utilities/make-lp-user',
413+ username,
414+ 'ubuntu-team'
415+ ]
416+ if have_email:
417+ command_line += ['--email', options.email]
418+
419+ pipe = subprocess.Popen(command_line, stderr=subprocess.PIPE)
420+ stdout, stderr = pipe.communicate()
421+ if stderr != '':
422+ print stderr
423+ if pipe.returncode != 0:
424+ sys.exit(2)
425+
426+ transaction.commit()
427+
428+ person = getUtility(IPersonSet).getByName(username)
429+ sign_code_of_conduct(person, log)
430+
431+ return person
432+
433+
434+def create_ppa(distribution, person, name):
435+ """Create a PPA for `person`."""
436+ ppa = LaunchpadObjectFactory().makeArchive(
437+ distribution=distribution, owner=person, name=name, virtualized=False,
438+ description="Automatically created test PPA.")
439+
440+ series_name = distribution.currentseries.name
441+ ppa.external_dependencies = (
442+ "deb http://archive.ubuntu.com/ubuntu %s "
443+ "main restricted universe multiverse\n") % series_name
444+
445+
446 def main(argv):
447 options, args, log = parse_args(argv[1:])
448
449@@ -302,15 +397,26 @@
450 clean_up(ubuntu, log)
451
452 # Use Hoary as the root, as Breezy and Grumpy are broken.
453- populate(ubuntu, 'hoary', 'ubuntu-team', log)
454-
455- if options.dry_run:
456- txn.abort()
457- else:
458- txn.commit()
459-
460+ populate(ubuntu, 'hoary', 'ubuntu-team', options, log)
461+
462+ admin = get_person_set().getByName('name16')
463+ person = create_ppa_user(user_name, options, admin, log)
464+
465+ create_ppa(ubuntu, person, 'test-ppa')
466+
467+ txn.commit()
468 log.info("Done.")
469
470+ print dedent("""
471+ Now start your local Launchpad with "make run_codehosting" and log
472+ into https://launchpad.dev/ as "%(email)s" with "test" as the
473+ password.
474+ Your user name will be %(user_name)s."""
475+ % {
476+ 'email': options.email,
477+ 'user_name': user_name,
478+ })
479+
480
481 if __name__ == "__main__":
482 main(sys.argv)
483
484=== added file 'utilities/start-dev-soyuz.sh'
485--- utilities/start-dev-soyuz.sh 1970-01-01 00:00:00 +0000
486+++ utilities/start-dev-soyuz.sh 2010-03-05 14:34:40 +0000
487@@ -0,0 +1,21 @@
488+#!/bin/sh -e
489+# Start up Soyuz for local testing on a dev machine.
490+
491+start_twistd() {
492+ # Start twistd for service $1.
493+ mkdir -p "/var/tmp/$1"
494+ echo "Starting $1."
495+ bin/twistd \
496+ --logfile "/var/tmp/development-$1.log" \
497+ --pidfile "/var/tmp/development-$1.pid" \
498+ -y "daemons/$1.tac"
499+}
500+
501+start_twistd zeca
502+start_twistd buildd-manager
503+
504+echo "Starting poppy."
505+mkdir -p /var/tmp/poppy
506+bin/py daemons/poppy-upload.py /var/tmp/poppy/incoming 2121 &
507+
508+echo "Done."