Merge lp:~frankban/lpsetup/branch-subcommand into lp:lpsetup

Proposed by Francesco Banconi on 2012-05-15
Status: Merged
Approved by: Graham Binns on 2012-05-16
Approved revision: 36
Merged at revision: 24
Proposed branch: lp:~frankban/lpsetup/branch-subcommand
Merge into: lp:lpsetup
Diff against target: 466 lines (+240/-24)
10 files modified
lpsetup/__init__.py (+1/-1)
lpsetup/cli.py (+2/-0)
lpsetup/handlers.py (+21/-0)
lpsetup/settings.py (+15/-9)
lpsetup/subcommands/branch.py (+88/-0)
lpsetup/subcommands/install.py (+33/-11)
lpsetup/subcommands/lxcinstall.py (+7/-3)
lpsetup/tests/test_handlers.py (+41/-0)
lpsetup/tests/test_utils.py (+17/-0)
lpsetup/utils.py (+15/-0)
To merge this branch: bzr merge lp:~frankban/lpsetup/branch-subcommand
Reviewer Review Type Date Requested Status
Graham Binns (community) code 2012-05-15 Approve on 2012-05-16
Review via email: mp+105857@code.launchpad.net

Description of the Change

== Summary ==

This branch adds the *branch* subcommand to *lp-setup*.
The branch subcommand can be used to create and build a new Launchpad branch,
e.g.::

    lp-setup branch bug-666

It is similar (and intended to replace) *rocketfuel-get*,
but adds some functionalities:

- it can be called by root (specifying the actual user with -u argument)
- it can be called specifying different launchpad repository and dependencies
  paths (i.e. when you have multiple launchpad checkouts)
- `make schema` can be triggered after the branch is created (-S argument).

== Changes ==

Added a branch sub command.

Added a validator for branch parent and target.

Bumped version up: branch sub command.

Other minor fixes:

the *install* subcommand now changes the owner of `/srv/launchpad.dev/`.

Apparmor workaround has been fixed to not fail if the hack was already applied.

Better handling of bzr locations: now it is a separate step, and it takes
advantage of a customized config parser that parses bzr location files.

Updated *lxc-install* sub command to reflect the new step added in
the *install* one.

== Tests ==

$ nosetests
............................................................
Name Stmts Miss Cover Missing
---------------------------------------------------
lpsetup 6 1 83% 16
lpsetup.argparser 125 6 95% 113, 221, 278-279, 298, 307
lpsetup.exceptions 6 0 100%
lpsetup.handlers 65 1 98% 57
lpsetup.settings 28 0 100%
lpsetup.subcommands 0 0 100%
lpsetup.utils 133 32 76% 96, 130-140, 155, 206, 216, 237-239, 257-263, 278-279, 291-297
---------------------------------------------------
TOTAL 363 40 89%
----------------------------------------------------------------------
Ran 60 tests in 0.521s

OK

