diff mbox

[Branch,~linaro-validation/lava-dispatcher/trunk] Rev 519: sdmux support

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

Commit Message

Andy Doan Jan. 4, 2013, 7:28 p.m. UTC
Merge authors:
  Andy Doan (doanac)
Related merge proposals:
  https://code.launchpad.net/~doanac/lava-dispatcher/sdmux-support/+merge/141494
  proposed by: Andy Doan (doanac)
  review: Approve - Michael Hudson-Doyle (mwhudson)
------------------------------------------------------------
revno: 519 [merge]
committer: Andy Doan <andy.doan@linaro.org>
branch nick: lava-dispatcher
timestamp: Fri 2013-01-04 13:26:51 -0600
message:
  sdmux support
added:
  doc/sdmux.png
  doc/sdmux.rst
  lava_dispatcher/device/sdmux.py
  lava_dispatcher/device/sdmux.sh
modified:
  doc/index.rst
  lava_dispatcher/config.py
  lava_dispatcher/device/master.py
  lava_dispatcher/utils.py
  setup.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 'doc/index.rst'
--- doc/index.rst	2012-12-16 22:24:10 +0000
+++ doc/index.rst	2013-01-04 00:03:43 +0000
@@ -36,6 +36,7 @@ 
    lava_test_shell.rst
    external_measurement.rst
    arm_energy_probe.rst
+   sdmux.rst
    proxy.rst
 
 * :ref:`search`

=== added file 'doc/sdmux.png'
Binary files doc/sdmux.png	1970-01-01 00:00:00 +0000 and doc/sdmux.png	2013-01-04 00:03:43 +0000 differ
=== added file 'doc/sdmux.rst'
--- doc/sdmux.rst	1970-01-01 00:00:00 +0000
+++ doc/sdmux.rst	2013-01-04 19:26:51 +0000
@@ -0,0 +1,89 @@ 
+Configuring and Using the SD-Mux
+================================
+
+An sd-mux is a piece of hardware that's been created that allows a single
+SD card to be controlled by two different card readers. Access between the
+card readers is mutually exclusive and requires them to work in conjunction
+with each other. This provides an extremely useful way to deal with embedded
+system use cases for devices that boot from an SD card.
+
+LAVA uses sd-mux devices to allow running unmodified test images including
+bootloaders on test devices. This is a big improvement to the
+`master image`_ approach.
+
+.. _`master image`: http://lava.readthedocs.org/en/latest/lava-image-creation.html#preparing-a-master-image
+
+.. image:: sdmux.png
+
+Manual Usage
+------------
+
+Before deploying to LAVA, its probably best to understand the mechanics of
+the actual device and ensure its functioning. A setup like in the image above
+is assumed where:
+
+ * the target end of the mux is plugged into a dev board
+ * the host end is plugged into a USB SD card reader
+ * the SD card reader is plugged into a USB hub that's plugged into the host
+
+With that in place, the device can be identified. The easiest way to do this
+is:
+
+ * ensure the target device is off
+ * cause a usb plug event on the host (unplug and plug the usb hub)
+
+At this point, "dmesg" should show what device this SD card appeared under
+like "/dev/sdb". Since these entries can change, the sd-mux code needs to know
+the actual USB device/port information. This can be found with the sdmux.sh
+script by running::
+
+  ./sdmux.sh -f /dev/sdb
+  Finding id for /dev/sdb
+  Device: /devices/pci0000:00/0000:00:1d.7/usb2/2-1/2-1.1
+  Bus ID: 2-1.1
+
+The key piece of information is "Bus ID: 2-1.1". This is required by the sdmux
+script to turn on/off the USB port with access to the device. To turn the
+device off which gives the target safe access run::
+
+  ID=2-1.1
+  ./sdmux -d $ID off
+
+At this point the target can be powered on and use the device. After powering
+off the target, the sd-card can be access on the host with::
+
+  ./sdmux -d $ID on
+
+This command will also print out the device entry like "/dev/sdb" to STDOUT
+
+Deploying in LAVA
+-----------------
+
+In order for the dispatcher's sd-mux driver to work a few fields must be added
+the device config::
+
+  # client_type required so that the sdmux driver will be used
+  client_type = sdmux
+  # this is the ID as discovered above using "sdmux.sh -f"
+  sdmux_id = 2-1.1
+  # sdmux_version is optional, but can be used to help identify which hardware
+  # revision this target is using.
+  sdmux_version = 0.01-dave_anders
+  # power on/off commands are also required
+  power_on_cmd = /usr/local/bin/pdu_power.sh 1 1
+  power_off_cmd = /usr/local/bin/pdu_power.sh 1 0
+
+About Kernel Versions
+---------------------
+
+Testing of the sdmux code was done on Ubuntu Precise (12.04.1 LTS). As January
+2013, some newer kernels are demonstrating bugs when toggling the sdmux on/off
+from the host using the sdmux.sh script. Here's a list of what's currently
+known:
+
+ * 3.2.0-31 - Works
+ * 3.2.0-32 - Looks like it will work. Target boots, so its reading partition 1.
+   However, it can't mount the root partition. This likely implies that the host
+   is still supplying some amount of current to the USB port.
+ * 3.2.0-34 - Not working at all. sdmux.sh can turn off the port, but not turn
+   it back on.

