A deployment strategy using USB Gadget
by Ros Dos Santos, Alfonso (CT RDA DS EVO OPS DIA SE 1)
Hi everyone,
in the last few months my team and I have being using LAVA in our
Continuous Integration process for the development of a Yocto Linux
image for some development boards. These boards were not compatible with
the available strategies in LAVA so we had to improvise a little.
These boards are however capable of booting from a USB device. Our idea
was then to create a new deployment strategy that will download the
image file into some Linux device with a OTG USB port and "expose" it
using the g_mass_storage kernel module. The OTG USB port will be
connected to the test development board USB. For the booting strategy we
use the already existing minimal boot where we simply power up the
device and let it boot from the USB.
We would like to know your thoughts about this idea and if you see any
value in these changes for a possible contribution.
In the board's device configuration we add the host to where download
and mount the image
actions:
deploy:
methods:
usbgadget:
usb_gadget_host: {{ usb_gadget_host }}
We developed these changes on top of the lava-dispatcher verision
2017.7-1~bpo9+1 from the strerch-backports repository.
Here is our patch with the changes:
We also added some options to apply a patch to the image boot partition
to make it usb bootable, but if the image is already usb bootable it is
not needed.
---
.../pipeline/actions/deploy/strategies.py | 1 +
.../pipeline/actions/deploy/usbgadget.py | 254
+++++++++++++++++++++
2 files changed, 255 insertions(+)
create mode 100644 lava_dispatcher/pipeline/actions/deploy/usbgadget.py
diff --git a/lava_dispatcher/pipeline/actions/deploy/strategies.py
b/lava_dispatcher/pipeline/actions/deploy/strategies.py
index da1e155..cfd6438 100644
--- a/lava_dispatcher/pipeline/actions/deploy/strategies.py
+++ b/lava_dispatcher/pipeline/actions/deploy/strategies.py
@@ -32,3 +32,4 @@ from lava_dispatcher.pipeline.actions.deploy.lxc
import Lxc
from lava_dispatcher.pipeline.actions.deploy.iso import DeployIso
from lava_dispatcher.pipeline.actions.deploy.nfs import Nfs
from lava_dispatcher.pipeline.actions.deploy.vemsd import VExpressMsd
+from lava_dispatcher.pipeline.actions.deploy.usbgadget import
USBGadgetDeployment
diff --git a/lava_dispatcher/pipeline/actions/deploy/usbgadget.py
b/lava_dispatcher/pipeline/actions/deploy/usbgadget.py
new file mode 100644
index 0000000..65347f4
--- /dev/null
+++ b/lava_dispatcher/pipeline/actions/deploy/usbgadget.py
@@ -0,0 +1,254 @@
+import os
+import patch
+import guestfs
+import errno
+import gzip
+
+from paramiko import SSHClient, AutoAddPolicy
+from scp import SCPClient
+from tempfile import mkdtemp
+from shutil import rmtree
+
+from lava_dispatcher.pipeline.action import Pipeline,
InfrastructureError, Action
+from lava_dispatcher.pipeline.logical import Deployment
+from lava_dispatcher.pipeline.actions.deploy import DeployAction
+
+from lava_dispatcher.pipeline.actions.deploy.download import (
+ DownloaderAction,
+)
+
+from lava_dispatcher.pipeline.actions.deploy.overlay import (
+ OverlayAction,
+)
+
+from lava_dispatcher.pipeline.actions.deploy.apply_overlay import (
+ ApplyOverlayImage,
+)
+
+
+class PatchFileAction(Action):
+
+ def __init__(self):
+ super(PatchFileAction, self).__init__()
+ self.name = "patch-image-file"
+ self.summary = "patch-image-file"
+ self.description = "patch-image-file"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(PatchFileAction, self).run(
+ connection, max_end_time, args)
+
+ decompressed_image = self.get_namespace_data(
+ action='download-action', label='image', key='file')
+ self.logger.debug("Image: %s", decompressed_image)
+
+ partition = self.parameters['patch'].get('partition')
+ if partition is None:
+ raise JobError(
+ "Unable to apply the patch, image without 'partition'")
+
+ patchfile = self.get_namespace_data(
+ action='download-action', label='file', key='patch')
+
+ destination = self.parameters['patch'].get('dst')
+
+ self.patch_file(decompressed_image, partition, destination,
patchfile)
+ return connection
+
+ @staticmethod
+ def patch_file(image, partition, destination, patchfile):
+ """
+ Reads the destination file from the image, patchs it and writes
it back
+ to the image.
+ """
+ guest = guestfs.GuestFS(python_return_dict=True)
+ guest.add_drive(image)
+ guest.launch()
+ partitions = guest.list_partitions()
+ if not partitions:
+ raise InfrastructureError("Unable to prepare guestfs")
+ guest_partition = partitions[partition]
+ guest.mount(guest_partition, '/')
+
+ # create mount point
+ tmpd = mkdtemp()
+
+ # read the file to be patched
+ f_to_patch = guest.read_file(destination)
+
+ if destination.startswith('/'):
+ # copy the file locally
+ copy_dst = os.path.join(tmpd, destination[1:])
+ else:
+ copy_dst = os.path.join(tmpd, destination)
+
+ try:
+ os.makedirs(os.path.dirname(copy_dst))
+ except OSError as exc:
+ if exc.errno == errno.EEXIST:
+ pass
+ else:
+ raise
+
+ with open(copy_dst, 'w') as dst:
+ dst.write(f_to_patch)
+
+ # read the patch
+ ptch = patch.fromfile(patchfile)
+
+ # apply the patch
+ ptch.apply(root=tmpd)
+
+ # write the patched file back to the image
+ with open(copy_dst, 'r') as copy:
+ guest.write(destination, copy.read())
+
+ guest.umount(guest_partition)
+
+ # remove the mount point
+ rmtree(tmpd)
+
+
+class USBGadgetScriptAction(Action):
+
+ def __init__(self, host):
+ super(USBGadgetScriptAction, self).__init__()
+ self.name = "deploy-usb-gadget"
+ self.summary = "deploy-usb-gadget"
+ self.description = "deploy-usb-gadget"
+ self.host = host
+
+ def validate(self):
+ if 'deployment_data' in self.parameters:
+ lava_test_results_dir = self.parameters[
+ 'deployment_data']['lava_test_results_dir']
+ lava_test_results_dir = lava_test_results_dir % self.job.job_id
+ self.set_namespace_data(action='test', label='results',
+ key='lava_test_results_dir',
value=lava_test_results_dir)
+
+ def print_transfer_progress(self, filename, size, sent):
+ current_progress = (100 * sent) / size
+ if current_progress >= self.transfer_progress + 5:
+ self.transfer_progress = current_progress
+ self.logger.debug(
+ "Transferring file %s. Progress %d%%", filename,
current_progress)
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(USBGadgetScriptAction, self).run(
+ connection, max_end_time, args)
+
+ # # Compressing the image file
+ uncompressed_image = self.get_namespace_data(
+ action='download-action', label='file', key='image')
+ self.logger.debug("Compressing the image %s", uncompressed_image)
+ compressed_image = uncompressed_image + '.gz'
+ with open(uncompressed_image) as f_in,
gzip.open(compressed_image, 'wb') as f_out:
+ f_out.writelines(f_in)
+
+ # # Try to connect to the usb gadget host
+ ssh = SSHClient()
+ ssh.set_missing_host_key_policy(AutoAddPolicy())
+ ssh.connect(hostname=self.host, username='root', password='')
+ dest_file = os.path.join('/mnt/',
os.path.basename(compressed_image))
+
+ # # Clear /mnt folder
+ self.logger.debug("Clearing /mnt directory")
+ stdin, stdout, stderr = ssh.exec_command('rm -rf /mnt/*')
+ exit_code = stdout.channel.recv_exit_status()
+ if exit_code == 0:
+ self.logger.debug("/mnt clear")
+ else:
+ self.logger.error("Could not clear /mnt on secondary device")
+
+ # # Transfer the compressed image file
+ self.logger.debug(
+ "Transferring file %s to the usb gadget host",
compressed_image)
+ self.transfer_progress = 0
+
+ scp = SCPClient(ssh.get_transport(),
+ progress=self.print_transfer_progress,
+ socket_timeout=600.0)
+ scp.put(compressed_image, dest_file)
+ scp.close()
+
+ # # Decompress the sent image
+ self.logger.debug("Decompressing the file %s", dest_file)
+ stdin, stdout, stderr = ssh.exec_command('gzip -d %s' %
(dest_file))
+ exit_code = stdout.channel.recv_exit_status()
+ if exit_code == 0:
+ self.logger.debug("Decompressed file")
+ else:
+ self.logger.error("Could not decompress file: %s",
+ stderr.readlines())
+
+ # # Run the g_mass_storage module
+ dest_file_uncompressed = dest_file[:-3]
+ self.logger.debug(
+ "Exposing the image %s as a usb storage",
dest_file_uncompressed)
+
+ stdin, stdout, stderr = ssh.exec_command('rmmod g_mass_storage')
+ exit_code = stdout.channel.recv_exit_status()
+
+ stdin, stdout, stderr = ssh.exec_command(
+ 'modprobe g_mass_storage file=%s' % (dest_file_uncompressed))
+ exit_code = stdout.channel.recv_exit_status()
+ if exit_code == 0:
+ self.logger.debug("Mounted mass storage file")
+ else:
+ self.logger.error("Could not mount file: %s",
+ stderr.readlines())
+
+ ssh.close()
+ return connection
+
+
+class USBGadgetDeploymentAction(DeployAction):
+
+ def __init__(self):
+ super(USBGadgetDeploymentAction, self).__init__()
+ self.name = 'usb-gadget-deploy'
+ self.description = "deploy images using the fake usb device"
+ self.summary = "deploy images"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(
+ parent=self, job=self.job, parameters=parameters)
+ path = self.mkdtemp()
+
+ # Download the image
+ self.internal_pipeline.add_action(DownloaderAction('image', path))
+
+ if self.test_needs_overlay(parameters):
+ self.internal_pipeline.add_action(OverlayAction())
+ self.internal_pipeline.add_action(ApplyOverlayImage())
+
+ # Patch it if needed
+ if 'patch' in parameters:
+ self.internal_pipeline.add_action(DownloaderAction('patch', path))
+ self.internal_pipeline.add_action(PatchFileAction())
+
+ host = self.job.device['actions']['deploy'][
+ 'methods']['usbgadget']['usb_gadget_host']
+ self.internal_pipeline.add_action(USBGadgetScriptAction(host))
+
+
+class USBGadgetDeployment(Deployment):
+ """
+ Only for iot2000-usb
+ """
+ compatibility = 4
+
+ def __init__(self, parent, parameters):
+ super(USBGadgetDeployment, self).__init__(parent)
+ self.priority = 1
+ self.action = USBGadgetDeploymentAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ """
+ Accept only iot2000-usb jobs
+ """
+ return device['device_type'] == 'iot2000-usb'
--
2.7.4