← Back to team overview

yellow team mailing list archive

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

 

Francesco Banconi has proposed merging lp:~frankban/lpsetup/branch-subcommand into lp:lpsetup.

Requested reviews:
  Launchpad Yellow Squad (yellow)

For more details, see:
https://code.launchpad.net/~frankban/lpsetup/branch-subcommand/+merge/105857

== 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

-- 
https://code.launchpad.net/~frankban/lpsetup/branch-subcommand/+merge/105857
Your team Launchpad Yellow Squad is requested to review the proposed merge of lp:~frankban/lpsetup/branch-subcommand into lp:lpsetup.
=== modified file 'lpsetup/__init__.py'
--- lpsetup/__init__.py	2012-05-08 10:52:06 +0000
+++ lpsetup/__init__.py	2012-05-15 17:00:40 +0000
@@ -9,7 +9,7 @@
     'get_version',
     ]
 
-VERSION = (0, 2, 0)
+VERSION = (0, 2, 1)
 
 
 def get_version():

=== modified file 'lpsetup/cli.py'
--- lpsetup/cli.py	2012-05-02 16:32:06 +0000
+++ lpsetup/cli.py	2012-05-15 17:00:40 +0000
@@ -15,6 +15,7 @@
     exceptions,
     )
 from lpsetup.subcommands import (
+    branch,
     install,
     lxcinstall,
     update,
@@ -26,6 +27,7 @@
     parser.register_subcommand('install', install.SubCommand)
     parser.register_subcommand('update', update.SubCommand)
     parser.register_subcommand('lxc-install', lxcinstall.SubCommand)
+    parser.register_subcommand('branch', branch.SubCommand)
     args = parser.parse_args()
     try:
         return args.main(args)

=== modified file 'lpsetup/handlers.py'
--- lpsetup/handlers.py	2012-04-20 14:56:44 +0000
+++ lpsetup/handlers.py	2012-05-15 17:00:40 +0000
@@ -6,6 +6,7 @@
 
 __metaclass__ = type
 __all__ = [
+    'handle_branch_creation',
     'handle_directories',
     'handle_lpuser',
     'handle_ssh_keys',
@@ -24,6 +25,7 @@
     )
 
 from lpsetup.exceptions import ValidationError
+from lpsetup.settings import LP_CHECKOUT
 
 
 def handle_user(namespace):
@@ -255,3 +257,22 @@
         namespace.create_scripts = True
         namespace.install_subunit = True
         namespace.use_urandom = True
+
+
+def handle_branch_creation(namespace):
+    """Handle directories involved in branch creation.
+
+    The validation fails if the branch parent does not exist, or if a branch
+    with the provided name already exists.
+
+    After this handler is called, `parent` and `branch` names
+    are present as attributes of the namespace.
+    """
+    base_dir = namespace.directory
+    parent = os.path.join(base_dir, LP_CHECKOUT)
+    branch = os.path.join(base_dir, namespace.name)
+    if not os.path.isdir(parent):
+        raise ValidationError('parent branch {0} not found.'.format(parent))
+    if os.path.exists(branch):
+        raise ValidationError('{0} already exists.'.format(branch))
+    namespace.parent, namespace.branch = parent, branch

=== modified file 'lpsetup/settings.py'
--- lpsetup/settings.py	2012-05-08 08:06:52 +0000
+++ lpsetup/settings.py	2012-05-15 17:00:40 +0000
@@ -35,15 +35,21 @@
     '/var/tmp/archive',
     '/var/tmp/ppa',
     )
