=== modified file 'doc/index.rst'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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'
@@ -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=[