← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~andreserl/maas/trunk-fpi into lp:maas

 

Andres Rodriguez has proposed merging lp:~andreserl/maas/trunk-fpi into lp:maas.

Commit message:
Merge Fast Path Installer.

Requested reviews:
  Andres Rodriguez (andreserl)
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~andreserl/maas/trunk-fpi/+merge/163382
-- 
https://code.launchpad.net/~andreserl/maas/trunk-fpi/+merge/163382
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~andreserl/maas/trunk-fpi into lp:maas.
=== added file 'contrib/maas-cluster-http.conf'
--- contrib/maas-cluster-http.conf	1970-01-01 00:00:00 +0000
+++ contrib/maas-cluster-http.conf	2013-05-10 22:54:27 +0000
@@ -0,0 +1,6 @@
+# Server static files for tftp images as FPI
+# installer needs them
+Alias /MAAS/static/images/ /var/lib/maas/tftp/
+<Directory /var/lib/maas/tftp/>
+    SetHandler None
+</Directory>

=== modified file 'contrib/preseeds_v2/generic'
--- contrib/preseeds_v2/generic	2012-11-14 10:32:25 +0000
+++ contrib/preseeds_v2/generic	2013-05-10 22:54:27 +0000
@@ -1,4 +1,8 @@
+{{if node.should_use_traditional_installer() }}
 {{inherit "preseed_master"}}
+{{else}}
+{{inherit "preseed_xinstall"}}
+{{endif}}
 
 {{def proxy}}
 d-i     mirror/country string manual

=== added file 'contrib/preseeds_v2/preseed_xinstall'
--- contrib/preseeds_v2/preseed_xinstall	1970-01-01 00:00:00 +0000
+++ contrib/preseeds_v2/preseed_xinstall	2013-05-10 22:54:27 +0000
@@ -0,0 +1,1 @@
+# Disabled by default

=== modified file 'scripts/maas-import-ephemerals'
--- scripts/maas-import-ephemerals	2012-11-23 11:00:04 +0000
+++ scripts/maas-import-ephemerals	2013-05-10 22:54:27 +0000
@@ -239,6 +239,9 @@
         tar -Sxzf - -C  "$exdir" < "$tarball" ||
         { error "failed to extract tarball from $furl"; return 1; }
 
+    # Create root disk
+    uec2roottar "$tarball" || { error "failed to create root image"; return 1; }
+
     # Look for our files in the extracted tarball.
     local x="" img="" kernel="" initrd=""
     for x in "$exdir/"*.img; do
@@ -342,12 +345,14 @@
             return 1
         copy_first_available "$src/initrd.gz" "$src/initrd" "$tmpdir/initrd.gz" ||
             return 1
+        copy_first_available "$src/dist-root.tar.gz" "" "$tmpdir/root.tar.gz" ||
+            return 1
     fi
 
     local cmd out=""
     cmd=( maas-provision install-pxe-image
           "--arch=$arch" "--subarch=$subarch" "--release=$release"
-          --purpose="commissioning" --image="$tmpdir" )
+          --purpose="commissioning" --image="$tmpdir" --symlink="xinstall")
     debug 2 "${cmd[@]}"
     out=$("${cmd[@]}" 2>&1) ||
         { error "cmd failed:" "${cmd[@]}"; error "$out"; return 1; }