-LP_BZR_LOCATIONS = (
-    ('submit_branch', '{checkout_dir}'),
-    ('public_branch', 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad'),
-    ('public_branch:policy', 'appendpath'),
-    ('push_location', 'lp:~{lpuser}/launchpad'),
-    ('push_location:policy', 'appendpath'),
-    ('merge_target', '{checkout_dir}'),
-    ('submit_to', 'merge@xxxxxxxxxxxxxxxxxx'),
-    )
+LP_BZR_LOCATIONS = {
+    '{directory}': {
+        'submit_branch': '{checkout_dir}',
+        'public_branch': 'bzr+ssh://bazaar.launchpad.net/~{lpuser}/launchpad',
+        'public_branch:policy': 'appendpath',
+        'push_location': 'lp:~{lpuser}/launchpad',
+        'push_location:policy': 'appendpath',
+        'merge_target': '{checkout_dir}',
+        'submit_to': 'merge@xxxxxxxxxxxxxxxxxx',
+        },
+    '{checkout_dir}': {
+        'public_branch':
+            'bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel',
+        }
+    }
 LP_CHECKOUT = 'devel'
 LP_PACKAGES = [
     # "launchpad-database-dependencies-9.1" can be removed once 8.x is

=== added file 'lpsetup/subcommands/branch.py'
--- lpsetup/subcommands/branch.py	1970-01-01 00:00:00 +0000
+++ lpsetup/subcommands/branch.py	2012-05-15 17:00:40 +0000
@@ -0,0 +1,88 @@
+#!/usr/bin/env python
+# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Branch subcommand: create and build a new Launchpad branch."""
+
+__metaclass__ = type
+__all__ = [
+    'create_branch',
+    'setup_and_make',
+    'SubCommand',
+    ]
+
+from shelltoolbox import (
+    cd,
+    get_su_command,
+    su,
+    )
+
+from lpsetup import (
+    argparser,
+    handlers,
+    )
+from lpsetup.settings import (
+    CHECKOUT_DIR,
+    DEPENDENCIES_DIR,
+    )
+from lpsetup.subcommands import install
+from lpsetup.utils import call
+
+
+def create_branch(user, parent, branch):
+    """Create a branch of `devel`."""
+    cmd = ('bzr', 'branch', parent, branch)
+    # Using real su because bzr uses uid.
+    call(*get_su_command(user, cmd), stderr=None)
+
+
+def setup_and_make(user, dependencies_dir, branch, make_schema):
+    """Set up external source code for the new branch and run `make`."""
+    with cd(branch):
+        with su(user):
+            call('utilities/link-external-sourcecode', dependencies_dir)
+    install.make_launchpad(user, branch, install=False)
+    if make_schema:
+        with su(user):
+            call('make', '-C', branch, 'schema')
+
+
+class SubCommand(argparser.StepsBasedSubCommand):
+    """Create and build a new Launchpad branch."""
+
+    steps = (
+        (create_branch,
+         'user', 'parent', 'branch'),
+        (setup_and_make,
+         'user', 'dependencies_dir', 'branch', 'make_schema'),
+        )
+    help = __doc__
+    validators = (
+        handlers.handle_user,
+        handlers.handle_directories,
+        handlers.handle_branch_creation,
+        )
+
+    def add_arguments(self, parser):
+        super(SubCommand, self).add_arguments(parser)
+        parser.add_argument(
+            '-u', '--user',
+            help='The name of the system user that will own the branch. '
+                 'The current user is used if this script is not run as '
+                 'root and this argument is omitted.')
+        parser.add_argument(
+            '-d', '--dependencies-dir', default=DEPENDENCIES_DIR,
+            help='The directory of the Launchpad dependencies to be linked. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(DEPENDENCIES_DIR))
+        parser.add_argument(
+            '-c', '--directory', default=CHECKOUT_DIR,
+            help='The directory of the existing Launchpad repository. '
+                 'The directory must reside under the home directory of the '
+                 'given user (see -u argument). '
+                 '[DEFAULT={0}]'.format(CHECKOUT_DIR))
+        parser.add_argument(
+            '-S', '--make-schema', action='store_true',
+            help='Run `make schema` in the newly created branch.')
+        parser.add_argument('name', help='The name of the branch to create.')

=== modified file 'lpsetup/subcommands/install.py'
--- lpsetup/subcommands/install.py	2012-05-08 13:43:08 +0000
+++ lpsetup/subcommands/install.py	2012-05-15 17:00:40 +0000
@@ -52,7 +52,10 @@
     LP_SOURCE_DEPS,
     SSH_KEY_NAME,
     )
-from lpsetup.utils import call
+from lpsetup.utils import (
+    call,
+    ConfigParser,
+    )
 
 
 def setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir):
@@ -157,14 +160,6 @@
     # Set up the codebase.
     checkout_dir = os.path.join(directory, LP_CHECKOUT)
     setup_codebase(user, valid_ssh_keys, checkout_dir, dependencies_dir)
-    # Set up bzr locations
-    with su(user) as env:
-        bzr_locations = os.path.join(env.home, '.bazaar', 'locations.conf')
-        file_append(bzr_locations, '[{0}]\n'.format(directory))
-        lines = ['{0} = {1}\n'.format(k, v) for k, v in LP_BZR_LOCATIONS]
-        for line in lines:
-            location = line.format(checkout_dir=checkout_dir, lpuser=lpuser)
-            file_append(bzr_locations, location)
     # rng-tools is used to set /dev/urandom as random data source, avoiding
     # entropy exhaustion during automated parallel tests.
     if use_urandom:
@@ -173,6 +168,29 @@
         call('/etc/init.d/rng-tools', 'start')
 
 
+def setup_bzr_locations(user, lpuser, directory, template=LP_BZR_LOCATIONS):
+    """Set up bazaar locations."""
+    context = {
+        'checkout_dir': os.path.join(directory, LP_CHECKOUT),
+        'directory': directory,
+        'lpuser': lpuser,
+        }
+    with su(user) as env:
+        bazaar_dir = os.path.join(env.home, '.bazaar')
+        mkdirs(bazaar_dir)
+        path = os.path.join(bazaar_dir, 'locations.conf')
+        parser = ConfigParser()
+        parser.read(path)
+        for section_template, options in template.items():
+            section = section_template.format(**context)
+            if not parser.has_section(section):
+                parser.add_section(section)
+            for option, value in options.items():
+                parser.set(section, option, value.format(**context))
+        with open(path, 'w') as f:
+            parser.write(f)
+
+
 def setup_apt(no_repositories=True):
     """Setup, update and upgrade deb packages."""
     if not no_repositories:
@@ -191,8 +209,8 @@
     """Set up the Launchpad environment."""
     # User configuration.
     subprocess.call(['adduser', user, 'sudo'])
-    gid = pwd.getpwnam(user).pw_gid
-    subprocess.call(['addgroup', '--gid', str(gid), user])
+    pwd_database = pwd.getpwnam(user)
+    subprocess.call(['addgroup', '--gid', str(pwd_database.pw_gid), user])
     # Set up Launchpad dependencies.
     checkout_dir = os.path.join(directory, LP_CHECKOUT)
     setup_external_sourcecode(
@@ -208,6 +226,8 @@
         call('utilities/launchpad-database-setup', user)
     # Make and install launchpad.
     make_launchpad(user, checkout_dir, install=True)
+    # Change owner of /srv/launchpad.dev/.
+    os.chown('/srv/launchpad.dev/', pwd_database.pw_uid, pwd_database.pw_gid)
     # Set up container hosts file.
     lines = ['{0}\t{1}\n'.format(ip, names) for ip, names in HOSTS_CONTENT]
     for line in lines:
@@ -222,6 +242,8 @@
          'user', 'full_name', 'email', 'lpuser',
          'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
          'use_urandom', 'dependencies_dir', 'directory'),
+        (setup_bzr_locations,
+         'user', 'lpuser', 'directory'),
         (setup_apt,
          'no_repositories'),
         (setup_launchpad,

=== modified file 'lpsetup/subcommands/lxcinstall.py'
--- lpsetup/subcommands/lxcinstall.py	2012-05-08 13:43:08 +0000
+++ lpsetup/subcommands/lxcinstall.py	2012-05-15 17:00:40 +0000
@@ -103,9 +103,11 @@
     #     much a Good Thing.
     # Disable the apparmor profiles for lxc so that we don't have
     # problems installing postgres.
-    call('ln', '-s',
-         '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/')
-    call('apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start')
+    subprocess.call([
+        'ln', '-s',
+        '/etc/apparmor.d/usr.bin.lxc-start', '/etc/apparmor.d/disable/'])
+    subprocess.call([
+        'apparmor_parser', '-R', '/etc/apparmor.d/usr.bin.lxc-start'])
     # Container configuration template.
     lxc_gateway = get_lxc_gateway()
     if lxc_gateway is None:
@@ -204,6 +206,8 @@
          'user', 'full_name', 'email', 'lpuser',
          'private_key', 'public_key', 'valid_ssh_keys', 'ssh_key_path',
          'use_urandom', 'dependencies_dir', 'directory'),
+        (install.setup_bzr_locations,
+         'user', 'lpuser', 'directory'),
         (create_scripts,
          'user', 'lxc_name', 'ssh_key_path'),
         (create_lxc,

=== modified file 'lpsetup/tests/test_handlers.py'
--- lpsetup/tests/test_handlers.py	2012-04-23 17:13:36 +0000
+++ lpsetup/tests/test_handlers.py	2012-05-15 17:00:40 +0000
@@ -7,11 +7,15 @@
 import argparse
 from contextlib import contextmanager
 import getpass
+import os
 import pwd
+import shutil
+import tempfile
 import unittest
 
 from lpsetup.exceptions import ValidationError
 from lpsetup.handlers import (
+    handle_branch_creation,
     handle_directories,
     handle_lpuser,
     handle_ssh_keys,
@@ -19,6 +23,7 @@
     handle_user,
     handle_userdata,
     )
+from lpsetup.settings import LP_CHECKOUT
 
 
 class HandlersTestMixin(object):
@@ -61,6 +66,42 @@
                 raise TypeError
 
 
+class HandleBranchCreationTest(HandlersTestMixin, unittest.TestCase):
+
+    def setUp(self):
+        directory = tempfile.mkdtemp()
+        self.namespace = argparse.Namespace(
+            directory=directory, name='new-branch')
+        self.parent = os.path.join(directory, LP_CHECKOUT)
+        os.mkdir(self.parent)
+
+    def get_branch(self):
+        return os.path.join(self.namespace.directory, self.namespace.name)
+
+    def tearDown(self):
+        shutil.rmtree(self.namespace.directory)
+
+    def test_correct_paths(self):
+        # Ensure the namespace contains the correct `parent` and `branch`
+        # attributes if the validation passes.
+        handle_branch_creation(self.namespace)
+        self.assertEqual(self.parent, self.namespace.parent)
+        self.assertEqual(self.get_branch(), self.namespace.branch)
+
+    def test_invalid_parent(self):
+        # The validation fails if the parent branch is not found.
+        os.rmdir(self.parent)
+        with self.assertNotValid(self.parent):
+            handle_branch_creation(self.namespace)
+
+    def test_invalid_branch_name(self):
+        # The validation fails if the branch already exists.
+        branch = self.get_branch()
+        os.mkdir(branch)
+        with self.assertNotValid(branch):
+            handle_branch_creation(self.namespace)
+
+
 class HandleDirectoriesTest(HandlersTestMixin, unittest.TestCase):
 
     home_dir = '/home/foo'

=== modified file 'lpsetup/tests/test_utils.py'
--- lpsetup/tests/test_utils.py	2012-04-30 16:53:35 +0000
+++ lpsetup/tests/test_utils.py	2012-05-15 17:00:40 +0000
@@ -8,6 +8,7 @@
 import os
 import shutil
 import sys
+import StringIO
 import tempfile
 import unittest
 
@@ -17,6 +18,7 @@
     LXC_NAME,
     )
 from lpsetup.utils import (
+    ConfigParser,
     get_container_path,
     get_lxc_gateway,
     get_network_interfaces,
@@ -28,6 +30,21 @@
     )
 
 
+class ConfigParserTest(unittest.TestCase):
+
+    def get_parser(self, config):
+        parser = ConfigParser()
+        parser.readfp(StringIO.StringIO(config))
+        return parser
+
+    def test_parser(self):
+        # Ensure the parser correctly parses options containing colons.
+        config = '[section]\noption1 = value1\noption2:colon = value2\n'
+        items = dict(self.get_parser(config).items('section'))
+        self.assertEqual('value1', items['option1'])
+        self.assertEqual('value2', items['option2:colon'])
+
+
 class GetContainerPathTest(unittest.TestCase):
 
     def test_root_path(self):

=== modified file 'lpsetup/utils.py'
--- lpsetup/utils.py	2012-05-02 16:32:06 +0000
+++ lpsetup/utils.py	2012-05-15 17:00:40 +0000
@@ -21,6 +21,7 @@
     'this_command',
     ]
 
+from ConfigParser import RawConfigParser
 from functools import (
     partial,
     wraps,
@@ -55,6 +56,20 @@
 call = partial(run, stdout=None)
 
 
+class ConfigParser(RawConfigParser):
+    """A customized configuration parser.
+
+    The base `RawConfigParser` separates options from values using a colon
+    or a equal sign. This parser forces the separator to be an equal sign,
+    allowing the colon to be part of an option name.
+    """
+    OPTCRE = re.compile(
+        r'(?P<option>[^=\s][^=]*)'  # very permissive!
+        r'\s*(?P<vi>=)\s*'          # space/tab, separator, space/tab
+        r'(?P<value>.*)$'           # everything up to eol
+        )
+
+
 def get_container_path(lxc_name, path='', base_path=LXC_PATH):
     """Return the path of LXC container called `lxc_name`.