=== modified file 'lava_dispatcher/config.py'
--- lava_dispatcher/config.py	2012-12-16 22:20:53 +0000
+++ lava_dispatcher/config.py	2013-01-04 00:03:43 +0000
@@ -57,6 +57,8 @@ 
     pre_connect_command = schema.StringOption()
     qemu_drive_interface = schema.StringOption()
     qemu_machine_type = schema.StringOption()
+    power_on_cmd = schema.StringOption()  # for sdmux
+    power_off_cmd = schema.StringOption()  # for sdmux
     reset_port_command = schema.StringOption()
     root_part = schema.IntOption()
     sdcard_part_android = schema.IntOption()
@@ -69,6 +71,9 @@ 
     possible_partitions_files = schema.ListOption(default=["init.partitions.rc",
                                                            "fstab.partitions",
                                                            "init.rc"])
+    # see doc/sdmux.rst for details
+    sdmux_id = schema.StringOption()
+    sdmux_version = schema.StringOption(default="unknown")
 
     simulator_version_command = schema.StringOption()
     simulator_command = schema.StringOption()

=== modified file 'lava_dispatcher/device/master.py'
--- lava_dispatcher/device/master.py	2012-12-18 19:50:48 +0000
+++ lava_dispatcher/device/master.py	2012-12-29 03:50:52 +0000
@@ -19,7 +19,6 @@ 
 # along
 # with this program; if not, see <http://www.gnu.org/licenses>.
 
-import atexit
 import contextlib
 import logging
 import os
@@ -47,7 +46,7 @@ 
     OperationFailed,
 )
 from lava_dispatcher.utils import (
-    logging_spawn,
+    connect_to_serial,
     logging_system,
     mk_targz,
     string_to_list,
@@ -86,8 +85,7 @@ 
         if config.pre_connect_command:
             logging_system(config.pre_connect_command)
 
-        self.proc = self._connect_carefully(config.connection_command)
-        atexit.register(self._close_logging_spawn)
+        self.proc = connect_to_serial(config, self.sio)
 
     def get_device_version(self):
         return self.device_version
@@ -316,53 +314,6 @@ 
             finally:
                 runner.run('umount /mnt')
 
-    def _connect_carefully(self, cmd):
-        retry_count = 0
-        retry_limit = 3
-
-        port_stuck_message = 'Data Buffering Suspended\.'
-        conn_closed_message = 'Connection closed by foreign host\.'
-
-        expectations = {
-            port_stuck_message: 'reset-port',
-            'Connected\.\r': 'all-good',
-            conn_closed_message: 'retry',
-            pexpect.TIMEOUT: 'all-good',
-            }
-        patterns = []
-        results = []
-        for pattern, result in expectations.items():
-            patterns.append(pattern)
-            results.append(result)
-
-        while retry_count < retry_limit:
-            proc = logging_spawn(cmd, timeout=1200)
-            proc.logfile_read = self.sio
-            logging.info('Attempting to connect to device')
-            match = proc.expect(patterns, timeout=10)
-            result = results[match]
-            logging.info('Matched %r which means %s', patterns[match], result)
-            if result == 'retry':
-                proc.close(True)
-                retry_count += 1
-                time.sleep(5)
-                continue
-            elif result == 'all-good':
-                return proc
-            elif result == 'reset-port':
-                reset_port = self.config.reset_port_command
-                if reset_port:
-                    logging_system(reset_port)
-                else:
-                    raise OperationFailed("no reset_port command configured")
-                proc.close(True)
-                retry_count += 1
-                time.sleep(5)
-        raise OperationFailed("could execute connection_command successfully")
-
-    def _close_logging_spawn(self):
-        self.proc.close(True)
-
     def _wait_for_master_boot(self):
         self.proc.expect(self.config.image_boot_msg, timeout=300)
         self.proc.expect(self.config.master_str, timeout=300)

=== added file 'lava_dispatcher/device/sdmux.py'
--- lava_dispatcher/device/sdmux.py	1970-01-01 00:00:00 +0000
+++ lava_dispatcher/device/sdmux.py	2013-01-04 19:26:51 +0000
@@ -0,0 +1,221 @@ 
+# Copyright (C) 2012 Linaro Limited
+#
+# Author: Andy Doan <andy.doan@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 subprocess
+import sys
+import time
+
+from lava_dispatcher.errors import (
+    CriticalError,
+)
+from lava_dispatcher.device.target import (
+    Target
+)
+from lava_dispatcher.client.lmc_utils import (
+    generate_android_image,
+    generate_image,
+    image_partition_mounted,
+)
+from lava_dispatcher.downloader import (
+    download_image,
+)
+from lava_dispatcher.utils import (
+    connect_to_serial,
+    ensure_directory,
+    extract_targz,
+    logging_system,
+)
+
+
+class SDMuxTarget(Target):
+    """
+    This adds support for the "sd mux" device. An SD-MUX device is a piece of
+    hardware that allows the host and target to both connect to the same SD
+    card. The control of the SD card can then be toggled between the target
+    and host via software. The schematics and pictures of this device can be
+    found at:
+      http://people.linaro.org/~doanac/sdmux/
+
+    Documentation for setting this up is located under doc/sdmux.rst.
+
+    NOTE: please read doc/sdmux.rst about kernel versions
+    """
+
+    def __init__(self, context, config):
+        super(SDMuxTarget, self).__init__(context, config)
+
+        self.proc = None
+
+        if not config.sdmux_id:
+            raise CriticalError('Device config requires "sdmux_id"')
+        if not config.power_on_cmd:
+            raise CriticalError('Device config requires "power_on_cmd"')
+        if not config.power_off_cmd:
+            raise CriticalError('Device config requires "power_off_cmd"')
+
+        if config.pre_connect_command:
+            logging_system(config.pre_connect_command)
+
+    def deploy_linaro(self, hwpack=None, rootfs=None):
+        img = generate_image(self, hwpack, rootfs, self.scratch_dir)
+        self._customize_linux(img)
+        self._write_image(img)
+
+    def deploy_linaro_prebuilt(self, image):
+        img = download_image(image, self.context)
+        self._customize_linux(img)
+        self._write_image(img)
+
+    def _customize_android(self, img):
+        sys_part = self.config.sys_part_android_org
+        with image_partition_mounted(img, sys_part) as d:
+            with open('%s/etc/mkshrc' % d, 'a') as f:
+                f.write('\n# LAVA CUSTOMIZATIONS\n')
+                f.write('PS1="%s"\n' % self.ANDROID_TESTER_PS1)
+        self.deployment_data = Target.android_deployment_data
+
+    def deploy_android(self, boot, system, data):
+        scratch = self.scratch_dir
+        boot = download_image(boot, self.context, scratch, decompress=False)
+        data = download_image(data, self.context, scratch, decompress=False)
+        system = download_image(system, self.context, scratch, decompress=False)
+
+        img = os.path.join(scratch, 'android.img')
+        device_type = self.config.lmc_dev_arg
+        generate_android_image(device_type, boot, data, system, img)
+        self._customize_android(img)
+        self._write_image(img)
+
+    def _as_chunks(self, fname, bsize):
+        with open(fname, 'r') as fd:
+            while True:
+                data = fd.read(bsize)
+                if not data:
+                    break
+                yield data
+
+    def _write_image(self, image):
+        with self.mux_device() as device:
+            logging.info("dd'ing image to device (%s)", device)
+            with open(device, 'w') as of:
+                written = 0
+                size = os.path.getsize(image)
+                # 4M chunks work well for SD cards
+                for chunk in self._as_chunks(image, 4 << 20):
+                    of.write(chunk)
+                    written += len(chunk)
+                    if written % (20 * (4 << 20)) == 0:  # only log every 80MB
+                        logging.info("wrote %d of %d bytes", written, size)
+                logging.info('closing %s, could take a while...', device)
+
+    @contextlib.contextmanager
+    def mux_device(self):
+        """
+        This function gives us a safe context in which to deal with the
+        raw sdmux device. It will ensure that:
+          * the target is powered off
+          * the proper sdmux USB device is powered on
+
+        It will then yield to the caller a dev entry like /dev/sdb
+        This entry can be used safely during this context. Upon exiting,
+        the USB device connect to the sdmux will be powered off so that the
+        target will be able to safely access it.
+        """
+        muxid = self.config.sdmux_id
+        source_dir = os.path.abspath(os.path.dirname(__file__))
+        muxscript = os.path.join(source_dir, 'sdmux.sh')
+
+        self.power_off(self.proc)
+        self.proc = None
+
+        try:
+            deventry = subprocess.check_output([muxscript, '-d', muxid, 'on'])
+            deventry = deventry.strip()
+            logging.info('returning sdmux device as: %s', deventry)
+            yield deventry
+        except subprocess.CalledProcessError:
+            raise CriticalError('Unable to access sdmux device')
+        finally:
+            logging.info('powering off sdmux')
+            subprocess.check_call([muxscript, '-d', muxid, 'off'])
+
+    @contextlib.contextmanager
+    def file_system(self, partition, directory):
+        """
+        This works in cojunction with the "mux_device" function to safely
+        access a partition/directory on the sdmux filesystem
+        """
+        mntdir = os.path.join(self.scratch_dir, 'sdmux_mnt')
+        if not os.path.exists(mntdir):
+            os.mkdir(mntdir)
+
+        with self.mux_device() as device:
+            device = '%s%s' % (device, partition)
+            try:
+                subprocess.check_call(['mount', device, mntdir])
+                if directory[0] == '/':
+                    directory = directory[1:]
+                path = os.path.join(mntdir, directory)
+                ensure_directory(path)
+                logging.info('sdmux(%s) mounted at: %s', device, path)
+                yield path
+            except CriticalError:
+                raise
+            except subprocess.CalledProcessError:
+                raise CriticalError('Unable to access sdmux device')
+            except:
+                logging.exception('Error accessing sdmux filesystem')
+                raise CriticalError('Error accessing sdmux filesystem')
+            finally:
+                logging.info('unmounting sdmux')
+                try:
+                    subprocess.check_call(['umount', device])
+                except subprocess.CalledProcessError:
+                    logging.exception('umount failed, re-try in 5 seconds')
+                    time.sleep(5)
+                    if subprocess.call(['umount', device]) == 0:
+                        logging.error(
+                            'Unable to unmount sdmux device %s', device)
+
+    def extract_tarball(self, tarball_url, partition, directory='/'):
+        logging.info('extracting %s to target', tarball_url)
+        with self.file_system(partition, directory) as mntdir:
+            tb = download_image(tarball_url, self.context, decompress=False)
+            extract_targz(tb, '%s/%s' % (mntdir, directory))
+
+    def power_off(self, proc):
+        super(SDMuxTarget, self).power_off(proc)
+        logging_system(self.config.power_off_cmd)
+
+    def power_on(self):
+        self.proc = connect_to_serial(self.config, self.sio)
+
+        logging.info('powering on')
+        logging_system(self.config.power_on_cmd)
+
+        return self.proc
+
+    def get_device_version(self):
+        return self.config.sdmux_version
+
+target_class = SDMuxTarget

=== added file 'lava_dispatcher/device/sdmux.sh'
--- lava_dispatcher/device/sdmux.sh	1970-01-01 00:00:00 +0000
+++ lava_dispatcher/device/sdmux.sh	2013-01-04 00:03:43 +0000
@@ -0,0 +1,77 @@ 
+#!/bin/bash
+# based on  https://github.com/liyan/suspend-usb-device
+
+#set -e
+
+usage()
+{
+    cat<<EOF
+This script will turn on/off power to a USB port. Its being
+used in conjunction with the SD Mux device.
+
+Power on/off a device or find its /dev/sdX with:
+ $0 -d device_id on|off|deventry
+
+Find the device ID from a /dev/entry with
+$0 -f /dev/sdX
+
+EOF
+}
+
+while getopts "f:d:" opt; do
+	case $opt in
+		f)  DEV=$OPTARG ;;
+		d)  ID=$OPTARG ;;
+		\?) usage ; exit 1 ;;
+	esac
+done
+
+if [ -n "$DEV" ] ; then
+	echo "Finding id for $DEV"
+	DEVICE=$(udevadm info --query=path --name=${DEV} --attribute-walk | \
+	egrep "looking at parent device" | head -1 | \
+	sed -e "s/.*looking at parent device '\(\/devices\/.*\)\/.*\/host.*/\1/g")
+
+	if [ -z $DEVICE ]; then
+	    1>&2 echo "cannot find appropriate parent USB device, "
+	    1>&2 echo "perhaps ${DEV} is not an USB device?"
+	    exit 1
+	fi
+
+	# the trailing basename of ${DEVICE} is DEV_BUS_ID
+	DEV_BUS_ID=${DEVICE##*/}
+	echo Device: ${DEVICE}
+	echo Bus ID: ${DEV_BUS_ID}
+
+elif [ -n "$ID" ] ; then
+	ACTION=${!OPTIND:-}
+	DIR=/sys/bus/usb/devices/${ID}/${ID}*/host*/target*/*:0:0:0/block
+	if [ $ACTION == "on" ] ; then
+		if [ -d $DIR ] ; then
+			echo "<sdmux script> already on" 1>&2
+		else
+			echo -n "${ID}" > /sys/bus/usb/drivers/usb/bind
+			sleep 2
+		fi
+		device_path=`ls $DIR 2>/dev/null`
+		if [ $? -ne 0 ] ; then
+			echo "<sdmux script> No sdmux found at ${DIR}" 1>&2
+			exit 1
+		fi
+		echo /dev/${device_path}
+
+	elif [ $ACTION = "off" ] ; then
+		echo "<sdmux script> Powering off sdmux: $ID"
+		echo -n "${ID}" > /sys/bus/usb/drivers/usb/unbind
+		echo -n '0' > /sys/bus/usb/devices/$ID/power/autosuspend_delay_ms
+		echo -n 'auto' > /sys/bus/usb/devices/$ID/power/control
+		sleep 2
+	elif [ $ACTION = "deventry" ] ; then
+		echo /dev/`ls $DIR`
+	else
+		echo "ERROR: Action must be on/off"
+		usage; exit 1
+	fi
+else
+	usage
+fi

=== modified file 'lava_dispatcher/utils.py'
--- lava_dispatcher/utils.py	2012-12-19 05:46:44 +0000
+++ lava_dispatcher/utils.py	2013-01-04 00:03:43 +0000
@@ -29,6 +29,7 @@ 
 import time
 import urlparse
 import subprocess
+
 from shlex import shlex
 
 import pexpect
@@ -57,9 +58,11 @@ 
         os.makedirs(dir)
     shutil.copy(src, dest)
 
+
 def rmtree(directory):
     subprocess.call(['rm', '-rf', directory])
 
+
 def mkdtemp(basedir='/tmp'):
     """ returns a temporary directory that's deleted when the process exits
     """
@@ -211,6 +214,55 @@ 
                 timeout=1, lava_no_logging=1)
 
 
+def connect_to_serial(device_config, sio):
+    """
+    Attempts to connect to a serial console server like conmux or cyclades
+    """
+    retry_count = 0
+    retry_limit = 3
+
+    port_stuck_message = 'Data Buffering Suspended\.'
+    conn_closed_message = 'Connection closed by foreign host\.'
+
+    expectations = {
+        port_stuck_message: 'reset-port',
+        'Connected\.\r': 'all-good',
+        conn_closed_message: 'retry',
+        pexpect.TIMEOUT: 'all-good',
+    }
+    patterns = []
+    results = []
+    for pattern, result in expectations.items():
+        patterns.append(pattern)
+        results.append(result)
+
+    while retry_count < retry_limit:
+        proc = logging_spawn(device_config.connection_command, timeout=1200)
+        proc.logfile_read = sio
+        logging.info('Attempting to connect to device')
+        match = proc.expect(patterns, timeout=10)
+        result = results[match]
+        logging.info('Matched %r which means %s', patterns[match], result)
+        if result == 'retry':
+            proc.close(True)
+            retry_count += 1
+            time.sleep(5)
+            continue
+        elif result == 'all-good':
+            atexit.register(proc.close, True)
+            return proc
+        elif result == 'reset-port':
+            reset_cmd = device_config.reset_port_command
+            if reset_cmd:
+                logging_system(reset_cmd)
+            else:
+                raise CriticalError('no reset_port command configured')
+            proc.close(True)
+            retry_count += 1
+            time.sleep(5)
+    raise CriticalError('could execute connection_command successfully')
+
+
 # XXX Duplication: we should reuse lava-test TestArtifacts
 def generate_bundle_file_name(test_name):
     return ("{test_id}.{time.tm_year:04}-{time.tm_mon:02}-{time.tm_mday:02}T"

=== modified file 'setup.py'
--- setup.py	2012-12-16 22:20:53 +0000
+++ setup.py	2012-12-29 22:35:35 +0000
@@ -31,6 +31,7 @@ 
             'default-config/lava-dispatcher/device-defaults.conf',
             'default-config/lava-dispatcher/device-types/*.conf',
             'default-config/lava-dispatcher/devices/*.conf',
+            'device/sdmux.sh',
             ],
         },
     data_files=[