=== added file 'scripts/uec2roottar'
--- scripts/uec2roottar	1970-01-01 00:00:00 +0000
+++ scripts/uec2roottar	2013-05-10 22:54:27 +0000
@@ -0,0 +1,56 @@
+#!/bin/sh
+
+TEMP_D=""
+MP=""
+
+Usage() {
+cat <<EOF
+   ${0##*/} converts an ubuntu ephemeral image into a root
+   disk image tarball.
+
+   Usage: ${0##*/} uec-tarball [output]
+EOF
+}
+error() { echo "$(date -R):" "$@" 1>&2; }
+fail() { [ $# -eq 0 ] || error "$@"; exit 1; }
+cleanup() { 
+  [ -z "$MP" ] || umount "$MP"
+  [ -z "$TEMP_D" -o ! -d "$TEMP_D" ] || rm -Rf "${TEMP_D}";
+}
+
+[ "$1" = "-h" -o "$1" = "--help" ] && { Usage; exit 0; }
+[ $# -eq 1 -o $# -eq 2 ] || { Usage 1>&2; exit 1; }
+[ -f "$1" ] || { Usage 1>&2; error "$1: not a file"; exit 1; }
+
+[ "$(id -u)" = "0" ] || fail "must be root"
+
+TEMP_D=$(mktemp -d "${TMPDIR:-/tmp}/${0##*/}.XXXXXX") || fail "failed mktemp"
+trap cleanup EXIT
+
+uec="$1"
+output=${2}
+
+[ -n "$output" ] || output="${uec%.tar.gz}-root.tar.gz"; 
+error "converting ${uec} to ${output}"
+
+error "extracting *.img from ${uec}"
+tar -C ${TEMP_D} --wildcards "*.img" -Sxvzf "$uec" ||
+   fail "failed extract $uec"
+
+img=""
+for cand in "${TEMP_D}/"*.img; do
+   [ -z "$img" ] || fail "multiple .img files in $uec"
+   [ -f "$cand" ] && img="$cand"
+done
+
+[ -n "$img" ] || fail "failed to find image in $uec"
+
+mkdir "${TEMP_D}/mp" && mount -o ro "$img" "${TEMP_D}/mp" &&
+  MP="$TEMP_D/mp" || fail "failed to mount $img"
+
+error "copying contents of ${img#${TEMP_D}/} in ${uec} to ${output}"
+tar -C "$MP" -cpSzf "${output}" --numeric-owner . ||
+  fail "failed to create ${output}"
+
+error "finished. wrote to ${output}"
+

=== modified file 'setup.py'
--- setup.py	2012-12-18 17:06:43 +0000
+++ setup.py	2013-05-10 22:54:27 +0000
@@ -62,6 +62,7 @@
              'etc/maas/import_ephemerals',
              'etc/maas/import_pxe_files',
              'contrib/maas-http.conf',
+             'contrib/maas-cluster-http.conf',
              'contrib/maas_local_settings.py']),
         ('/usr/share/maas',
             ['contrib/wsgi.py',
@@ -73,10 +74,13 @@
              'contrib/preseeds_v2/enlist',
              'contrib/preseeds_v2/generic',
              'contrib/preseeds_v2/enlist_userdata',
+             'contrib/preseeds_v2/preseed_xinstall',
              'contrib/preseeds_v2/preseed_master']),
         ('/usr/sbin',
             ['scripts/maas-import-ephemerals',
              'scripts/maas-import-pxe-files']),
+        ('/usr/bin',
+            ['scripts/uec2roottar']),
     ],
 
     install_requires=[

=== modified file 'src/maasserver/api.py'
--- src/maasserver/api.py	2013-05-08 23:03:51 +0000
+++ src/maasserver/api.py	2013-05-10 22:54:27 +0000
@@ -1753,7 +1753,10 @@
     elif node.status == NODE_STATUS.ALLOCATED:
         # Install the node if netboot is enabled, otherwise boot locally.
         if node.netboot:
-            return "install"
+            if node.should_use_traditional_installer():
+                return "install"
+            else:
+                return "xinstall"
         else:
             return "local"  # TODO: Investigate.
     else:

=== modified file 'src/maasserver/models/nodegroup.py'
--- src/maasserver/models/nodegroup.py	2012-11-23 09:54:47 +0000
+++ src/maasserver/models/nodegroup.py	2013-05-10 22:54:27 +0000
@@ -204,6 +204,11 @@
             self.api_key = api_token.key
         return super(NodeGroup, self).save(*args, **kwargs)
 
+    def get_any_interface(self):
+        for interface in self.nodegroupinterface_set.all():
+            return interface
+        return None
+
     def get_managed_interface(self):
         """Return the interface for which MAAS managed the DHCP service.
 

=== modified file 'src/maasserver/preseed.py'
--- src/maasserver/preseed.py	2013-04-08 19:56:00 +0000
+++ src/maasserver/preseed.py	2013-05-10 22:54:27 +0000
@@ -232,7 +232,13 @@
     else:
         base_url = nodegroup.maas_url
         cluster_if = nodegroup.get_managed_interface()
-        cluster_host = None if cluster_if is None else cluster_if.ip
+        any_cluster_if = nodegroup.get_any_interface()
+        cluster_host = None
+        if cluster_if is None:
+            if any_cluster_if is not None:
+                cluster_host = any_cluster_if.ip
+        else:
+            cluster_host = cluster_if.ip
     return {
         'main_archive_hostname': main_archive_hostname,
         'main_archive_directory': main_archive_directory,

=== modified file 'src/maasserver/tests/test_api.py'
--- src/maasserver/tests/test_api.py	2013-05-08 23:03:51 +0000
+++ src/maasserver/tests/test_api.py	2013-05-10 22:54:27 +0000
@@ -3776,11 +3776,14 @@
             ("poweroff", {"status": NODE_STATUS.READY}),
             ("poweroff", {"status": NODE_STATUS.RESERVED}),
             ("install", {"status": NODE_STATUS.ALLOCATED, "netboot": True}),
+            ("xinstall", {"status": NODE_STATUS.ALLOCATED, "netboot": True}),
             ("local", {"status": NODE_STATUS.ALLOCATED, "netboot": False}),
             ("poweroff", {"status": NODE_STATUS.RETIRED}),
             ]
         node = factory.make_node()
         for purpose, parameters in options:
+            if purpose == "xinstall":
+                node.use_fastpath_installer()
             for name, value in parameters.items():
                 setattr(node, name, value)
             self.assertEqual(purpose, api.get_boot_purpose(node))

=== modified file 'src/maasserver/tests/test_preseed.py'
--- src/maasserver/tests/test_preseed.py	2013-02-21 23:32:20 +0000
+++ src/maasserver/tests/test_preseed.py	2013-05-10 22:54:27 +0000
@@ -368,17 +368,19 @@
             nodegroup.get_managed_interface().ip,
             context["cluster_host"])
 
-    def test_preseed_context_null_cluster_host_if_unmanaged(self):
-        # If the nodegroup has no managed interface recorded, which is
-        # possible in the data model but would be a bit weird, the
-        # cluster_host context variable is present, but None.
+    def test_preseed_context_cluster_host_if_unmanaged(self):
+        # If the nodegroup has no managed interface recorded, the cluster_host
+        # context variable is still present and derived from the nodegroup.
         release = factory.getRandomString()
         nodegroup = factory.make_node_group(maas_url=factory.getRandomString())
         for interface in nodegroup.nodegroupinterface_set.all():
             interface.management = NODEGROUPINTERFACE_MANAGEMENT.UNMANAGED
             interface.save()
         context = get_preseed_context(release, nodegroup)
-        self.assertIsNone(context["cluster_host"])
+        self.assertIsNotNone(context["cluster_host"])
+        self.assertEqual(
+            nodegroup.get_any_interface().ip,
+            context["cluster_host"])
 
     def test_preseed_context_null_cluster_host_if_does_not_exist(self):
         # If there's no nodegroup, the cluster_host context variable is
@@ -525,6 +527,12 @@
         preseed = get_preseed(node)
         self.assertIn('preseed/late_command', preseed)
 
+    def test_get_preseed_returns_xinstall_preseed(self):
+        node = factory.make_node()
+        node.use_fastpath_installer()
+        preseed = get_preseed(node)
+        self.assertIn('# Disabled by default', preseed)
+
     def test_get_enlist_preseed_returns_enlist_preseed(self):
         preseed = get_enlist_preseed()
         self.assertTrue(preseed.startswith('#cloud-config'))

=== modified file 'src/provisioningserver/kernel_opts.py'
--- src/provisioningserver/kernel_opts.py	2013-02-01 01:07:25 +0000
+++ src/provisioningserver/kernel_opts.py	2013-05-10 22:54:27 +0000
@@ -124,11 +124,11 @@
 
 def compose_purpose_opts(params):
     """Return the list of the purpose-specific kernel options."""
-    if params.purpose == "commissioning":
+    if params.purpose == "commissioning" or params.purpose == "xinstall":
         # These are kernel parameters read by the ephemeral environment.
         tname = "%s:%s" % (ISCSI_TARGET_NAME_PREFIX,
                            get_ephemeral_name(params.release, params.arch))
-        return [
+        kernel_params = [
             # Read by the open-iscsi initramfs code.
             "iscsi_target_name=%s" % tname,
             "iscsi_target_ip=%s" % params.fs_host,
@@ -145,6 +145,9 @@
             # Read by cloud-init.
             "cloud-config-url=%s" % params.preseed_url,
             ]
+        if params.purpose == "xinstall":
+            kernel_params.append("ds=nocloud-net")
+        return kernel_params
     else:
         # These are options used by the Debian Installer.
         return [

=== added file 'src/provisioningserver/pxe/config.xinstall.template'
--- src/provisioningserver/pxe/config.xinstall.template	1970-01-01 00:00:00 +0000
+++ src/provisioningserver/pxe/config.xinstall.template	2013-05-10 22:54:27 +0000
@@ -0,0 +1,20 @@
+DEFAULT execute
+
+SAY Booting under MAAS direction...
+SAY {{kernel_params() | kernel_command}}
+
+LABEL execute
+  KERNEL ifcpu64.c32
+  APPEND amd64 -- i386
+
+LABEL amd64
+  KERNEL {{kernel_params(arch="amd64") | kernel_path }}
+  INITRD {{kernel_params(arch="amd64") | initrd_path }}
+  APPEND {{kernel_params(arch="amd64") | kernel_command}}
+  IPAPPEND 2
+
+LABEL i386
+  KERNEL {{kernel_params(arch="i386") | kernel_path }}
+  INITRD {{kernel_params(arch="i386") | initrd_path }}
+  APPEND {{kernel_params(arch="i386") | kernel_command}}
+  IPAPPEND 2

=== modified file 'src/provisioningserver/pxe/install_image.py'
--- src/provisioningserver/pxe/install_image.py	2012-08-31 17:21:01 +0000
+++ src/provisioningserver/pxe/install_image.py	2013-05-10 22:54:27 +0000
@@ -70,7 +70,7 @@
         return False
 
 
-def install_dir(new, old):
+def install_dir(new, old, symlink=None):
     """Install directory `new`, replacing directory `old` if it exists.
 
     This works as atomically as possible, but isn't entirely.  Moreover,
@@ -121,6 +121,13 @@
     # Now delete the old image directory at leisure.
     rmtree('%s.old' % old, ignore_errors=True)
 
+    # Symlink the new image directory to 'symlink'.
+    if symlink is not None:
+        sdest = "%s/%s" % (os.path.dirname(old), symlink)
+        if os.path.exists(sdest) or os.path.islink(sdest):
+            os.unlink(sdest)
+        os.symlink(old, sdest)
+
 
 def add_arguments(parser):
     parser.add_argument(
@@ -138,6 +145,9 @@
     parser.add_argument(
         '--image', dest='image', default=None,
         help="Netboot image directory, containing kernel & initrd.")
+    parser.add_argument(
+        '--symlink', dest='symlink', default=None,
+        help="Destination directory to symlink the installed images to.")
 
 
 def run(args):
@@ -154,5 +164,5 @@
         tftproot, args.arch, args.subarch, args.release, args.purpose)
     if not are_identical_dirs(destination, args.image):
         # Image has changed.  Move the new version into place.
-        install_dir(args.image, destination)
+        install_dir(args.image, destination, args.symlink)
     rmtree(args.image, ignore_errors=True)

=== modified file 'src/provisioningserver/pxe/tests/test_config.py'
--- src/provisioningserver/pxe/tests/test_config.py	2012-09-19 13:51:15 +0000
+++ src/provisioningserver/pxe/tests/test_config.py	2013-05-10 22:54:27 +0000
@@ -223,14 +223,22 @@
         self.assertIn("chain.c32", output)
         self.assertNotIn("LOCALBOOT", output)
 
-    def test_render_pxe_config_for_commissioning(self):
+class TestRenderPXEConfigScenarios(TestCase):
+    """Tests for `provisioningserver.pxe.config.render_pxe_config`."""
+
+    scenarios = [
+        ("commissioning", dict(purpose="commissioning")),
+        ("xinstall", dict(purpose="xinstall")),
+        ]
+
+    def test_render_pxe_config_scenarios(self):
         # The commissioning config uses an extra PXELINUX module to auto
         # select between i386 and amd64.
         get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
         get_ephemeral_name.return_value = factory.make_name("ephemeral")
         options = {
             "kernel_params":
-                make_kernel_parameters(purpose="commissioning"),
+                make_kernel_parameters(purpose=self.purpose),
             }
         output = render_pxe_config(**options)
         config = parse_pxe_config(output)

=== modified file 'src/provisioningserver/pxe/tests/test_install_image.py'
--- src/provisioningserver/pxe/tests/test_install_image.py	2012-08-31 15:41:18 +0000
+++ src/provisioningserver/pxe/tests/test_install_image.py	2013-05-10 22:54:27 +0000
@@ -209,3 +209,38 @@
         self.assertEqual(
             "rw-r--r--",
             target_dir.child("image").getPermissions().shorthand())
+
+    def test_install_dir_moves_dir_into_place_with_symlink(self):
+        download_image = os.path.join(self.make_dir(), 'download-image')
+        published_image = os.path.join(self.make_dir(), 'published-image')
+        base_path = os.path.dirname(published_image)
+        symlink_dest = 'xinstall'
+        contents = factory.getRandomString()
+        os.makedirs(download_image)
+        sample_file = factory.make_file(download_image, contents=contents)
+        install_dir(download_image, published_image, symlink_dest)
+        self.assertThat(
+            os.path.join(published_image, os.path.basename(sample_file)),
+            FileContains(contents))
+        self.assertThat(
+            os.path.join(base_path, symlink_dest, os.path.basename(sample_file)),
+            FileContains(contents))
+
+    def test_install_dir_replaces_existing_dir_with_symlink(self):
+        download_image = os.path.join(self.make_dir(), 'download-image')
+        published_image = os.path.join(self.make_dir(), 'published-image')
+        base_path = os.path.dirname(published_image)
+        symlink_dest = 'xinstall'
+        os.makedirs(download_image)
+        sample_file = factory.make_file(download_image)
+        os.makedirs(published_image)
+        os.symlink(published_image, os.path.join(base_path, symlink_dest))
+        obsolete_file = factory.make_file(published_image)
+        install_dir(download_image, published_image, symlink_dest)
+        self.assertThat(
+            os.path.join(published_image, os.path.basename(sample_file)),
+            FileExists())
+        self.assertThat(obsolete_file, Not(FileExists()))
+        self.assertThat(
+            os.path.join(base_path, symlink_dest, os.path.basename(sample_file)),
+            FileExists())

=== modified file 'src/provisioningserver/tests/test_kernel_opts.py'
--- src/provisioningserver/tests/test_kernel_opts.py	2013-02-01 01:07:25 +0000
+++ src/provisioningserver/tests/test_kernel_opts.py	2013-05-10 22:54:27 +0000
@@ -118,6 +118,22 @@
             "netcfg/choose_interface=auto",
             compose_kernel_command_line(params))
 
+    def test_xinstall_compose_kernel_command_line_inc_purpose_opts(self):
+        # The result of compose_kernel_command_line includes the purpose
+        # options for a non "xinstall" node.
+        get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
+        get_ephemeral_name.return_value = "RELEASE-ARCH"
+        params = make_kernel_parameters(purpose="xinstall")
+        cmdline = compose_kernel_command_line(params)
+        self.assertThat(
+            cmdline,
+            ContainsAll([
+                "ds=nocloud-net",
+                "root=/dev/disk/by-path/ip-",
+                "iscsi_initiator=",
+                "overlayroot=tmpfs",
+                "ip=::::%s:BOOTIF" % params.hostname]))
+
     def test_commissioning_compose_kernel_command_line_inc_purpose_opts(self):
         # The result of compose_kernel_command_line includes the purpose
         # options for a non "commissioning" node.
@@ -147,8 +163,8 @@
         self.assertNotIn(cmdline, "None")
 
     def test_compose_kernel_command_line_inc_common_opts(self):
-        # Test that some kernel arguments appear on both commissioning
-        # and install command lines.
+        # Test that some kernel arguments appear on commissioning, install
+        # and xinstall command lines.
         get_ephemeral_name = self.patch(kernel_opts, "get_ephemeral_name")
         get_ephemeral_name.return_value = "RELEASE-ARCH"
         expected = ["nomodeset"]
@@ -159,6 +175,11 @@
         self.assertThat(cmdline, ContainsAll(expected))
 
         params = make_kernel_parameters(
+            purpose="xinstall", arch="i386")
+        cmdline = compose_kernel_command_line(params)
+        self.assertThat(cmdline, ContainsAll(expected))
+
+        params = make_kernel_parameters(
             purpose="install", arch="i386")
         cmdline = compose_kernel_command_line(params)
         self.assertThat(cmdline, ContainsAll(expected))
@@ -182,6 +203,23 @@
         factory.make_file(
             ephemeral_dir, name='info', contents=ephemeral_info)
 
+    def test_compose_kernel_command_line_inc_purpose_opts_xinstall_node(self):
+        # The result of compose_kernel_command_line includes the purpose
+        # options for a "xinstall" node.
+        ephemeral_name = factory.make_name("ephemeral")
+        params = make_kernel_parameters(purpose="xinstall")
+        self.create_ephemeral_info(
+            ephemeral_name, params.arch, params.release)
+        self.assertThat(
+            compose_kernel_command_line(params),
+            ContainsAll([
+                "ds=nocloud-net",
+                "iscsi_target_name=%s:%s" % (
+                    ISCSI_TARGET_NAME_PREFIX, ephemeral_name),
+                "iscsi_target_port=3260",
+                "iscsi_target_ip=%s" % params.fs_host,
+                ]))
+
     def test_compose_kernel_command_line_inc_purpose_opts_comm_node(self):
         # The result of compose_kernel_command_line includes the purpose
         # options for a "commissioning" node.


Follow ups