To post a comment you must log in.
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lpsetup/__init__.py'
2--- lpsetup/__init__.py 2012-05-08 10:52:06 +0000
3+++ lpsetup/__init__.py 2012-05-15 17:00:40 +0000
4@@ -9,7 +9,7 @@
5 'get_version',
6 ]
7
8-VERSION = (0, 2, 0)
9+VERSION = (0, 2, 1)
10
11
12 def get_version():
13
14=== modified file 'lpsetup/cli.py'
15--- lpsetup/cli.py 2012-05-02 16:32:06 +0000
16+++ lpsetup/cli.py 2012-05-15 17:00:40 +0000
17@@ -15,6 +15,7 @@
18 exceptions,
19 )
20 from lpsetup.subcommands import (
21+ branch,
22 install,
23 lxcinstall,
24 update,
25@@ -26,6 +27,7 @@
26 parser.register_subcommand('install', install.SubCommand)
27 parser.register_subcommand('update', update.SubCommand)
28 parser.register_subcommand('lxc-install', lxcinstall.SubCommand)
29+ parser.register_subcommand('branch', branch.SubCommand)
30 args = parser.parse_args()
31 try:
32 return args.main(args)
33
34=== modified file 'lpsetup/handlers.py'
35--- lpsetup/handlers.py 2012-04-20 14:56:44 +0000
36+++ lpsetup/handlers.py 2012-05-15 17:00:40 +0000
37@@ -6,6 +6,7 @@
38
39 __metaclass__ = type
40 __all__ = [
41+ 'handle_branch_creation',
42 'handle_directories',
43 'handle_lpuser',
44 'handle_ssh_keys',
45@@ -24,6 +25,7 @@
46 )
47
48 from lpsetup.exceptions import ValidationError
49+from lpsetup.settings import LP_CHECKOUT
50
51
52 def handle_user(namespace):
53@@ -255,3 +257,22 @@
54 namespace.create_scripts = True
55 namespace.install_subunit = True
56 namespace.use_urandom = True
57+
58+
59+def handle_branch_creation(namespace):
60+ """Handle directories involved in branch creation.
61+
62+ The validation fails if the branch parent does not exist, or if a branch
63+ with the provided name already exists.
64+
65+ After this handler is called, `parent` and `branch` names
66+ are present as attributes of the namespace.
67+ """
68+ base_dir = namespace.directory
69+ parent = os.path.join(base_dir, LP_CHECKOUT)
70+ branch = os.path.join(base_dir, namespace.name)
71+ if not os.path.isdir(parent):
72+ raise ValidationError('parent branch {0} not found.'.format(parent))
73+ if os.path.exists(branch):
74+ raise ValidationError('{0} already exists.'.format(branch))
75+ namespace.parent, namespace.branch = parent, branch
76
77=== modified file 'lpsetup/settings.py'
78--- lpsetup/settings.py 2012-05-08 08:06:52 +0000
79+++ lpsetup/settings.py 2012-05-15 17:00:40 +0000
80@@ -35,15 +35,21 @@
81 '/var/tmp/archive',
82 '/var/tmp/ppa',
83 )
84-LP_BZR_LOCATIONS = (
85- ('submit_branch', '{checkout_dir}'),
86- ('public_branch', 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad'),
87- ('public_branch:policy', 'appendpath'),
88- ('push_location', 'lp:~{lpuser}/launchpad'),
89- ('push_location:policy', 'appendpath'),
90- ('merge_target', '{checkout_dir}'),
91- ('submit_to', 'merge@code.launchpad.net'),
92- )
93+LP_BZR_LOCATIONS = {
94+ '{directory}': {
95+ 'submit_branch': '{checkout_dir}',
96+ 'public_branch': 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad',
97+ 'public_branch:policy': 'appendpath',
98+ 'push_location': 'lp:~{lpuser}/launchpad',
99+ 'push_location:policy': 'appendpath',
100+ 'merge_target': '{checkout_dir}',
101+ 'submit_to': 'merge@code.launchpad.net',
102+ },
103+ '{checkout_dir}': {
104+ 'public_branch':
105+ 'bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel',
106+ }
107+ }
108 LP_CHECKOUT = 'devel'
109 LP_PACKAGES = [
110 # "launchpad-database-dependencies-9.1" can be removed once 8.x is
111
112=== added file 'lpsetup/subcommands/branch.py'
113--- lpsetup/subcommands/branch.py 1970-01-01 00:00:00 +0000
114+++ lpsetup/subcommands/branch.py 2012-05-15 17:00:40 +0000
115@@ -0,0 +1,88 @@
116+#!/usr/bin/env python
117+# Copyright 2012 Canonical Ltd. This software is licensed under the
118+# GNU Affero General Public License version 3 (see the file LICENSE).
119+
120+"""Branch subcommand: create and build a new Launchpad branch."""
121+
122+__metaclass__ = type
123+__all__ = [
124+ 'create_branch',
125+ 'setup_and_make',
126+ 'SubCommand',
127+ ]
128+
129+from shelltoolbox import (
130+ cd,
131+ get_su_command,
132+ su,
133+ )
134+
135+from lpsetup import (
136+ argparser,
137+ handlers,
138+ )
139+from lpsetup.settings import (
140+ CHECKOUT_DIR,
141+ DEPENDENCIES_DIR,
142+ )
143+from lpsetup.subcommands import install
144+from lpsetup.utils import call
145+
146+
147+def create_branch(user, parent, branch):
148+ """Create a branch of `devel`."""
149+ cmd = ('bzr', 'branch', parent, branch)
150+ # Using real su because bzr uses uid.
151+ call(*get_su_command(user, cmd), stderr=None)
152+
153+
154+def setup_and_make(user, dependencies_dir, branch, make_schema):
155+ """Set up external source code for the new branch and run `make`."""
156+ with cd(branch):
157+ with su(user):
158+ call('utilities/link-external-sourcecode', dependencies_dir)
159+ install.make_launchpad(user, branch, install=False)
160+ if make_schema:
161+ with su(user):
162+ call('make', '-C', branch, 'schema')
163+
164+
165+class SubCommand(argparser.StepsBasedSubCommand):
166+ """Create and build a new Launchpad branch."""
167+
168+ steps = (
169+ (create_branch,
170+ 'user', 'parent', 'branch'),
171+ (setup_and_make,
172+ 'user', 'dependencies_dir', 'branch', 'make_schema'),
173+ )
174+ help = __doc__
175+ validators = (
176+ handlers.handle_user,
177+ handlers.handle_directories,
178+ handlers.handle_branch_creation,
179+ )
180+
181+ def add_arguments(self, parser):
182+ super(SubCommand, self).add_arguments(parser)
183+ parser.add_argument(
184+ '-u', '--user',
185+ help='The name of the system user that will own the branch. '
186+ 'The current user is used if this script is not run as '
187+ 'root and this argument is omitted.')
188+ parser.add_argument(
189+ '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
190+ help='The directory of the Launchpad dependencies to be linked. '
191+ 'The directory must reside under the home directory of the '
192+ 'given user (see -u argument). '
193+ '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
194+ parser.add_argument(
195+ '-c', '--directory', default=CHECKOUT_DIR,
196+ help='The directory of the existing Launchpad repository. '
197+ 'The directory must reside under the home directory of the '
198+ 'given user (see -u argument). '
199+ '[DEFAULT={0}]'.format(CHECKOUT_DIR))
200+ parser.add_argument(
201+ '-S', '--make-schema', action='store_true',
202+ help='Run `make schema` in the newly created branch.')
203+ parser.add_argument('name', help='The name of the branch to create.')
204
205=== modified file 'lpsetup/subcommands/install.py'
206--- lpsetup/subcommands/install.py 2012-05-08 13:43:08 +0000
207+++ lpsetup/subcommands/install.py 2012-05-15 17:00:40 +0000
208@@ -52,7 +52,10 @@
209 LP_SOURCE_DEPS,
210 SSH_KEY_NAME,
211 )
212-from lpsetup.utils import call
213+from lpsetup.utils import (
214+ call,
215+ ConfigParser,
216+ )
217
218
219 def setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir):
220@@ -157,14 +160,6 @@
221 # Set up the codebase.
222 checkout_dir = os.path.join(directory, LP_CHECKOUT)
223 setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir)
224- # Set up bzr locations
225- with su(user) as env:
226- bzr_locations = os.path.join(env.home, '.bazaar', 'locations.conf')
227- file_append(bzr_locations, '[{0}]\n'.format(directory))
228- lines = ['{0} = {1}\n'.format(k, v) for k, v in LP_BZR_LOCATIONS]
229- for line in lines:
230- location = line.format(checkout_dir=checkout_dir, lpuser=lpuser)
231- file_append(bzr_locations, location)
232 # rng-tools is used to set /dev/urandom as random data source, avoiding
233 # entropy exhaustion during automated parallel tests.
234 if use_urandom:
235@@ -173,6 +168,29 @@
236 call('/etc/init.d/rng-tools', 'start')
237
238
239+def setup_bzr_locations(user, lpuser, directory, template=LP_BZR_LOCATIONS):
240+ """Set up bazaar locations."""
241+ context = {
242+ 'checkout_dir': os.path.join(directory, LP_CHECKOUT),
243+ 'directory': directory,
244+ 'lpuser': lpuser,
245+ }
246+ with su(user) as env:
247+ bazaar_dir = os.path.join(env.home, '.bazaar')
248+ mkdirs(bazaar_dir)
249+ path = os.path.join(bazaar_dir, 'locations.conf')
250+ parser = ConfigParser()
251+ parser.read(path)
252+ for section_template, options in template.items():
253+ section = section_template.format(**context)
254+ if not parser.has_section(section):
255+ parser.add_section(section)
256+ for option, value in options.items():
257+ parser.set(section, option, value.format(**context))
258+ with open(path, 'w') as f:
259+ parser.write(f)
260+
261+
262 def setup_apt(no_repositories=True):
263 """Setup, update and upgrade deb packages."""
264 if not no_repositories:
265@@ -191,8 +209,8 @@
266 """Set up the Launchpad environment."""
267 # User configuration.
268 subprocess.call(['adduser', user, 'sudo'])
269- gid = pwd.getpwnam(user).pw_gid
270- subprocess.call(['addgroup', '--gid', str(gid), user])
271+ pwd_database = pwd.getpwnam(user)
272+ subprocess.call(['addgroup', '--gid', str(pwd_database.pw_gid), user])
273 # Set up Launchpad dependencies.
274 checkout_dir = os.path.join(directory, LP_CHECKOUT)
275 setup_external_sourcecode(
276@@ -208,6 +226,8 @@
277 call('utilities/launchpad-database-setup', user)
278 # Make and install launchpad.
279 make_launchpad(user, checkout_dir, install=True)
280+ # Change owner of /srv/launchpad.dev/.
281+ os.chown('/srv/launchpad.dev/', pwd_database.pw_uid, pwd_database.pw_gid)
282 # Set up container hosts file.
283 lines = ['{0}\t{1}\n'.format(ip, names) for ip, names in HOSTS_CONTENT]
284 for line in lines:
285@@ -222,6 +242,8 @@
286 'user', 'full_name', 'email', 'lpuser',
287 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
288 'use_urandom', 'dependencies_dir', 'directory'),
289+ (setup_bzr_locations,
290+ 'user', 'lpuser', 'directory'),
291 (setup_apt,
292 'no_repositories'),
293 (setup_launchpad,
294
295=== modified file 'lpsetup/subcommands/lxcinstall.py'
296--- lpsetup/subcommands/lxcinstall.py 2012-05-08 13:43:08 +0000
297+++ lpsetup/subcommands/lxcinstall.py 2012-05-15 17:00:40 +0000
298@@ -103,9 +103,11 @@
299 # much a Good Thing.
300 # Disable the apparmor profiles for lxc so that we don't have
301 # problems installing postgres.
302- call('ln', '-s',
303- '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/')
304- call('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start')
305+ subprocess.call([
306+ 'ln', '-s',
307+ '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/'])
308+ subprocess.call([
309+ 'apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start'])
310 # Container configuration template.
311 lxc_gateway = get_lxc_gateway()
312 if lxc_gateway is None:
313@@ -204,6 +206,8 @@
314 'user', 'full_name', 'email', 'lpuser',
315 'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
316 'use_urandom', 'dependencies_dir', 'directory'),
317+ (install.setup_bzr_locations,
318+ 'user', 'lpuser', 'directory'),
319 (create_scripts,
320 'user', 'lxc_name', 'ssh_key_path'),
321 (create_lxc,
322
323=== modified file 'lpsetup/tests/test_handlers.py'
324--- lpsetup/tests/test_handlers.py 2012-04-23 17:13:36 +0000
325+++ lpsetup/tests/test_handlers.py 2012-05-15 17:00:40 +0000
326@@ -7,11 +7,15 @@
327 import argparse
328 from contextlib import contextmanager
329 import getpass
330+import os
331 import pwd
332+import shutil
333+import tempfile
334 import unittest
335
336 from lpsetup.exceptions import ValidationError
337 from lpsetup.handlers import (
338+ handle_branch_creation,
339 handle_directories,
340 handle_lpuser,
341 handle_ssh_keys,
342@@ -19,6 +23,7 @@
343 handle_user,
344 handle_userdata,
345 )
346+from lpsetup.settings import LP_CHECKOUT
347
348
349 class HandlersTestMixin(object):
350@@ -61,6 +66,42 @@
351 raise TypeError
352
353
354+class HandleBranchCreationTest(HandlersTestMixin, unittest.TestCase):
355+
356+ def setUp(self):
357+ directory = tempfile.mkdtemp()
358+ self.namespace = argparse.Namespace(
359+ directory=directory, name='new-branch')
360+ self.parent = os.path.join(directory, LP_CHECKOUT)
361+ os.mkdir(self.parent)
362+
363+ def get_branch(self):
364+ return os.path.join(self.namespace.directory, self.namespace.name)
365+
366+ def tearDown(self):
367+ shutil.rmtree(self.namespace.directory)
368+
369+ def test_correct_paths(self):
370+ # Ensure the namespace contains the correct `parent` and `branch`
371+ # attributes if the validation passes.
372+ handle_branch_creation(self.namespace)
373+ self.assertEqual(self.parent, self.namespace.parent)
374+ self.assertEqual(self.get_branch(), self.namespace.branch)
375+
376+ def test_invalid_parent(self):
377+ # The validation fails if the parent branch is not found.
378+ os.rmdir(self.parent)
379+ with self.assertNotValid(self.parent):
380+ handle_branch_creation(self.namespace)
381+
382+ def test_invalid_branch_name(self):
383+ # The validation fails if the branch already exists.
384+ branch = self.get_branch()
385+ os.mkdir(branch)
386+ with self.assertNotValid(branch):
387+ handle_branch_creation(self.namespace)
388+
389+
390 class HandleDirectoriesTest(HandlersTestMixin, unittest.TestCase):
391
392 home_dir = '/home/foo'
393
394=== modified file 'lpsetup/tests/test_utils.py'
395--- lpsetup/tests/test_utils.py 2012-04-30 16:53:35 +0000
396+++ lpsetup/tests/test_utils.py 2012-05-15 17:00:40 +0000
397@@ -8,6 +8,7 @@
398 import os
399 import shutil
400 import sys
401+import StringIO
402 import tempfile
403 import unittest
404
405@@ -17,6 +18,7 @@
406 LXC_NAME,
407 )
408 from lpsetup.utils import (
409+ ConfigParser,
410 get_container_path,
411 get_lxc_gateway,
412 get_network_interfaces,
413@@ -28,6 +30,21 @@
414 )
415
416
417+class ConfigParserTest(unittest.TestCase):
418+
419+ def get_parser(self, config):
420+ parser = ConfigParser()
421+ parser.readfp(StringIO.StringIO(config))
422+ return parser
423+
424+ def test_parser(self):
425+ # Ensure the parser correctly parses options containing colons.
426+ config = '[section]\noption1 = value1\noption2:colon = value2\n'
427+ items = dict(self.get_parser(config).items('section'))
428+ self.assertEqual('value1', items['option1'])
429+ self.assertEqual('value2', items['option2:colon'])
430+
431+
432 class GetContainerPathTest(unittest.TestCase):
433
434 def test_root_path(self):
435
436=== modified file 'lpsetup/utils.py'
437--- lpsetup/utils.py 2012-05-02 16:32:06 +0000
438+++ lpsetup/utils.py 2012-05-15 17:00:40 +0000
439@@ -21,6 +21,7 @@
440 'this_command',
441 ]
442
443+from ConfigParser import RawConfigParser
444 from functools import (
445 partial,
446 wraps,
447@@ -55,6 +56,20 @@
448 call = partial(run, stdout=None)
449
450
451+class ConfigParser(RawConfigParser):
452+ """A customized configuration parser.
453+
454+ The base `RawConfigParser` separates options from values using a colon
455+ or a equal sign. This parser forces the separator to be an equal sign,
456+ allowing the colon to be part of an option name.
457+ """
458+ OPTCRE = re.compile(
459+ r'(?P<option>[^=\s][^=]*)' # very permissive!
460+ r'\s*(?P<vi>=)\s*' # space/tab, separator, space/tab
461+ r'(?P<value>.*)$' # everything up to eol
462+ )
463+
464+
465 def get_container_path(lxc_name, path='', base_path=LXC_PATH):
466 """Return the path of LXC container called `lxc_name`.
467

Subscribers

People subscribed via source and target branches

to all changes: