diff mbox

[Branch,~linaro-validation/lava-dispatcher/trunk] Rev 584: Add highbank support

Message ID 20130419083517.15355.81166.launchpad@ackee.canonical.com
State Accepted
Headers show

Commit Message

Nick Schutt April 19, 2013, 8:35 a.m. UTC
Merge authors:
  Antonio Terceiro (terceiro)
  Michael Hudson-Doyle (mwhudson)
  Nicholas Schutt (nick-schutt)
Related merge proposals:
  https://code.launchpad.net/~nick-schutt/lava-dispatcher/nicks-highbank-support/+merge/159678
  proposed by: Nicholas Schutt (nick-schutt)
  review: Approve - Antonio Terceiro (terceiro)
------------------------------------------------------------
revno: 584 [merge]
committer: Nick Schutt <nick.schutt@linaro.org>
branch nick: trunk
timestamp: Fri 2013-04-19 10:35:03 +0200
message:
  Add highbank support
added:
  lava_dispatcher/default-config/lava-dispatcher/device-types/highbank.conf
  lava_dispatcher/device/highbank.py
  lava_dispatcher/ipmi.py
modified:
  lava_dispatcher/client/base.py
  lava_dispatcher/client/lmc_utils.py
  lava_dispatcher/config.py


--
lp:lava-dispatcher
https://code.launchpad.net/~linaro-validation/lava-dispatcher/trunk

You are subscribed to branch lp:lava-dispatcher.
To unsubscribe from this branch go to https://code.launchpad.net/~linaro-validation/lava-dispatcher/trunk/+edit-subscription
diff mbox

Patch

=== modified file 'lava_dispatcher/client/base.py'
--- lava_dispatcher/client/base.py	2013-03-27 11:22:07 +0000
+++ lava_dispatcher/client/base.py	2013-04-15 16:15:45 +0000
@@ -154,7 +154,7 @@ 
         lava_server_ip = self._client.context.config.lava_server_ip
         self.run(
             "LC_ALL=C ping -W4 -c1 %s" % lava_server_ip,
-            ["1 received", "0 received", "Network is unreachable"],
+            ["1 received|1 packets received", "0 received|0 packets received", "Network is unreachable"],
             timeout=5, failok=True)
         if self.match_id == 0:
             return True

=== modified file 'lava_dispatcher/client/lmc_utils.py'
--- lava_dispatcher/client/lmc_utils.py	2013-02-18 03:19:14 +0000
+++ lava_dispatcher/client/lmc_utils.py	2013-04-11 13:05:31 +0000
@@ -15,7 +15,8 @@ 
     )
 
 
-def generate_image(client, hwpack_url, rootfs_url, outdir, bootloader='u_boot', rootfstype=None):
+def generate_image(client, hwpack_url, rootfs_url, outdir, bootloader='u_boot', rootfstype=None,
+                   extra_boot_args=None, image_size=None):
     """Generate image from a hwpack and rootfs url
 
     :param hwpack_url: url of the Linaro hwpack to download
@@ -47,6 +48,10 @@ 
            (client.config.lmc_dev_arg, image_file, rootfs_path, hwpack_path, bootloader))
     if rootfstype is not None:
         cmd += ' --rootfs ' + rootfstype
+    if image_size is not None:
+        cmd += ' --image-size ' + image_size
+    if extra_boot_args is not None:
+        cmd += ' --extra-boot-args "%s"' % extra_boot_args
     logging.info("Executing the linaro-media-create command")
     logging.info(cmd)
 

=== modified file 'lava_dispatcher/config.py'
--- lava_dispatcher/config.py	2013-04-05 17:19:57 +0000
+++ lava_dispatcher/config.py	2013-04-08 13:29:48 +0000
@@ -104,6 +104,8 @@ 
         default='Press Enter to stop auto boot...')
     vexpress_usb_mass_storage_device = schema.StringOption(default=None)
 
+    ecmeip = schema.StringOption()
+
 class OptionDescriptor(object):
     def __init__(self, name):
         self.name = name

=== added file 'lava_dispatcher/default-config/lava-dispatcher/device-types/highbank.conf'
--- lava_dispatcher/default-config/lava-dispatcher/device-types/highbank.conf	1970-01-01 00:00:00 +0000
+++ lava_dispatcher/default-config/lava-dispatcher/device-types/highbank.conf	2013-02-14 03:14:34 +0000
@@ -0,0 +1,2 @@ 
+client_type = highbank
+connection_command = ipmitool -I lanplus -U admin -P admin -H %(ecmeip)s sol activate

=== added file 'lava_dispatcher/device/highbank.py'
--- lava_dispatcher/device/highbank.py	1970-01-01 00:00:00 +0000
+++ lava_dispatcher/device/highbank.py	2013-04-18 16:40:27 +0000
@@ -0,0 +1,286 @@ 
+# Copyright (C) 2012 Linaro Limited
+#
+# Author: Michael Hudson-Doyle <michael.hudson@linaro.org>
+# Author: Nicholas Schutt <nick.schutt@linaro.org>
+#
+# This file is part of LAVA Dispatcher.
+#
+# LAVA Dispatcher is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LAVA Dispatcher is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along
+# with this program; if not, see <http://www.gnu.org/licenses>.
+
+import contextlib
+import logging
+import os
+import pexpect
+import time
+
+from lava_dispatcher import tarballcache
+
+from lava_dispatcher.device.master import (
+    MasterCommandRunner,
+)
+from lava_dispatcher.device.target import (
+    Target
+)
+from lava_dispatcher.errors import (
+    NetworkError,
+    CriticalError,
+    OperationFailed,
+)
+from lava_dispatcher.downloader import (
+    download_image,
+    download_with_retry,
+    )
+from lava_dispatcher.utils import (
+    mk_targz,
+    rmtree,
+)
+from lava_dispatcher.client.lmc_utils import (
+    generate_image,
+)
+from lava_dispatcher.ipmi import IpmiPxeBoot
+
+
+class HighbankTarget(Target):
+
+    MASTER_PS1 = 'root@master [rc=$(echo \$?)]# '
+    MASTER_PS1_PATTERN = 'root@master \[rc=(\d+)\]# '
+
+    def __init__(self, context, config):
+        super(HighbankTarget, self).__init__(context, config)
+        self.proc = self.context.spawn(self.config.connection_command, timeout=1200)
+        self.device_version = None
+        if self.config.ecmeip == None:
+            msg = "The ecmeip address is not set for this target"
+            logging.error(msg)
+            raise CriticalError(msg)
+        self.bootcontrol = IpmiPxeBoot(context, self.config.ecmeip)
+
+    def get_device_version(self):
+        return self.device_version
+
+    def power_on(self):
+        self.bootcontrol.power_on_boot_image()
+        return self.proc
+
+    def power_off(self, proc):
+        self.bootcontrol.power_off()
+
+    def deploy_linaro(self, hwpack, rfs, bootloader):
+        image_file = generate_image(self, hwpack, rfs, self.scratch_dir, bootloader,
+                                    extra_boot_args='1', image_size='1G')
+        self._customize_linux(image_file)
+        self._deploy_image(image_file, '/dev/sda')
+
+    def deploy_linaro_prebuilt(self, image):
+        image_file = download_image(image, self.context, self.scratch_dir)
+        self._customize_linux(image_file)
+        self._deploy_image(image_file, '/dev/sda')
+
+    def _deploy_image(self, image_file, device):
+        with self._as_master() as runner:
+
+            # compress the image to reduce the transfer size
+            if not image_file.endswith('.bz2') and not image_file.endswith('gz'):
+                os.system('bzip2 -9v ' + image_file)
+                image_file += '.bz2'
+
+            tmpdir = self.context.config.lava_image_tmpdir
+            url = self.context.config.lava_image_url
+            image_file = image_file.replace(tmpdir, '')
+            image_url = '/'.join(u.strip('/') for u in [url, image_file])
+
+            build_dir = '/builddir'
+            image_file_base = build_dir + '/' + '/'.join(image_file.split('/')[-1:])
+
+            decompression_cmd = None
+            if image_file_base.endswith('.gz'):
+                decompression_cmd = '/bin/gzip -dc'
+            elif image_file_base.endswith('.bz2'):
+                decompression_cmd = '/bin/bzip2 -dc'
+
+            runner.run('mkdir %s' % build_dir)
+            runner.run('mount -t tmpfs -o size=100%% tmpfs %s' % build_dir)
+            runner.run('wget -O %s %s' % (image_file_base, image_url), timeout=1800)
+
+            if decompression_cmd != None:
+                cmd = '%s %s | dd bs=4M of=%s' % (decompression_cmd, image_file_base, device)
+            else:
+                cmd = 'dd bs=4M if=%s of=%s' % (image_file_base, device)
+
+            runner.run(cmd, timeout=1800)
+            runner.run('umount %s' % build_dir)
+
+            self.resize_rootfs_partition(runner)
+
+    def get_partition(self, runner, partition):
+        if partition == self.config.boot_part:
+            partition = '/dev/disk/by-label/boot'
+        elif partition == self.config.root_part:
+            partition = '/dev/disk/by-label/rootfs'
+        else:
+            raise RuntimeError(
+                'unknown master image partition(%d)' % partition)
+        return partition
+
+    def resize_rootfs_partition(self, runner):
+        partno = '2'
+        start = None
+
+        runner.run('parted -s /dev/sda print', 
+                   response='\s+%s\s+([0-9.]+.B)\s+\S+\s+\S+\s+primary\s+(\S+)' % partno,
+                   wait_prompt=False)
+        if runner.match_id != 0:
+            msg = "Unable to determine rootfs partition"
+            logging.warning(msg)
+        else:
+            start = runner.match.group(1)
+            parttype = runner.match.group(2)
+
+            if  parttype == 'ext2' or parttype == 'ext3' or parttype == 'ext4':
+                runner.run('parted -s /dev/sda rm %s' % partno)
+                runner.run('parted -s /dev/sda mkpart primary %s 100%%' % start)
+                runner.run('resize2fs -f /dev/sda%s' % partno)
+            elif parttpe == 'brtfs':
+                logging.warning("resize of btrfs partition not supported")
+            else:
+                logging.warning("unknown partition type for resize: %s" % parttype)
+
+
+    @contextlib.contextmanager
+    def file_system(self, partition, directory):
+        logging.info('attempting to access master filesystem %r:%s' %
+            (partition, directory))
+
+        assert directory != '/', "cannot mount entire partition"
+
+        with self._as_master() as runner:
+            runner.run('mkdir -p /mnt')
+            partition = self.get_partition(runner, partition)
+            runner.run('mount %s /mnt' % partition)
+            try:
+                targetdir = '/mnt/%s' % directory
+                runner.run('mkdir -p %s' % targetdir)
+
+                parent_dir, target_name = os.path.split(targetdir)
+
+                runner.run('/bin/tar -cmzf /tmp/fs.tgz -C %s %s' % (parent_dir, target_name))
+                runner.run('cd /tmp')  # need to be in same dir as fs.tgz
+
+                url_base = runner.start_http_server()
+                
+                url = url_base + '/fs.tgz'
+                logging.info("Fetching url: %s" % url)
+                tf = download_with_retry(self.context, self.scratch_dir, url, False)
+
+                tfdir = os.path.join(self.scratch_dir, str(time.time()))
+
+                try:
+                    os.mkdir(tfdir)
+                    self.context.run_command('/bin/tar -C %s -xzf %s' % (tfdir, tf))
+                    yield os.path.join(tfdir, target_name)
+
+                finally:
+                    tf = os.path.join(self.scratch_dir, 'fs.tgz')
+                    mk_targz(tf, tfdir)
+                    rmtree(tfdir)
+
+                    # get the last 2 parts of tf, ie "scratchdir/tf.tgz"
+                    tf = '/'.join(tf.split('/')[-2:])
+                    runner.run('rm -rf %s' % targetdir)
+                    self._target_extract(runner, tf, parent_dir)
+
+            finally:
+                    runner.stop_http_server()
+                    runner.run('umount /mnt')
+
+    def _target_extract(self, runner, tar_file, dest, timeout=-1):
+        tmpdir = self.context.config.lava_image_tmpdir
+        url = self.context.config.lava_image_url
+        tar_file = tar_file.replace(tmpdir, '')
+        tar_url = '/'.join(u.strip('/') for u in [url, tar_file])
+        self._target_extract_url(runner,tar_url,dest,timeout=timeout)
+
+    def _target_extract_url(self, runner, tar_url, dest, timeout=-1):
+        decompression_cmd = ''
+        if tar_url.endswith('.gz') or tar_url.endswith('.tgz'):
+            decompression_cmd = '| /bin/gzip -dc'
+        elif tar_url.endswith('.bz2'):
+            decompression_cmd = '| /bin/bzip2 -dc'
+        elif tar_url.endswith('.tar'):
+            decompression_cmd = ''
+        else:
+            raise RuntimeError('bad file extension: %s' % tar_url)
+
+        runner.run('wget -O - %s %s | /bin/tar -C %s -xmf -'
+            % (tar_url, decompression_cmd, dest),
+            timeout=timeout)
+
+    @contextlib.contextmanager
+    def _as_master(self):
+        self.bootcontrol.power_on_boot_master()
+
+        # Two reboots seem to be necessary to ensure that pxe boot is used.
+        # Need to identify the cause and fix it
+        self.proc.expect("Hit any key to stop autoboot:")
+        self.proc.sendline('')
+        self.bootcontrol.power_reset_boot_master()
+
+        self.proc.expect("\(initramfs\)")
+        self.proc.sendline('export PS1="%s"' % self.MASTER_PS1)
+        self.proc.expect(self.MASTER_PS1_PATTERN, timeout=180, lava_no_logging=1)
+        runner = HBMasterCommandRunner(self)
+        runner.run(". /scripts/functions")
+        device = "eth0"
+        runner.run("DEVICE=%s configure_networking" % device)
+
+        self.device_version = runner.get_device_version()
+
+        try:
+            yield runner
+        finally:
+           logging.debug("deploy done")
+
+
+target_class = HighbankTarget
+
+
+class HBMasterCommandRunner(MasterCommandRunner):
+    """A CommandRunner to use when the target is booted into the master image.
+    """
+    http_pid = None
+    
+    def __init__(self, target):
+        super(HBMasterCommandRunner, self).__init__(target)
+
+    def start_http_server(self):
+        master_ip = self.get_master_ip()
+        if self.http_pid != None:
+            raise OperationFailed("busybox httpd already running with pid %" % self.http_pid)
+        # busybox produces no output to parse for, so run it in the bg and get its pid
+        self.run('busybox httpd -f &')
+        self.run('echo pid:$!:pid',response="pid:(\d+):pid",timeout=10)
+        if self.match_id != 0:
+            raise OperationFailed("busybox httpd did not start")
+        else:
+            self.http_pid = self.match.group(1)
+        url_base = "http://%s" % (master_ip)
+        return url_base
+
+    def stop_http_server(self):
+        if self.http_pid == None:
+            raise OperationFailed("busybox httpd not running, but stop_http_server called.")
+        self.run('kill %s' % self.http_pid)
+        self.http_pid = None
+

=== added file 'lava_dispatcher/ipmi.py'
--- lava_dispatcher/ipmi.py	1970-01-01 00:00:00 +0000
+++ lava_dispatcher/ipmi.py	2013-04-12 10:01:48 +0000
@@ -0,0 +1,85 @@ 
+# Copyright (C) 2013 Linaro Limited
+#
+# Authors:
+#   Antonio Terceiro <antonio.terceiro@linaro.org>
+#   Michael Hudson-Doyle <michael.hudson@linaro.org>
+#
+# This file is part of LAVA Dispatcher.
+#
+# LAVA Dispatcher is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# LAVA Dispatcher is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.    See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along
+# with this program; if not, see <http://www.gnu.org/licenses>.
+
+
+class IPMITool(object):
+    """
+    This class wraps the ipmitool CLI to provide a convenient object-oriented
+    API that can be composed into the implementation of devices that can be
+    managed with IPMI.
+    """
+
+    def __init__(self, context, host, ipmitool="ipmitool"):
+        self.host = host
+        self.context = context
+        self.ipmitool = ipmitool
+
+    def __ipmi(self, command):
+        self.context.run_command(
+            "%s -H %s -U admin -P admin %s" % (
+                self.ipmitool, self.host, command
+            )
+        )
+
+    def set_to_boot_from_disk(self):
+        self.__ipmi("chassis bootdev disk")
+
+    def set_to_boot_from_pxe(self):
+        self.__ipmi("chassis bootdev pxe")
+
+    def power_off(self):
+        self.__ipmi("chassis power off")
+
+    def power_on(self):
+        self.__ipmi("chassis power on")
+
+    def reset(self):
+        self.__ipmi("chassis power reset")
+
+
+class IpmiPxeBoot(object):
+    """
+    This class provides a convenient object-oriented API that can be
+    used to initiate power on/off and boot device selection for pxe
+    and disk boot devices using ipmi commands.
+    """
+
+    def __init__(self, context, host):
+        self.ipmitool = IPMITool(context, host)
+
+    def power_on_boot_master(self):
+        self.ipmitool.set_to_boot_from_pxe()
+        self.ipmitool.power_on()
+        self.ipmitool.reset()
+
+    def power_reset_boot_master(self):
+        self.ipmitool.set_to_boot_from_pxe()
+        self.ipmitool.reset()
+
+    def power_on_boot_image(self):
+        self.ipmitool.set_to_boot_from_disk()
+        self.ipmitool.power_on()
+        self.ipmitool.reset()
+
+    def power_off(self):
+        self.ipmitool.power_off()
+