aboutsummaryrefslogtreecommitdiff
path: root/lava_dispatcher/actions/boot
diff options
context:
space:
mode:
authorRĂ©mi Duraffort <remi.duraffort@linaro.org>2017-09-12 14:46:34 +0200
committerNeil Williams <neil.williams@linaro.org>2017-10-25 10:37:31 +0000
commitddf5d2fc956f3b261635cb9487a6bdf6f6cca1c7 (patch)
tree6c13da8c8f8b0946d2e493b3773efafbdd4e11e6 /lava_dispatcher/actions/boot
parent7dbdc2b65b039027540b77e986f62910d5620bc6 (diff)
Remove v1 code
Change-Id: I098edd9edfeb968b303eaedc6afb6654e43b4b98
Diffstat (limited to 'lava_dispatcher/actions/boot')
-rw-r--r--lava_dispatcher/actions/boot/__init__.py615
-rw-r--r--lava_dispatcher/actions/boot/cmsis_dap.py144
-rw-r--r--lava_dispatcher/actions/boot/dfu.py159
-rw-r--r--lava_dispatcher/actions/boot/docker.py140
-rw-r--r--lava_dispatcher/actions/boot/environment.py69
-rw-r--r--lava_dispatcher/actions/boot/fastboot.py356
-rw-r--r--lava_dispatcher/actions/boot/grub.py313
-rw-r--r--lava_dispatcher/actions/boot/ipxe.py165
-rw-r--r--lava_dispatcher/actions/boot/iso.py212
-rw-r--r--lava_dispatcher/actions/boot/kexec.py122
-rw-r--r--lava_dispatcher/actions/boot/lxc.py154
-rw-r--r--lava_dispatcher/actions/boot/minimal.py81
-rw-r--r--lava_dispatcher/actions/boot/pyocd.py133
-rw-r--r--lava_dispatcher/actions/boot/qemu.py254
-rw-r--r--lava_dispatcher/actions/boot/ssh.py292
-rw-r--r--lava_dispatcher/actions/boot/strategies.py42
-rw-r--r--lava_dispatcher/actions/boot/u_boot.py272
-rw-r--r--lava_dispatcher/actions/boot/uefi.py195
-rw-r--r--lava_dispatcher/actions/boot/uefi_menu.py278
19 files changed, 3996 insertions, 0 deletions
diff --git a/lava_dispatcher/actions/boot/__init__.py b/lava_dispatcher/actions/boot/__init__.py
new file mode 100644
index 000000000..5263c4f73
--- /dev/null
+++ b/lava_dispatcher/actions/boot/__init__.py
@@ -0,0 +1,615 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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 os
+import re
+import shutil
+from lava_dispatcher.action import (
+ Action,
+ Pipeline,
+ JobError,
+ ConfigurationError,
+ Timeout,
+ LAVABug)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.logical import RetryAction
+from lava_dispatcher.utils.constants import (
+ DISPATCHER_DOWNLOAD_DIR,
+ DISTINCTIVE_PROMPT_CHARACTERS,
+ LINE_SEPARATOR,
+ BOOTLOADER_DEFAULT_CMD_TIMEOUT,
+ LOGIN_INCORRECT_MSG,
+ LOGIN_TIMED_OUT_MSG
+)
+from lava_dispatcher.utils.messages import LinuxKernelMessages
+from lava_dispatcher.utils.strings import substitute
+from lava_dispatcher.utils.network import dispatcher_ip
+from lava_dispatcher.utils.filesystem import write_bootscript
+from lava_dispatcher.connections.ssh import SShSession
+from lava_dispatcher.connections.serial import ConnectShell
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import ExpectShellSession
+
+# pylint: disable=too-many-locals,too-many-instance-attributes,superfluous-parens
+# pylint: disable=too-many-branches,too-many-statements
+
+
+class BootAction(RetryAction):
+ """
+ Base class for all actions which control power-on
+ and boot behaviour of a device under test.
+ The subclass selected to do the work will be the
+ subclass returning True in the accepts(device_type, image)
+ function.
+ Each new subclass needs a unit test to ensure it is
+ reliably selected for the correct job and not
+ selected for an invalid job or a job
+ accepted by a different subclass.
+
+ Boot and Test are closely related - a fail error in Boot
+ will cause subsequent Test actions to be skipped.
+ """
+
+ name = 'boot'
+
+ def has_prompts(self, parameters): # pylint: disable=no-self-use
+ return ('prompts' in parameters)
+
+ def has_boot_finished(self, parameters): # pylint: disable=no-self-use
+ return ('boot_finished' in parameters)
+
+
+class SecondaryShell(Boot):
+ """
+ SecondaryShell method can be used by a variety of other boot methods to
+ read from the kernel console independently of the shell interaction
+ required to interact with the bootloader and test shell.
+ It is also the updated way to connect to the primary console.
+ """
+
+ compatibility = 6
+
+ def __init__(self, parent, parameters):
+ super(SecondaryShell, self).__init__(parent)
+ self.action = SecondaryShellAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' not in parameters:
+ raise ConfigurationError("method not specified in boot parameters")
+ if parameters['method'] != 'new_connection':
+ return False, 'new_connection not in method'
+ if 'actions' not in device:
+ raise ConfigurationError("Invalid device configuration")
+ if 'boot' not in device['actions']:
+ return False, 'boot not in device actions'
+ if 'methods' not in device['actions']['boot']:
+ raise ConfigurationError("Device misconfiguration")
+ if 'method' not in parameters:
+ return False, 'no boot method'
+ return True, 'accepted'
+
+
+class SecondaryShellAction(BootAction):
+
+ def __init__(self):
+ super(SecondaryShellAction, self).__init__()
+ self.name = "secondary-shell-action"
+ self.description = "Connect to a secondary shell on specified hardware"
+ self.summary = "connect to a specified second shell"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ name = parameters['connection']
+ self.internal_pipeline.add_action(ConnectShell(name=name))
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+# FIXME: move to it's own file
+class AutoLoginAction(Action):
+ """
+ Automatically login on the device.
+ If 'auto_login' is not present in the parameters, this action does nothing.
+
+ This Action expect POSIX-compatible support of PS1 from shell
+ """
+ def __init__(self):
+ super(AutoLoginAction, self).__init__()
+ self.name = 'auto-login-action'
+ self.description = "automatically login after boot using job parameters and checking for messages."
+ self.summary = "Auto-login after boot with support for kernel messages."
+ self.check_prompt_characters_warning = (
+ "The string '%s' does not look like a typical prompt and"
+ " could match status messages instead. Please check the"
+ " job log files and use a prompt string which matches the"
+ " actual prompt string more closely."
+ )
+ self.force_prompt = False
+
+ def validate(self): # pylint: disable=too-many-branches
+ super(AutoLoginAction, self).validate()
+ # Skip auto login if the configuration is not found
+ params = self.parameters.get('auto_login', None)
+ if params:
+ if not isinstance(params, dict):
+ self.errors = "'auto_login' should be a dictionary"
+ return
+
+ if 'login_prompt' not in params:
+ self.errors = "'login_prompt' is mandatory for auto_login"
+ elif not params['login_prompt']:
+ self.errors = "Value for 'login_prompt' cannot be empty"
+
+ if 'username' not in params:
+ self.errors = "'username' is mandatory for auto_login"
+
+ if 'password_prompt' in params:
+ if 'password' not in params:
+ self.errors = "'password' is mandatory if 'password_prompt' is used in auto_login"
+
+ if 'login_commands' in params:
+ login_commands = params['login_commands']
+ if not isinstance(login_commands, list):
+ self.errors = "'login_commands' must be a list"
+ if not login_commands:
+ self.errors = "'login_commands' must not be empty"
+
+ prompts = self.parameters.get('prompts', None)
+ if prompts is None:
+ self.errors = "'prompts' is mandatory for AutoLoginAction"
+
+ if not isinstance(prompts, (list, str)):
+ self.errors = "'prompts' should be a list or a str"
+
+ if not prompts:
+ self.errors = "Value for 'prompts' cannot be empty"
+
+ if isinstance(prompts, list):
+ for prompt in prompts:
+ if not prompt:
+ self.errors = "Items of 'prompts' can't be empty"
+
+ def check_kernel_messages(self, connection, max_end_time):
+ """
+ Use the additional pexpect expressions to detect warnings
+ and errors during the kernel boot. Ensure all test jobs using
+ auto-login-action have a result set so that the duration is
+ always available when the action completes successfully.
+ """
+ if isinstance(connection, SShSession):
+ self.logger.debug("Skipping kernel messages")
+ return
+ self.logger.info("Parsing kernel messages")
+ self.logger.debug(connection.prompt_str)
+ parsed = LinuxKernelMessages.parse_failures(connection, self, max_end_time=max_end_time)
+ if len(parsed) and 'success' in parsed[0]:
+ self.results = {'success': parsed[0]['success']}
+ elif not parsed:
+ self.results = {'success': "No kernel warnings or errors detected."}
+ else:
+ self.results = {'fail': parsed}
+ self.logger.warning("Kernel warnings or errors detected.")
+
+ def run(self, connection, max_end_time, args=None):
+ # Prompts commonly include # - when logging such strings,
+ # use lazy logging or the string will not be quoted correctly.
+ def check_prompt_characters(chk_prompt):
+ if not any([True for c in DISTINCTIVE_PROMPT_CHARACTERS if c in chk_prompt]):
+ self.logger.warning(self.check_prompt_characters_warning, chk_prompt)
+
+ connection = super(AutoLoginAction, self).run(connection, max_end_time, args)
+ if not connection:
+ return connection
+ prompts = self.parameters.get('prompts', None)
+ for prompt in prompts:
+ check_prompt_characters(prompt)
+
+ connection.prompt_str = LinuxKernelMessages.get_init_prompts()
+ connection.prompt_str.extend(prompts)
+
+ # linesep should come from deployment_data as from now on it is OS dependent
+ linesep = self.get_namespace_data(
+ action='deploy-device-env',
+ label='environment',
+ key='line_separator'
+ )
+ connection.raw_connection.linesep = linesep if linesep else LINE_SEPARATOR
+ self.logger.debug("Using line separator: #%r#", connection.raw_connection.linesep)
+
+ # Skip auto login if the configuration is not found
+ params = self.parameters.get('auto_login', None)
+ if not params:
+ self.logger.debug("No login prompt set.")
+ self.force_prompt = True
+ # If auto_login is not enabled, login will time out if login
+ # details are requested.
+ connection.prompt_str.append(LOGIN_TIMED_OUT_MSG)
+ connection.prompt_str.append(LOGIN_INCORRECT_MSG)
+ # wait for a prompt or kernel messages
+ self.check_kernel_messages(connection, max_end_time)
+ if 'success' in self.results:
+ check = self.results['success']
+ if LOGIN_TIMED_OUT_MSG in check or LOGIN_INCORRECT_MSG in check:
+ raise JobError("auto_login not enabled but image requested login details.")
+ # clear kernel message prompt patterns
+ connection.prompt_str = list(self.parameters.get('prompts', []))
+ # already matched one of the prompts
+ else:
+ self.logger.info("Waiting for the login prompt")
+ connection.prompt_str.append(params['login_prompt'])
+ connection.prompt_str.append(LOGIN_INCORRECT_MSG)
+
+ # wait for a prompt or kernel messages
+ self.check_kernel_messages(connection, max_end_time)
+ if 'success' in self.results:
+ if LOGIN_INCORRECT_MSG in self.results['success']:
+ self.logger.warning("Login incorrect message matched before the login prompt. "
+ "Please check that the login prompt is correct. Retrying login...")
+ self.logger.debug("Sending username %s", params['username'])
+ connection.sendline(params['username'], delay=self.character_delay)
+ # clear the kernel_messages patterns
+ connection.prompt_str = list(self.parameters.get('prompts', []))
+
+ if 'password_prompt' in params:
+ self.logger.info("Waiting for password prompt")
+ connection.prompt_str.append(params['password_prompt'])
+ # This can happen if password_prompt is misspelled.
+ connection.prompt_str.append(LOGIN_TIMED_OUT_MSG)
+
+ # wait for the password prompt
+ index = self.wait(connection, max_end_time)
+ if index:
+ self.logger.debug("Matched prompt #%s: %s", index, connection.prompt_str[index])
+ if connection.prompt_str[index] == LOGIN_TIMED_OUT_MSG:
+ raise JobError("Password prompt not matched, please update the job definition with the correct one.")
+ self.logger.debug("Sending password %s", params['password'])
+ connection.sendline(params['password'], delay=self.character_delay)
+ # clear the Password pattern
+ connection.prompt_str = list(self.parameters.get('prompts', []))
+
+ connection.prompt_str.append(LOGIN_INCORRECT_MSG)
+ connection.prompt_str.append(LOGIN_TIMED_OUT_MSG)
+ # wait for the login process to provide the prompt
+ index = self.wait(connection, max_end_time)
+ if index:
+ self.logger.debug("Matched %s %s", index, connection.prompt_str[index])
+ if connection.prompt_str[index] == LOGIN_INCORRECT_MSG:
+ self.errors = LOGIN_INCORRECT_MSG
+ raise JobError(LOGIN_INCORRECT_MSG)
+ if connection.prompt_str[index] == LOGIN_TIMED_OUT_MSG:
+ self.errors = LOGIN_TIMED_OUT_MSG
+ raise JobError(LOGIN_TIMED_OUT_MSG)
+
+ login_commands = params.get('login_commands', None)
+ if login_commands is not None:
+ self.logger.debug("Running login commands")
+ for command in login_commands:
+ connection.sendline(command)
+
+ connection.prompt_str.extend([self.job.device.get_constant(
+ 'default-shell-prompt')])
+ self.logger.debug("Setting shell prompt(s) to %s" % connection.prompt_str) # pylint: disable=logging-not-lazy
+ connection.sendline('export PS1="%s"' % self.job.device.get_constant(
+ 'default-shell-prompt'), delay=self.character_delay)
+
+ return connection
+
+
+class BootloaderCommandOverlay(Action):
+ """
+ Replace KERNEL_ADDR and DTB placeholders with the actual values for this
+ particular pipeline.
+ addresses are read from the device configuration parameters
+ bootloader_type is determined from the boot action method strategy
+ bootz or bootm is determined by boot action method type. (i.e. it is up to
+ the test writer to select the correct download file for the correct boot command.)
+ server_ip is calculated at runtime
+ filenames are determined from the download Action.
+ """
+ def __init__(self):
+ super(BootloaderCommandOverlay, self).__init__()
+ self.name = "bootloader-overlay"
+ self.summary = "replace placeholders with job data"
+ self.description = "substitute job data into bootloader command list"
+ self.commands = None
+ self.method = ""
+ self.use_bootscript = False
+ self.lava_mac = None
+ self.bootcommand = ''
+ self.ram_disk = None
+
+ def validate(self):
+ super(BootloaderCommandOverlay, self).validate()
+ self.method = self.parameters['method']
+ device_methods = self.job.device['actions']['boot']['methods']
+ if isinstance(self.parameters['commands'], list):
+ self.commands = self.parameters['commands']
+ self.logger.warning("WARNING: Using boot commands supplied in the job definition, NOT the LAVA device configuration")
+ else:
+ if self.method not in self.job.device['actions']['boot']['methods']:
+ self.errors = "%s boot method not found" % self.method
+ if 'method' not in self.parameters:
+ self.errors = "missing method"
+ elif 'commands' not in self.parameters:
+ self.errors = "missing commands"
+ elif self.parameters['commands'] not in device_methods[self.parameters['method']]:
+ self.errors = "Command not found in supported methods"
+ elif 'commands' not in device_methods[self.parameters['method']][self.parameters['commands']]:
+ self.errors = "No commands found in parameters"
+ self.commands = device_methods[self.parameters['method']][self.parameters['commands']]['commands']
+ # download-action will set ['dtb'] as tftp_path, tmpdir & filename later, in the run step.
+ if 'use_bootscript' in self.parameters:
+ self.use_bootscript = self.parameters['use_bootscript']
+ if 'lava_mac' in self.parameters:
+ if re.match("([0-9A-F]{2}[:-]){5}([0-9A-F]{2})", self.parameters['lava_mac'], re.IGNORECASE):
+ self.lava_mac = self.parameters['lava_mac']
+ else:
+ self.errors = "lava_mac is not a valid mac address"
+
+ def run(self, connection, max_end_time, args=None):
+ """
+ Read data from the download action and replace in context
+ Use common data for all values passed into the substitutions so that
+ multiple actions can use the same code.
+ """
+ # Multiple deployments would overwrite the value if parsed in the validate step.
+ # FIXME: implement isolation for repeated steps.
+ connection = super(BootloaderCommandOverlay, self).run(connection, max_end_time, args)
+ ip_addr = dispatcher_ip(self.job.parameters['dispatcher'])
+
+ self.ram_disk = self.get_namespace_data(action='compress-ramdisk', label='file', key='ramdisk')
+ # most jobs substitute RAMDISK, so also use this for the initrd
+ if self.get_namespace_data(action='nbd-deploy', label='nbd', key='initrd'):
+ self.ram_disk = self.get_namespace_data(action='download-action', label='file', key='initrd')
+
+ substitutions = {
+ '{SERVER_IP}': ip_addr,
+ '{PRESEED_CONFIG}': self.get_namespace_data(action='download-action', label='file', key='preseed'),
+ '{PRESEED_LOCAL}': self.get_namespace_data(action='compress-ramdisk', label='file', key='preseed_local'),
+ '{DTB}': self.get_namespace_data(action='download-action', label='file', key='dtb'),
+ '{RAMDISK}': self.ram_disk,
+ '{INITRD}': self.ram_disk,
+ '{KERNEL}': self.get_namespace_data(action='download-action', label='file', key='kernel'),
+ '{LAVA_MAC}': self.lava_mac
+ }
+ self.bootcommand = self.get_namespace_data(action='uboot-prepare-kernel', label='bootcommand', key='bootcommand')
+ if not self.bootcommand:
+ if 'type' in self.parameters:
+ self.logger.warning("Using type from the boot action as the boot-command. "
+ "Declaring a kernel type in the deploy is preferred.")
+ self.bootcommand = self.parameters['type']
+ prepared_kernel = self.get_namespace_data(action='prepare-kernel', label='file', key='kernel')
+ if prepared_kernel:
+ self.logger.info("Using kernel file from prepare-kernel: %s", prepared_kernel)
+ substitutions['{KERNEL}'] = prepared_kernel
+ if self.bootcommand:
+ self.logger.debug("%s" % self.job.device['parameters'])
+ kernel_addr = self.job.device['parameters'][self.bootcommand]['kernel']
+ dtb_addr = self.job.device['parameters'][self.bootcommand]['dtb']
+ ramdisk_addr = self.job.device['parameters'][self.bootcommand]['ramdisk']
+
+ if not self.get_namespace_data(action='tftp-deploy', label='tftp', key='ramdisk') \
+ and not self.get_namespace_data(action='download-action', label='file', key='ramdisk') \
+ and not self.get_namespace_data(action='download-action', label='file', key='initrd'):
+ ramdisk_addr = '-'
+ add_header = self.job.device['actions']['deploy']['parameters'].get('add_header', None)
+ if self.method == 'u-boot' and not add_header == "u-boot":
+ self.logger.debug("No u-boot header, not passing ramdisk to bootX cmd")
+ ramdisk_addr = '-'
+
+ if self.get_namespace_data(action='download-action', label='file', key='initrd'):
+ # no u-boot header, thus no embedded size, so we have to add it to the
+ # boot cmd with colon after the ramdisk
+ substitutions['{BOOTX}'] = "%s %s %s:%s %s" % (
+ self.bootcommand, kernel_addr, ramdisk_addr, '${initrd_size}', dtb_addr)
+ else:
+ substitutions['{BOOTX}'] = "%s %s %s %s" % (
+ self.bootcommand, kernel_addr, ramdisk_addr, dtb_addr)
+
+ substitutions['{KERNEL_ADDR}'] = kernel_addr
+ substitutions['{DTB_ADDR}'] = dtb_addr
+ substitutions['{RAMDISK_ADDR}'] = ramdisk_addr
+ self.results = {
+ 'kernel_addr': kernel_addr,
+ 'dtb_addr': dtb_addr,
+ 'ramdisk_addr': ramdisk_addr
+ }
+
+ nfs_address = self.get_namespace_data(action='persistent-nfs-overlay', label='nfs_address', key='nfsroot')
+ nfs_root = self.get_namespace_data(action='download-action', label='file', key='nfsrootfs')
+ if nfs_root:
+ substitutions['{NFSROOTFS}'] = self.get_namespace_data(action='extract-rootfs', label='file', key='nfsroot')
+ substitutions['{NFS_SERVER_IP}'] = ip_addr
+ elif nfs_address:
+ substitutions['{NFSROOTFS}'] = nfs_address
+ substitutions['{NFS_SERVER_IP}'] = self.get_namespace_data(
+ action='persistent-nfs-overlay', label='nfs_address', key='serverip')
+
+ nbd_root = self.get_namespace_data(action='download-action', label='file', key='nbdroot')
+ if nbd_root:
+ substitutions['{NBDSERVERIP}'] = str(self.get_namespace_data(action='nbd-deploy', label='nbd', key='nbd_server_ip'))
+ substitutions['{NBDSERVERPORT}'] = str(self.get_namespace_data(action='nbd-deploy', label='nbd', key='nbd_server_port'))
+
+ substitutions['{ROOT}'] = self.get_namespace_data(action='bootloader-from-media', label='uuid', key='root') # UUID label, not a file
+ substitutions['{ROOT_PART}'] = self.get_namespace_data(action='bootloader-from-media', label='uuid', key='boot_part')
+ if self.use_bootscript:
+ script = "/script.ipxe"
+ bootscript = self.get_namespace_data(action='tftp-deploy', label='tftp', key='tftp_dir') + script
+ bootscripturi = "tftp://%s/%s" % (ip_addr, os.path.dirname(substitutions['{KERNEL}']) + script)
+ write_bootscript(substitute(self.commands, substitutions), bootscript)
+ bootscript_commands = ['dhcp net0', "chain %s" % bootscripturi]
+ self.set_namespace_data(action=self.name, label=self.method, key='commands', value=bootscript_commands)
+ self.logger.info("Parsed boot commands: %s", '; '.join(bootscript_commands))
+ return connection
+ subs = substitute(self.commands, substitutions)
+ self.set_namespace_data(action='bootloader-overlay', label=self.method, key='commands', value=subs)
+ self.logger.info("Parsed boot commands: %s", '; '.join(subs))
+ return connection
+
+
+class BootloaderSecondaryMedia(Action):
+ """
+ Generic class for secondary media substitutions
+ """
+ def __init__(self):
+ super(BootloaderSecondaryMedia, self).__init__()
+ self.name = "bootloader-from-media"
+ self.summary = "set bootloader strings for deployed media"
+ self.description = "let bootloader know where to find the kernel in the image on secondary media"
+
+ def validate(self):
+ super(BootloaderSecondaryMedia, self).validate()
+ if 'media' not in self.job.device.get('parameters', []):
+ return
+ media_keys = self.job.device['parameters']['media'].keys()
+ if self.parameters['commands'] not in media_keys:
+ return
+ if 'kernel' not in self.parameters:
+ self.errors = "Missing kernel location"
+ # ramdisk does not have to be specified, nor dtb
+ if 'root_uuid' not in self.parameters:
+ # FIXME: root_node also needs to be supported
+ self.errors = "Missing UUID of the roofs inside the deployed image"
+ if 'boot_part' not in self.parameters:
+ self.errors = "Missing boot_part for the partition number of the boot files inside the deployed image"
+ self.set_namespace_data(action='download-action', label='file', key='kernel', value=self.parameters.get('kernel', ''))
+ self.set_namespace_data(action='compress-ramdisk', label='file', key='ramdisk', value=self.parameters.get('ramdisk', ''))
+ self.set_namespace_data(action='download-action', label='file', key='ramdisk', value=self.parameters.get('ramdisk', ''))
+ self.set_namespace_data(action='download-action', label='file', key='dtb', value=self.parameters.get('dtb', ''))
+ self.set_namespace_data(action='bootloader-from-media', label='uuid', key='root', value=self.parameters.get('root_uuid', ''))
+ self.set_namespace_data(action='bootloader-from-media', label='uuid', key='boot_part', value=str(self.parameters.get('boot_part')))
+
+
+class OverlayUnpack(Action):
+ """
+ Transfer the overlay.tar.gz to the device using test writer tools
+ Can be used with inline bootloader commands or where the rootfs is
+ not deployed directly by LAVA.
+ Whether the device has booted by tftp or ipxe or something else does
+ not matter for this action - the file will be downloaded from the
+ worker tmp dir using the default apache config.
+ """
+ def __init__(self):
+ super(OverlayUnpack, self).__init__()
+ self.name = 'overlay-unpack'
+ self.description = 'transfer and unpack overlay to persistent rootfs after login'
+ self.summary = 'transfer and unpack overlay'
+ self.url = None
+
+ def cleanup(self, connection):
+ super(OverlayUnpack, self).cleanup(connection)
+ if self.url:
+ os.unlink(self.url)
+
+ def validate(self):
+ super(OverlayUnpack, self).validate()
+ if 'transfer_overlay' not in self.parameters:
+ self.errors = "Unable to identify transfer commands for overlay."
+ return
+ if 'download_command' not in self.parameters['transfer_overlay']:
+ self.errors = "Unable to identify download command for overlay."
+ if 'unpack_command' not in self.parameters['transfer_overlay']:
+ self.errors = "Unable to identify unpack command for overlay."
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(OverlayUnpack, self).run(connection, max_end_time, args)
+ if not connection:
+ raise LAVABug("Cannot transfer overlay, no connection available.")
+ ip_addr = dispatcher_ip(self.job.parameters['dispatcher'])
+ overlay_file = self.get_namespace_data(action='compress-overlay', label='output', key='file')
+ if not overlay_file:
+ raise JobError("No overlay file identified for the transfer.")
+ overlay = os.path.basename(overlay_file).strip()
+ self.url = os.path.join(DISPATCHER_DOWNLOAD_DIR, overlay)
+ shutil.move(overlay_file, self.url)
+ self.logger.debug("Moved %s to %s", overlay_file, self.url)
+ dwnld = self.parameters['transfer_overlay']['download_command']
+ dwnld += " http://%s/tmp/%s" % (ip_addr, overlay)
+ unpack = self.parameters['transfer_overlay']['unpack_command']
+ unpack += ' ' + overlay
+ connection.sendline("rm %s; %s && %s" % (overlay, dwnld, unpack))
+ return connection
+
+
+class BootloaderCommandsAction(Action):
+ """
+ Send the boot commands to the bootloader
+ """
+ def __init__(self):
+ super(BootloaderCommandsAction, self).__init__()
+ self.name = "bootloader-commands"
+ self.description = "send commands to bootloader"
+ self.summary = "interactive bootloader"
+ self.params = None
+ self.timeout = Timeout(self.name, BOOTLOADER_DEFAULT_CMD_TIMEOUT)
+ self.method = ""
+
+ def validate(self):
+ super(BootloaderCommandsAction, self).validate()
+ self.method = self.parameters['method']
+ self.params = self.job.device['actions']['boot']['methods'][self.method]['parameters']
+
+ def line_separator(self):
+ return LINE_SEPARATOR
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ self.errors = "%s started without a connection already in use" % self.name
+ connection = super(BootloaderCommandsAction, self).run(connection, max_end_time, args)
+ connection.raw_connection.linesep = self.line_separator()
+ connection.prompt_str = self.params['bootloader_prompt']
+ self.logger.debug("Changing prompt to start interaction: %s", connection.prompt_str)
+ self.wait(connection)
+ i = 1
+ commands = self.get_namespace_data(action='bootloader-overlay', label=self.method, key='commands')
+
+ for line in commands:
+ connection.sendline(line, delay=self.character_delay)
+ if i != (len(commands)):
+ self.wait(connection)
+ i += 1
+
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ # allow for auto_login
+ if self.parameters.get('prompts', None):
+ connection.prompt_str = [
+ self.params.get('boot_message',
+ self.job.device.get_constant('boot-message')),
+ self.job.device.get_constant('cpu-reset-message')
+ ]
+ self.logger.debug("Changing prompt to boot_message %s",
+ connection.prompt_str)
+ index = self.wait(connection)
+ if connection.prompt_str[index] == self.job.device.get_constant('cpu-reset-message'):
+ self.logger.error("Bootloader reset detected: Bootloader "
+ "failed to load the required file into "
+ "memory correctly so the bootloader reset "
+ "the CPU.")
+ raise InfrastructureError("Bootloader reset detected")
+ return connection
diff --git a/lava_dispatcher/actions/boot/cmsis_dap.py b/lava_dispatcher/actions/boot/cmsis_dap.py
new file mode 100644
index 000000000..187fa4e66
--- /dev/null
+++ b/lava_dispatcher/actions/boot/cmsis_dap.py
@@ -0,0 +1,144 @@
+# Copyright (C) 2016 Linaro Limited
+#
+# Author: Tyler Baker <tyler.baker@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 shutil
+
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+ InfrastructureError
+)
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.utils.filesystem import mkdtemp
+from lava_dispatcher.utils.udev import WaitUSBSerialDeviceAction, WaitDevicePathAction
+
+
+class CMSIS(Boot):
+
+ compatibility = 4 # FIXME: change this to 5 and update test cases
+
+ def __init__(self, parent, parameters):
+ super(CMSIS, self).__init__(parent)
+ self.action = BootCMSIS()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'cmsis-dap' not in device['actions']['boot']['methods']:
+ return False, '"cmsis-dap" is not in the device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" not in parameters'
+ if parameters['method'] != 'cmsis-dap':
+ return False, '"method" was not "cmsis-dap"'
+ if 'board_id' not in device:
+ return False, 'device has no "board_id" configured'
+ if 'parameters' not in device['actions']['boot']['methods']['cmsis-dap']:
+ return False, '"parameters" was not in the device boot method configuration for "cmsis-dap"'
+ if 'usb_mass_device' not in device['actions']['boot']['methods']['cmsis-dap']['parameters']:
+ return False, '"usb_mass_device" was not in the device configuration "cmsis-dap" boot method parameters'
+ return True, 'accepted'
+
+
+class BootCMSIS(BootAction):
+
+ def __init__(self):
+ super(BootCMSIS, self).__init__()
+ self.name = 'boot-cmsis'
+ self.description = "boot cmsis usb image"
+ self.summary = "boot cmsis usb image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootCMSISRetry())
+
+
+class BootCMSISRetry(RetryAction):
+
+ def __init__(self):
+ super(BootCMSISRetry, self).__init__()
+ self.name = 'boot-cmsis-retry'
+ self.description = "boot cmsis usb image with retry"
+ self.summary = "boot cmsis usb image with retry"
+
+ def validate(self):
+ super(BootCMSISRetry, self).validate()
+ method_params = self.job.device['actions']['boot']['methods']['cmsis-dap']['parameters']
+ usb_mass_device = method_params.get('usb_mass_device', None)
+ if not usb_mass_device:
+ self.errors = "usb_mass_device unset"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ method_params = self.job.device['actions']['boot']['methods']['cmsis-dap']['parameters']
+ usb_mass_device = method_params.get('usb_mass_device', None)
+ resets_after_flash = method_params.get('resets_after_flash', True)
+ if self.job.device.hard_reset_command:
+ self.internal_pipeline.add_action(ResetDevice())
+ self.internal_pipeline.add_action(WaitDevicePathAction(usb_mass_device))
+ self.internal_pipeline.add_action(FlashCMSISAction())
+ if resets_after_flash:
+ self.internal_pipeline.add_action(WaitUSBSerialDeviceAction())
+ self.internal_pipeline.add_action(ConnectDevice())
+
+
+class FlashCMSISAction(Action):
+
+ def __init__(self):
+ super(FlashCMSISAction, self).__init__()
+ self.name = "flash-cmsis"
+ self.description = "flash cmsis to usb mass storage"
+ self.summary = "flash cmsis to usb mass storage"
+ self.filelist = []
+ self.usb_mass_device = None
+
+ def validate(self):
+ super(FlashCMSISAction, self).validate()
+ if self.job.device['board_id'] == '0000000000':
+ self.errors = "board_id unset"
+ method_parameters = self.job.device['actions']['boot']['methods']['cmsis-dap']['parameters']
+ self.usb_mass_device = method_parameters.get('usb_mass_device', None)
+ if not self.usb_mass_device:
+ self.errors = "usb_mass_device unset"
+ namespace = self.parameters['namespace']
+ for action in self.data[namespace]['download-action'].keys():
+ action_arg = self.get_namespace_data(action='download-action', label=action, key='file')
+ self.filelist.extend([action_arg])
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(FlashCMSISAction, self).run(connection, max_end_time, args)
+ dstdir = mkdtemp()
+ mount_command = "mount -t vfat %s %s" % (self.usb_mass_device, dstdir)
+ self.run_command(mount_command.split(' '), allow_silent=True)
+ # mount
+ for f in self.filelist:
+ self.logger.debug("Copying %s to %s", f, dstdir)
+ shutil.copy2(f, dstdir)
+ # umount
+ umount_command = "umount %s" % self.usb_mass_device
+ self.run_command(umount_command.split(' '), allow_silent=True)
+ if self.errors:
+ raise InfrastructureError("Unable to (un)mount USB device: %s" % self.usb_mass_device)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/dfu.py b/lava_dispatcher/actions/boot/dfu.py
new file mode 100644
index 000000000..797ebdde8
--- /dev/null
+++ b/lava_dispatcher/actions/boot/dfu.py
@@ -0,0 +1,159 @@
+# Copyright (C) 2016 Linaro Limited
+#
+# Author: Tyler Baker <tyler.baker@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>.
+
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ InfrastructureError,
+ Pipeline,
+)
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.utils.udev import WaitDFUDeviceAction
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.utils.shell import which
+from lava_dispatcher.utils.strings import substitute
+
+
+class DFU(Boot):
+
+ compatibility = 4 # FIXME: change this to 5 and update test cases
+
+ def __init__(self, parent, parameters):
+ super(DFU, self).__init__(parent)
+ self.action = BootDFU()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'dfu' not in device['actions']['boot']['methods']:
+ return False, '"dfu" was not in the device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" was in the parameters'
+ if parameters['method'] != 'dfu':
+ return False, '"method" was not "dfu"'
+ if 'board_id' not in device:
+ return False, '"board_id" is not in the device configuration'
+ return True, 'accepted'
+
+
+class BootDFU(BootAction):
+
+ def __init__(self):
+ super(BootDFU, self).__init__()
+ self.name = 'boot-dfu-image'
+ self.description = "boot dfu image with retry"
+ self.summary = "boot dfu image with retry"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootDFURetry())
+
+
+class BootDFURetry(RetryAction):
+
+ def __init__(self):
+ super(BootDFURetry, self).__init__()
+ self.name = 'boot-dfu-retry'
+ self.description = "boot dfu image using the command line interface"
+ self.summary = "boot dfu image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(ResetDevice())
+ self.internal_pipeline.add_action(WaitDFUDeviceAction())
+ self.internal_pipeline.add_action(FlashDFUAction())
+
+
+class FlashDFUAction(Action):
+
+ def __init__(self):
+ super(FlashDFUAction, self).__init__()
+ self.name = "flash-dfu"
+ self.description = "use dfu to flash the images"
+ self.summary = "use dfu to flash the images"
+ self.base_command = []
+ self.exec_list = []
+ self.board_id = '0000000000'
+ self.usb_vendor_id = '0000'
+ self.usb_product_id = '0000'
+
+ def validate(self):
+ super(FlashDFUAction, self).validate()
+ try:
+ boot = self.job.device['actions']['boot']['methods']['dfu']
+ dfu_binary = which(boot['parameters']['command'])
+ self.base_command = [dfu_binary]
+ self.base_command.extend(boot['parameters'].get('options', []))
+ if self.job.device['board_id'] == '0000000000':
+ self.errors = "board_id unset"
+ if self.job.device['usb_vendor_id'] == '0000':
+ self.errors = 'usb_vendor_id unset'
+ if self.job.device['usb_product_id'] == '0000':
+ self.errors = 'usb_product_id unset'
+ self.usb_vendor_id = self.job.device['usb_vendor_id']
+ self.usb_product_id = self.job.device['usb_product_id']
+ self.board_id = self.job.device['board_id']
+ self.base_command.extend(['--serial', self.board_id])
+ self.base_command.extend(['--device', '%s:%s' % (self.usb_vendor_id, self.usb_product_id)])
+ except AttributeError as exc:
+ raise ConfigurationError(exc)
+ except (KeyError, TypeError):
+ self.errors = "Invalid parameters for %s" % self.name
+ substitutions = {}
+ namespace = self.parameters['namespace']
+ for action in self.data[namespace]['download-action'].keys():
+ dfu_full_command = []
+ image_arg = self.data[namespace]['download-action'][action].get('image_arg', None)
+ action_arg = self.data[namespace]['download-action'][action].get('file', None)
+ if not image_arg or not action_arg:
+ self.errors = "Missing image_arg for %s. " % action
+ continue
+ if not isinstance(image_arg, str):
+ self.errors = "image_arg is not a string (try quoting it)"
+ continue
+ substitutions["{%s}" % action] = action_arg
+ dfu_full_command.extend(self.base_command)
+ dfu_full_command.extend(substitute([image_arg], substitutions))
+ self.exec_list.append(dfu_full_command)
+ if len(self.exec_list) < 1:
+ self.errors = "No DFU command to execute"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(FlashDFUAction, self).run(connection, max_end_time, args)
+ count = 1
+ for dfu_command in self.exec_list:
+ if count == (len(self.exec_list)):
+ if self.job.device['actions']['boot']['methods']['dfu'].get('reset_works', True):
+ dfu_command.extend(['--reset'])
+ dfu = ' '.join(dfu_command)
+ output = self.run_command(dfu.split(' '))
+ if output:
+ if not ("No error condition is present\nDone!\n" in output):
+ raise InfrastructureError("command failed: %s" % dfu)
+ else:
+ raise InfrastructureError("command failed: %s" % dfu)
+ count += 1
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/docker.py b/lava_dispatcher/actions/boot/docker.py
new file mode 100644
index 000000000..755620cdd
--- /dev/null
+++ b/lava_dispatcher/actions/boot/docker.py
@@ -0,0 +1,140 @@
+# Copyright (C) 2017 Linaro Limited
+#
+# Author: Remi Duraffort <remi.duraffort@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 os
+
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+)
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import (
+ ExpectShellSession,
+ ShellCommand,
+ ShellSession
+)
+
+
+class BootDocker(Boot):
+ compatibility = 4
+
+ def __init__(self, parent, parameters):
+ super(BootDocker, self).__init__(parent)
+ self.action = BootDockerAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if "docker" not in device['actions']['boot']['methods']:
+ return False, '"docker" was not in the device configuration boot methods'
+ if "command" not in parameters:
+ return False, '"command" was not in boot parameters'
+ return True, 'accepted'
+
+
+class BootDockerAction(BootAction):
+
+ def __init__(self):
+ super(BootDockerAction, self).__init__()
+ self.name = 'boot-docker'
+ self.description = "boot docker image"
+ self.summary = "boot docker image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootDockerRetry())
+ if self.has_prompts(parameters):
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+class BootDockerRetry(RetryAction):
+
+ def __init__(self):
+ super(BootDockerRetry, self).__init__()
+ self.name = 'boot-docker-retry'
+ self.description = "boot docker image with retry"
+ self.summary = "boot docker image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(CallDockerAction())
+
+
+class CallDockerAction(Action):
+
+ def __init__(self):
+ super(CallDockerAction, self).__init__()
+ self.name = "docker-run"
+ self.description = "call docker run on the image"
+ self.summary = "call docker run"
+ self.cleanup_required = False
+ self.extra_options = ''
+
+ def validate(self):
+ super(CallDockerAction, self).validate()
+ self.container = "lava-%s-%s" % (self.job.job_id, self.level)
+
+ options = self.job.device['actions']['boot']['methods']['docker']['options']
+
+ if options['cpus']:
+ self.extra_options += ' --cpus %s' % options['cpus']
+ if options['memory']:
+ self.extra_options += ' --memory %s' % options['memory']
+ if options['volumes']:
+ for volume in options['volumes']:
+ self.extra_options += ' --volume %s' % volume
+
+ def run(self, connection, max_end_time, args=None):
+ location = self.get_namespace_data(action='test', label='shared', key='location')
+ overlay = self.get_namespace_data(action='test', label='results', key='lava_test_results_dir')
+ docker_image = self.get_namespace_data(action='deploy-docker', label='image', key='name')
+
+ # Build the command line
+ # The docker image is safe to be included in the command line
+ cmd = "docker run --interactive --tty --hostname lava"
+ cmd += " --name %s" % self.container
+ cmd += " --volume %s:%s" % (os.path.join(location, overlay.strip("/")), overlay)
+ cmd += self.extra_options
+ cmd += " %s %s" % (docker_image, self.parameters["command"])
+
+ self.logger.debug("Boot command: %s", cmd)
+ shell = ShellCommand(cmd, self.timeout, logger=self.logger)
+ self.cleanup_required = True
+
+ shell_connection = ShellSession(self.job, shell)
+ shell_connection = super(CallDockerAction, self).run(shell_connection, max_end_time, args)
+
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=shell_connection)
+ return shell_connection
+
+ def cleanup(self, connection):
+ super(CallDockerAction, self).cleanup(connection)
+ if self.cleanup_required:
+ self.logger.debug("Stopping container %s", self.container)
+ self.run_command(["docker", "stop", self.container], allow_fail=True)
+ self.logger.debug("Removing container %s", self.container)
+ self.run_command(["docker", "rm", self.container], allow_fail=True)
+ self.cleanup_required = False
diff --git a/lava_dispatcher/actions/boot/environment.py b/lava_dispatcher/actions/boot/environment.py
new file mode 100644
index 000000000..622406582
--- /dev/null
+++ b/lava_dispatcher/actions/boot/environment.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2015 Linaro Limited
+#
+# Author: Stevan Radakovic <stevan.radakovic@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>.
+
+from lava_dispatcher.action import Action
+
+
+class ExportDeviceEnvironment(Action):
+ """
+ Exports environment variables found in common data on to the device.
+ """
+
+ def __init__(self):
+ super(ExportDeviceEnvironment, self).__init__()
+ self.name = "export-device-env"
+ self.summary = "Exports environment variables action"
+ self.description = "Exports environment variables to the device"
+ self.env = []
+
+ def validate(self):
+ super(ExportDeviceEnvironment, self).validate()
+ shell_file = self.get_namespace_data(action='deploy-device-env',
+ label='environment', key='shell_file')
+ environment = self.get_namespace_data(action='deploy-device-env',
+ label='environment', key='env_dict')
+ if not environment:
+ return
+ # Append export commands to the shell init file.
+ # Retain quotes into the final shell.
+ for key in environment:
+ self.env.append("echo export %s=\\'%s\\' >> %s" % (
+ key, environment[key], shell_file))
+
+ def run(self, connection, max_end_time, args=None):
+
+ if not connection:
+ return
+
+ connection = super(ExportDeviceEnvironment, self).run(connection, max_end_time, args)
+
+ shell_file = self.get_namespace_data(
+ action='deploy-device-env',
+ label='environment',
+ key='shell_file'
+ )
+
+ for line in self.env:
+ connection.sendline(line, delay=self.character_delay)
+
+ if shell_file:
+ connection.sendline('. %s' % shell_file, delay=self.character_delay)
+
+ return connection
diff --git a/lava_dispatcher/actions/boot/fastboot.py b/lava_dispatcher/actions/boot/fastboot.py
new file mode 100644
index 000000000..a321fc166
--- /dev/null
+++ b/lava_dispatcher/actions/boot/fastboot.py
@@ -0,0 +1,356 @@
+# Copyright (C) 2015 Linaro Limited
+#
+# Author: Senthil Kumaran S <senthil.kumaran@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 os
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ InfrastructureError,
+ JobError,
+ Pipeline,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import (
+ BootAction,
+ AutoLoginAction,
+ BootloaderCommandsAction,
+ OverlayUnpack,
+)
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.utils.constants import LAVA_LXC_HOME
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.protocols.lxc import LxcProtocol
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.actions.boot.u_boot import UBootEnterFastbootAction
+
+
+def _fastboot_sequence_map(sequence):
+ """Maps fastboot sequence with corresponding class."""
+ sequence_map = {'boot': (FastbootBootAction, None),
+ 'reboot': (FastbootRebootAction, None),
+ 'no-flash-boot': (FastbootBootAction, None),
+ 'auto-login': (AutoLoginAction, None),
+ 'overlay-unpack': (OverlayUnpack, None),
+ 'shell-session': (ExpectShellSession, None),
+ 'export-env': (ExportDeviceEnvironment, None), }
+ return sequence_map.get(sequence, (None, None))
+
+
+class BootFastboot(Boot):
+ """
+ Expects fastboot bootloader, and boots.
+ """
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(BootFastboot, self).__init__(parent)
+ self.action = BootFastbootAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' in parameters:
+ if parameters['method'] == 'fastboot':
+ return True, 'accepted'
+ return False, 'boot "method" was not "fastboot"'
+
+
+class BootFastbootAction(BootAction):
+ """
+ Provide for auto_login parameters in this boot stanza and re-establish the
+ connection after boot.
+ """
+ def __init__(self):
+ super(BootFastbootAction, self).__init__()
+ self.name = "fastboot-boot"
+ self.summary = "fastboot boot"
+ self.description = "fastboot boot into the system"
+
+ def validate(self):
+ super(BootFastbootAction, self).validate()
+ sequences = self.job.device['actions']['boot']['methods'].get(
+ 'fastboot', [])
+ for sequence in sequences:
+ if not _fastboot_sequence_map(sequence):
+ self.errors = "Unknown boot sequence '%s'" % sequence
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job,
+ parameters=parameters)
+ # Always ensure the device is in fastboot mode before trying to boot.
+ # Check if the device has a power command such as HiKey, Dragonboard,
+ # etc. against device that doesn't like Nexus, etc.
+ if self.job.device.get('fastboot_via_uboot', False):
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(UBootEnterFastbootAction())
+ elif self.job.device.power_command:
+ self.force_prompt = True
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(ResetDevice())
+ else:
+ self.internal_pipeline.add_action(EnterFastbootAction())
+
+ # Based on the boot sequence defined in the device configuration, add
+ # the required pipeline actions.
+ sequences = self.job.device['actions']['boot']['methods'].get(
+ 'fastboot', [])
+ for sequence in sequences:
+ mapped = _fastboot_sequence_map(sequence)
+ if mapped[1]:
+ self.internal_pipeline.add_action(
+ mapped[0](device_actions=mapped[1]))
+ elif mapped[0]:
+ self.internal_pipeline.add_action(mapped[0]())
+
+
+class WaitFastBootInterrupt(Action):
+ """
+ Interrupts fastboot to access the next bootloader
+ Relies on fastboot-flash-action setting the prompt and string
+ from the deployment parameters.
+ """
+
+ def __init__(self, type):
+ super(WaitFastBootInterrupt, self).__init__()
+ self.name = 'wait-fastboot-interrupt'
+ self.summary = "watch output and try to interrupt fastboot"
+ self.description = "Check for prompt and pass the interrupt string to exit fastboot."
+ self.type = type
+ self.prompt = None
+ self.string = None
+
+ def validate(self):
+ super(WaitFastBootInterrupt, self).validate()
+ if 'fastboot_serial_number' not in self.job.device:
+ self.errors = "device fastboot serial number missing"
+ elif self.job.device['fastboot_serial_number'] == '0000000000':
+ self.errors = "device fastboot serial number unset"
+ if 'fastboot_options' not in self.job.device:
+ self.errors = "device fastboot options missing"
+ elif not isinstance(self.job.device['fastboot_options'], list):
+ self.errors = "device fastboot options is not a list"
+ device_methods = self.job.device['actions']['deploy']['methods']
+ if isinstance(device_methods.get('fastboot'), dict):
+ self.prompt = device_methods['fastboot'].get('interrupt_prompt')
+ self.string = device_methods['fastboot'].get('interrupt_string')
+ if not self.prompt or not self.string:
+ self.errors = "Missing interrupt configuration for device."
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ raise LAVABug("%s started without a connection already in use" % self.name)
+ connection = super(WaitFastBootInterrupt, self).run(connection, max_end_time, args)
+ device_methods = self.job.device['actions']['boot']['methods']
+ # device is to be put into a reset state, either by issuing 'reboot' or power-cycle
+ connection.prompt_str = self.prompt
+ self.logger.debug("Changing prompt to '%s'", connection.prompt_str)
+ self.wait(connection)
+ self.logger.debug("Sending '%s' to interrupt fastboot.", self.string)
+ connection.sendline(self.string)
+ return connection
+
+
+class FastbootBootAction(Action):
+ """
+ This action calls fastboot to boot into the system.
+ """
+
+ def __init__(self):
+ super(FastbootBootAction, self).__init__()
+ self.name = "boot-fastboot"
+ self.summary = "attempt to fastboot boot"
+ self.description = "fastboot boot into system"
+
+ def validate(self):
+ super(FastbootBootAction, self).validate()
+ if 'fastboot_serial_number' not in self.job.device:
+ self.errors = "device fastboot serial number missing"
+ elif self.job.device['fastboot_serial_number'] == '0000000000':
+ self.errors = "device fastboot serial number unset"
+ if 'fastboot_options' not in self.job.device:
+ self.errors = "device fastboot options missing"
+ elif not isinstance(self.job.device['fastboot_options'], list):
+ self.errors = "device fastboot options is not a list"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(FastbootBootAction, self).run(connection, max_end_time, args)
+ # this is the device namespace - the lxc namespace is not accessible
+ lxc_name = None
+ protocol = [protocol for protocol in self.job.protocols if protocol.name == LxcProtocol.name][0]
+ if protocol:
+ lxc_name = protocol.lxc_name
+ if not lxc_name:
+ raise JobError("Unable to use fastboot")
+ self.logger.debug("[%s] lxc name: %s", self.parameters['namespace'],
+ lxc_name)
+ serial_number = self.job.device['fastboot_serial_number']
+ boot_img = self.get_namespace_data(action='download-action',
+ label='boot', key='file')
+ if not boot_img:
+ raise JobError("Boot image not found, unable to boot")
+ else:
+ boot_img = os.path.join(LAVA_LXC_HOME, os.path.basename(boot_img))
+ fastboot_cmd = ['lxc-attach', '-n', lxc_name, '--', 'fastboot',
+ '-s', serial_number, 'boot',
+ boot_img] + self.job.device['fastboot_options']
+ command_output = self.run_command(fastboot_cmd, allow_fail=True)
+ if command_output and 'booting' not in command_output:
+ raise JobError("Unable to boot with fastboot: %s" % command_output)
+ else:
+ status = [status.strip() for status in command_output.split(
+ '\n') if 'finished' in status][0]
+ self.results = {'status': status}
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ lxc_active = any([pc for pc in self.job.protocols if pc.name == LxcProtocol.name])
+ if self.job.device.pre_os_command and not lxc_active:
+ self.logger.info("Running pre OS command.")
+ command = self.job.device.pre_os_command
+ if not self.run_command(command.split(' '), allow_silent=True):
+ raise InfrastructureError("%s failed" % command)
+ return connection
+
+
+class FastbootRebootAction(Action):
+ """
+ This action calls fastboot to reboot into the system.
+ """
+
+ def __init__(self):
+ super(FastbootRebootAction, self).__init__()
+ self.name = "fastboot-reboot"
+ self.summary = "attempt to fastboot reboot"
+ self.description = "fastboot reboot into system"
+
+ def validate(self):
+ super(FastbootRebootAction, self).validate()
+ if 'fastboot_serial_number' not in self.job.device:
+ self.errors = "device fastboot serial number missing"
+ elif self.job.device['fastboot_serial_number'] == '0000000000':
+ self.errors = "device fastboot serial number unset"
+ if 'fastboot_options' not in self.job.device:
+ self.errors = "device fastboot options missing"
+ elif not isinstance(self.job.device['fastboot_options'], list):
+ self.errors = "device fastboot options is not a list"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(FastbootRebootAction, self).run(connection, max_end_time, args)
+ # this is the device namespace - the lxc namespace is not accessible
+ lxc_name = None
+ protocol = [protocol for protocol in self.job.protocols if protocol.name == LxcProtocol.name][0]
+ if protocol:
+ lxc_name = protocol.lxc_name
+ if not lxc_name:
+ raise JobError("Unable to use fastboot")
+ self.logger.debug("[%s] lxc name: %s", self.parameters['namespace'],
+ lxc_name)
+ serial_number = self.job.device['fastboot_serial_number']
+ fastboot_opts = self.job.device['fastboot_options']
+ fastboot_cmd = ['lxc-attach', '-n', lxc_name, '--', 'fastboot', '-s',
+ serial_number, 'reboot'] + fastboot_opts
+ command_output = self.run_command(fastboot_cmd, allow_fail=True)
+ if command_output and 'rebooting' not in command_output:
+ raise JobError("Unable to fastboot reboot: %s" % command_output)
+ else:
+ status = [status.strip() for status in command_output.split(
+ '\n') if 'finished' in status][0]
+ self.results = {'status': status}
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class EnterFastbootAction(Action):
+ """
+ Enters fastboot bootloader.
+ """
+
+ def __init__(self):
+ super(EnterFastbootAction, self).__init__()
+ self.name = "enter-fastboot-action"
+ self.description = "enter fastboot bootloader"
+ self.summary = "enter fastboot"
+
+ def validate(self):
+ super(EnterFastbootAction, self).validate()
+ if 'adb_serial_number' not in self.job.device:
+ self.errors = "device adb serial number missing"
+ elif self.job.device['adb_serial_number'] == '0000000000':
+ self.errors = "device adb serial number unset"
+ if 'fastboot_serial_number' not in self.job.device:
+ self.errors = "device fastboot serial number missing"
+ elif self.job.device['fastboot_serial_number'] == '0000000000':
+ self.errors = "device fastboot serial number unset"
+ if 'fastboot_options' not in self.job.device:
+ self.errors = "device fastboot options missing"
+ elif not isinstance(self.job.device['fastboot_options'], list):
+ self.errors = "device fastboot options is not a list"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(EnterFastbootAction, self).run(connection, max_end_time, args)
+ # this is the device namespace - the lxc namespace is not accessible
+ lxc_name = None
+ protocol = [protocol for protocol in self.job.protocols if protocol.name == LxcProtocol.name][0]
+ if protocol:
+ lxc_name = protocol.lxc_name
+ if not lxc_name:
+ raise JobError("Unable to use fastboot")
+
+ self.logger.debug("[%s] lxc name: %s", self.parameters['namespace'], lxc_name)
+ fastboot_serial_number = self.job.device['fastboot_serial_number']
+
+ # Try to enter fastboot mode with adb.
+ adb_serial_number = self.job.device['adb_serial_number']
+ # start the adb daemon
+ adb_cmd = ['lxc-attach', '-n', lxc_name, '--', 'adb', 'start-server']
+ command_output = self.run_command(adb_cmd, allow_fail=True)
+ if command_output and 'successfully' in command_output:
+ self.logger.debug("adb daemon started: %s", command_output)
+ adb_cmd = ['lxc-attach', '-n', lxc_name, '--', 'adb', '-s',
+ adb_serial_number, 'devices']
+ command_output = self.run_command(adb_cmd, allow_fail=True)
+ if command_output and adb_serial_number in command_output:
+ self.logger.debug("Device is in adb: %s", command_output)
+ adb_cmd = ['lxc-attach', '-n', lxc_name, '--', 'adb',
+ '-s', adb_serial_number, 'reboot-bootloader']
+ self.run_command(adb_cmd)
+ return connection
+
+ # Enter fastboot mode with fastboot.
+ fastboot_opts = self.job.device['fastboot_options']
+ fastboot_cmd = ['lxc-attach', '-n', lxc_name, '--', 'fastboot', '-s',
+ fastboot_serial_number, 'devices'] + fastboot_opts
+ command_output = self.run_command(fastboot_cmd)
+ if command_output and fastboot_serial_number in command_output:
+ self.logger.debug("Device is in fastboot: %s", command_output)
+ fastboot_cmd = ['lxc-attach', '-n', lxc_name, '--', 'fastboot',
+ '-s', fastboot_serial_number,
+ 'reboot-bootloader'] + fastboot_opts
+ command_output = self.run_command(fastboot_cmd)
+ if command_output and 'OKAY' not in command_output:
+ raise InfrastructureError("Unable to enter fastboot: %s" %
+ command_output)
+ else:
+ status = [status.strip() for status in command_output.split(
+ '\n') if 'finished' in status][0]
+ self.results = {'status': status}
+ return connection
diff --git a/lava_dispatcher/actions/boot/grub.py b/lava_dispatcher/actions/boot/grub.py
new file mode 100644
index 000000000..d7f5d43a8
--- /dev/null
+++ b/lava_dispatcher/actions/boot/grub.py
@@ -0,0 +1,313 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Matthew Hart <matthew.hart@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>.
+
+# List just the subclasses supported for this base strategy
+# imported by the parser to populate the list of subclasses.
+
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ LAVABug,
+ Pipeline
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import (
+ BootAction,
+ AutoLoginAction,
+ BootloaderCommandOverlay,
+ BootloaderSecondaryMedia,
+ BootloaderCommandsAction,
+ OverlayUnpack,
+)
+from lava_dispatcher.actions.boot.uefi_menu import (
+ UEFIMenuInterrupt,
+ UefiMenuSelector
+)
+from lava_dispatcher.actions.boot.fastboot import WaitFastBootInterrupt
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.power import (
+ ResetDevice,
+ PowerOff
+)
+
+
+class GrubSequence(Boot):
+
+ compatibility = 3
+
+ def __init__(self, parent, parameters):
+ super(GrubSequence, self).__init__(parent)
+ self.action = GrubSequenceAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' not in parameters:
+ raise ConfigurationError("method not specified in boot parameters")
+ if parameters["method"] not in ["grub", "grub-efi"]:
+ return False, '"method" was not "grub" or "grub-efi"'
+ if 'actions' not in device:
+ raise ConfigurationError("Invalid device configuration")
+ if 'boot' not in device['actions']:
+ return False, '"boot" was not in the device configuration actions'
+ if 'methods' not in device['actions']['boot']:
+ raise ConfigurationError("Device misconfiguration")
+ params = device['actions']['boot']['methods']
+ if 'grub' not in params:
+ return False, '"grub" was not in the device configuration boot methods'
+ if 'grub-efi' in params:
+ return False, '"grub-efi" was not in the device configuration boot methods'
+ if 'sequence' in params['grub']:
+ return True, 'accepted'
+ return False, '"sequence" not in device configuration boot methods'
+
+
+class Grub(Boot):
+
+ compatibility = 3
+
+ def __init__(self, parent, parameters):
+ super(Grub, self).__init__(parent)
+ self.action = GrubMainAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' not in parameters:
+ raise ConfigurationError("method not specified in boot parameters")
+ if parameters["method"] not in ["grub", "grub-efi"]:
+ return False, '"method" was not "grub" or "grub-efi"'
+ if 'actions' not in device:
+ raise ConfigurationError("Invalid device configuration")
+ if 'boot' not in device['actions']:
+ return False, '"boot" was not in the device configuration actions'
+ if 'methods' not in device['actions']['boot']:
+ raise ConfigurationError("Device misconfiguration")
+ params = device['actions']['boot']['methods']
+ if 'grub' in params and 'sequence' in params['grub']:
+ return False, '"sequence" was in "grub" parameters'
+ if 'grub' in params or 'grub-efi' in params:
+ return True, 'accepted'
+ else:
+ return False, '"grub" or "grub-efi" was not in the device configuration boot methods'
+
+
+def _grub_sequence_map(sequence):
+ """Maps grub sequence with corresponding class."""
+ sequence_map = {
+ 'wait-fastboot-interrupt': (WaitFastBootInterrupt, 'grub'),
+ 'auto-login': (AutoLoginAction, None),
+ 'shell-session': (ExpectShellSession, None),
+ 'export-env': (ExportDeviceEnvironment, None),
+ }
+ return sequence_map.get(sequence, (None, None))
+
+
+class GrubSequenceAction(BootAction):
+
+ def __init__(self):
+ super(GrubSequenceAction, self).__init__()
+ self.name = "grub-sequence-action"
+ self.description = "grub boot sequence"
+ self.summary = "run grub boot using specified sequence of actions"
+ self.expect_shell = False
+
+ def validate(self):
+ super(GrubSequenceAction, self).validate()
+ sequences = self.job.device['actions']['boot']['methods']['grub'].get(
+ 'sequence', [])
+ for sequence in sequences:
+ if not _grub_sequence_map(sequence):
+ self.errors = "Unknown boot sequence '%s'" % sequence
+
+ def populate(self, parameters):
+ super(GrubSequenceAction, self).populate(parameters)
+ self.internal_pipeline = Pipeline(parent=self, job=self.job,
+ parameters=parameters)
+ sequences = self.job.device['actions']['boot']['methods']['grub'].get(
+ 'sequence', [])
+ for sequence in sequences:
+ mapped = _grub_sequence_map(sequence)
+ if mapped[1]:
+ self.internal_pipeline.add_action(
+ mapped[0](type=mapped[1]))
+ elif mapped[0]:
+ self.internal_pipeline.add_action(mapped[0]())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+ else:
+ if self.has_boot_finished(parameters):
+ self.logger.debug("Doing a boot without a shell (installer)")
+ self.internal_pipeline.add_action(InstallerWait())
+ self.internal_pipeline.add_action(PowerOff())
+
+
+class GrubMainAction(BootAction):
+ def __init__(self):
+ super(GrubMainAction, self).__init__()
+ self.name = "grub-main-action"
+ self.description = "main grub boot action"
+ self.summary = "run grub boot from power to system"
+ self.expect_shell = True
+
+ def populate(self, parameters):
+ self.expect_shell = parameters.get('expect_shell', True)
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootloaderSecondaryMedia())
+ self.internal_pipeline.add_action(BootloaderCommandOverlay())
+ self.internal_pipeline.add_action(ConnectDevice())
+ # FIXME: reset_device is a hikey hack due to fastboot/OTG issues
+ # remove as part of LAVA-940 - convert to use fastboot-sequence
+ reset_device = self.job.device['actions']['boot']['methods'].get('grub-efi', {}).get('reset_device', True)
+ if parameters['method'] == 'grub-efi' and reset_device:
+ # added unless the device specifies not to reset the device in grub.
+ self.internal_pipeline.add_action(ResetDevice())
+ elif parameters['method'] == 'grub':
+ self.internal_pipeline.add_action(ResetDevice())
+ if parameters['method'] == 'grub-efi':
+ self.internal_pipeline.add_action(UEFIMenuInterrupt())
+ self.internal_pipeline.add_action(GrubMenuSelector())
+ self.internal_pipeline.add_action(BootloaderInterrupt())
+ self.internal_pipeline.add_action(BootloaderCommandsAction())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+ else:
+ if self.has_boot_finished(parameters):
+ self.logger.debug("Doing a boot without a shell (installer)")
+ self.internal_pipeline.add_action(InstallerWait())
+ self.internal_pipeline.add_action(PowerOff())
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(GrubMainAction, self).run(connection, max_end_time, args)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class BootloaderInterrupt(Action):
+ """
+ Support for interrupting the bootloader.
+ """
+ def __init__(self):
+ super(BootloaderInterrupt, self).__init__()
+ self.name = "bootloader-interrupt"
+ self.description = "interrupt bootloader"
+ self.summary = "interrupt bootloader to get a prompt"
+ self.type = "grub"
+
+ def validate(self):
+ super(BootloaderInterrupt, self).validate()
+ if self.job.device.connect_command is '':
+ self.errors = "Unable to connect to device"
+ device_methods = self.job.device['actions']['boot']['methods']
+ if self.parameters['method'] == 'grub-efi' and 'grub-efi' in device_methods:
+ self.type = 'grub-efi'
+ if 'bootloader_prompt' not in device_methods[self.type]['parameters']:
+ self.errors = "[%s] Missing bootloader prompt for device" % self.name
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ raise LAVABug("%s started without a connection already in use" % self.name)
+ connection = super(BootloaderInterrupt, self).run(connection, max_end_time, args)
+ device_methods = self.job.device['actions']['boot']['methods']
+ interrupt_prompt = device_methods[self.type]['parameters'].get('interrupt_prompt', self.job.device.get_constant('grub-autoboot-prompt'))
+ # interrupt_char can actually be a sequence of ASCII characters - sendline does not care.
+ interrupt_char = device_methods[self.type]['parameters'].get('interrupt_char', self.job.device.get_constant('grub-interrupt-character'))
+ # device is to be put into a reset state, either by issuing 'reboot' or power-cycle
+ connection.prompt_str = interrupt_prompt
+ self.wait(connection)
+ connection.raw_connection.send(interrupt_char)
+ return connection
+
+
+class GrubMenuSelector(UefiMenuSelector): # pylint: disable=too-many-instance-attributes
+
+ def __init__(self):
+ super(GrubMenuSelector, self).__init__()
+ self.name = 'grub-efi-menu-selector'
+ self.summary = 'select grub options in the efi menu'
+ self.description = 'select specified grub-efi menu items'
+ self.selector.prompt = "Start:"
+ self.commands = []
+ self.boot_message = None
+ self.params = None
+
+ def validate(self):
+ if self.method_name not in self.job.device['actions']['boot']['methods']:
+ self.errors = "No %s in device boot methods" % self.method_name
+ return
+ self.params = self.job.device['actions']['boot']['methods'][self.method_name]
+ if 'menu_options' not in self.params:
+ self.errors = "Missing entry for menu item to use for %s" % self.method_name
+ return
+ self.commands = self.params['menu_options']
+ super(GrubMenuSelector, self).validate()
+
+ def run(self, connection, max_end_time, args=None):
+ interrupt_prompt = self.params['parameters'].get(
+ 'interrupt_prompt', self.job.device.get_constant('grub-autoboot-prompt'))
+ self.logger.debug("Adding '%s' to prompt", interrupt_prompt)
+ connection.prompt_str = interrupt_prompt
+ # override base class behaviour to interact with grub.
+ self.boot_message = None
+ connection = super(GrubMenuSelector, self).run(connection, max_end_time, args)
+ return connection
+
+
+class InstallerWait(Action):
+ """
+ Wait for the non-interactive installer to finished
+ """
+ def __init__(self):
+ super(InstallerWait, self).__init__()
+ self.name = "installer-wait"
+ self.description = "installer wait"
+ self.summary = "wait for task to finish match arbitrary string"
+ self.type = "grub"
+
+ def validate(self):
+ super(InstallerWait, self).validate()
+ if "boot_finished" not in self.parameters:
+ self.errors = "Missing boot_finished string"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(InstallerWait, self).run(connection, max_end_time, args)
+ wait_string = self.parameters['boot_finished']
+ msg = wait_string if isinstance(wait_string, str) else ', '.join(wait_string)
+ self.logger.debug("Not expecting a shell, so waiting for boot_finished: %s", msg)
+ connection.prompt_str = wait_string
+ self.wait(connection)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/ipxe.py b/lava_dispatcher/actions/boot/ipxe.py
new file mode 100644
index 000000000..68ca5255e
--- /dev/null
+++ b/lava_dispatcher/actions/boot/ipxe.py
@@ -0,0 +1,165 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Matthew Hart <matthew.hart@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>.
+
+# List just the subclasses supported for this base strategy
+# imported by the parser to populate the list of subclasses.
+
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ LAVABug,
+ Pipeline,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import (
+ BootAction,
+ AutoLoginAction,
+ BootloaderCommandOverlay,
+ BootloaderCommandsAction,
+ OverlayUnpack,
+)
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.utils.constants import (
+ IPXE_BOOT_PROMPT,
+)
+
+
+class IPXE(Boot):
+ """
+ The IPXE method prepares the command to run on the dispatcher but this
+ command needs to start a new connection and then interrupt iPXE.
+ An expect shell session can then be handed over to the BootloaderAction.
+ self.run_command is a blocking call, so Boot needs to use
+ a direct spawn call via ShellCommand (which wraps pexpect.spawn) then
+ hand this pexpect wrapper to subsequent actions as a shell connection.
+ """
+
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(IPXE, self).__init__(parent)
+ self.action = BootloaderAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if parameters['method'] != 'ipxe':
+ return False, '"method" was not "ipxe"'
+ if 'ipxe' in device['actions']['boot']['methods']:
+ return True, 'accepted'
+ else:
+ return False, '"ipxe" was not in the device configuration boot methods'
+
+
+class BootloaderAction(BootAction):
+ """
+ Wraps the Retry Action to allow for actions which precede
+ the reset, e.g. Connect.
+ """
+ def __init__(self):
+ super(BootloaderAction, self).__init__()
+ self.name = "bootloader-action"
+ self.description = "interactive bootloader action"
+ self.summary = "pass boot commands"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ # customize the device configuration for this job
+ self.internal_pipeline.add_action(BootloaderCommandOverlay())
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(BootloaderRetry())
+
+
+class BootloaderRetry(BootAction):
+
+ def __init__(self):
+ super(BootloaderRetry, self).__init__()
+ self.name = "bootloader-retry"
+ self.description = "interactive uboot retry action"
+ self.summary = "uboot commands with retry"
+ self.type = "ipxe"
+ self.force_prompt = False
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ # establish a new connection before trying the reset
+ self.internal_pipeline.add_action(ResetDevice())
+ self.internal_pipeline.add_action(BootloaderInterrupt())
+ # need to look for Hit any key to stop autoboot
+ self.internal_pipeline.add_action(BootloaderCommandsAction())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+ def validate(self):
+ super(BootloaderRetry, self).validate()
+ if 'bootloader_prompt' not in self.job.device['actions']['boot']['methods'][self.type]['parameters']:
+ self.errors = "Missing bootloader prompt for device"
+ self.set_namespace_data(
+ action=self.name,
+ label='bootloader_prompt',
+ key='prompt',
+ value=self.job.device['actions']['boot']['methods'][self.type]['parameters']['bootloader_prompt']
+ )
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(BootloaderRetry, self).run(connection, max_end_time, args)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class BootloaderInterrupt(Action):
+ """
+ Support for interrupting the bootloader.
+ """
+ def __init__(self):
+ super(BootloaderInterrupt, self).__init__()
+ self.name = "bootloader-interrupt"
+ self.description = "interrupt bootloader"
+ self.summary = "interrupt bootloader to get a prompt"
+ self.type = "ipxe"
+
+ def validate(self):
+ super(BootloaderInterrupt, self).validate()
+ if self.job.device.connect_command is '':
+ self.errors = "Unable to connect to device"
+ device_methods = self.job.device['actions']['boot']['methods']
+ if 'bootloader_prompt' not in device_methods[self.type]['parameters']:
+ self.errors = "Missing bootloader prompt for device"
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ raise LAVABug("%s started without a connection already in use" % self.name)
+ connection = super(BootloaderInterrupt, self).run(connection, max_end_time, args)
+ self.logger.debug("Changing prompt to '%s'", IPXE_BOOT_PROMPT)
+ # device is to be put into a reset state, either by issuing 'reboot' or power-cycle
+ connection.prompt_str = IPXE_BOOT_PROMPT
+ self.wait(connection)
+ connection.sendcontrol("b")
+ return connection
diff --git a/lava_dispatcher/actions/boot/iso.py b/lava_dispatcher/actions/boot/iso.py
new file mode 100644
index 000000000..7cea61b32
--- /dev/null
+++ b/lava_dispatcher/actions/boot/iso.py
@@ -0,0 +1,212 @@
+# Copyright (C) 2016 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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 os
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ JobError,
+ Pipeline,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.utils.shell import which
+from lava_dispatcher.utils.strings import substitute
+from lava_dispatcher.utils.constants import INSTALLER_QUIET_MSG
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import (
+ ExpectShellSession,
+ ShellCommand,
+ ShellSession
+)
+from lava_dispatcher.actions.boot import AutoLoginAction
+
+
+class BootIsoInstaller(Boot):
+
+ compatibility = 3
+
+ def __init__(self, parent, parameters):
+ super(BootIsoInstaller, self).__init__(parent)
+ self.action = BootIsoInstallerAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'media' in parameters and parameters['media'] == 'img':
+ if 'method' in parameters and parameters['method'] == 'qemu-iso':
+ return True, 'accepted'
+ return False, '"media" was not in parameters or "media" was not "img"'
+
+
+class BootIsoInstallerAction(BootAction):
+
+ def __init__(self):
+ super(BootIsoInstallerAction, self).__init__()
+ self.name = 'boot-installer-iso'
+ self.description = "boot installer with preseed"
+ self.summary = "boot installer iso image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(IsoCommandLine())
+ self.internal_pipeline.add_action(MonitorInstallerSession())
+ self.internal_pipeline.add_action(IsoRebootAction())
+ # Add AutoLoginAction unconditionally as this action does nothing if
+ # the configuration does not contain 'auto_login'
+ self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+class IsoCommandLine(Action): # pylint: disable=too-many-instance-attributes
+
+ """
+ qemu-system-x86_64 -nographic -enable-kvm -cpu host -net nic,model=virtio,macaddr=52:54:00:12:34:59 -net user -m 2048 \
+ -drive format=raw,file=hd_img.img -drive file=${NAME},index=2,media=cdrom,readonly \
+ -boot c -no-reboot -kernel vmlinuz -initrd initrd.gz \
+ -append "\"${BASE} ${LOCALE} ${CONSOLE} ${KEYMAPS} ${NETCFG} preseed/url=${PRESEED_URL} --- ${CONSOLE}\"" \
+ """
+
+ def __init__(self):
+ super(IsoCommandLine, self).__init__()
+ self.name = 'execute-installer-command'
+ self.summary = 'include downloaded locations and call qemu'
+ self.description = 'add dynamic data values to command line and execute'
+
+ def run(self, connection, max_end_time, args=None):
+ # substitutions
+ substitutions = {'{emptyimage}': self.get_namespace_data(action='prepare-empty-image', label='prepare-empty-image', key='output')}
+ sub_command = self.get_namespace_data(action='prepare-qemu-commands', label='prepare-qemu-commands', key='sub_command')
+ sub_command = substitute(sub_command, substitutions)
+ command_line = ' '.join(sub_command)
+
+ commands = []
+ # get the download args in run()
+ image_arg = self.get_namespace_data(action='download-action', label='iso', key='image_arg')
+ action_arg = self.get_namespace_data(action='download-action', label='iso', key='file')
+ substitutions["{%s}" % 'iso'] = action_arg
+ commands.append(image_arg)
+ command_line += ' '.join(substitute(commands, substitutions))
+
+ preseed_file = self.get_namespace_data(action='download-action', label='file', key='preseed')
+ if not preseed_file:
+ raise JobError("Unable to identify downloaded preseed filename.")
+ substitutions = {'{preseed}': preseed_file}
+ append_args = self.get_namespace_data(action='prepare-qemu-commands', label='prepare-qemu-commands', key='append')
+ append_args = substitute([append_args], substitutions)
+ command_line += ' '.join(append_args)
+
+ self.logger.info(command_line)
+ shell = ShellCommand(command_line, self.timeout, logger=self.logger)
+ if shell.exitstatus:
+ raise JobError("%s command exited %d: %s" % (sub_command[0], shell.exitstatus, shell.readlines()))
+ self.logger.debug("started a shell command")
+
+ shell_connection = ShellSession(self.job, shell)
+ shell_connection.prompt_str = self.get_namespace_data(
+ action='prepare-qemu-commands', label='prepare-qemu-commands', key='prompts')
+ shell_connection = super(IsoCommandLine, self).run(shell_connection, max_end_time, args)
+ return shell_connection
+
+
+class MonitorInstallerSession(Action):
+ """
+ Waits for a shell connection to the device for the current job.
+ The shell connection can be over any particular connection,
+ all that is needed is a prompt.
+ """
+ compatibility = 3
+
+ def __init__(self):
+ super(MonitorInstallerSession, self).__init__()
+ self.name = "monitor-installer-connection"
+ self.summary = "Watch for error strings or end of install"
+ self.description = "Monitor installer operation"
+ self.force_prompt = True
+
+ def validate(self):
+ super(MonitorInstallerSession, self).validate()
+ if 'prompts' not in self.parameters:
+ self.errors = "Unable to identify test image prompts from parameters."
+
+ def run(self, connection, max_end_time, args=None):
+ self.logger.debug("%s: Waiting for prompt %s", self.name, ' '.join(connection.prompt_str))
+ self.wait(connection, max_end_time)
+ return connection
+
+
+class IsoRebootAction(Action):
+
+ def __init__(self):
+ super(IsoRebootAction, self).__init__()
+ self.name = 'reboot-into-installed'
+ self.summary = 'reboot into installed image'
+ self.description = 'reboot and login to the new system'
+ self.sub_command = None
+
+ def validate(self):
+ super(IsoRebootAction, self).validate()
+ if 'prompts' not in self.parameters:
+ self.errors = "Unable to identify boot prompts from job definition."
+ try:
+ boot = self.job.device['actions']['boot']['methods']['qemu']
+ qemu_binary = which(boot['parameters']['command'])
+ self.sub_command = [qemu_binary]
+ self.sub_command.extend(boot['parameters'].get('options', []))
+ except AttributeError as exc:
+ raise ConfigurationError(exc)
+ except (KeyError, TypeError):
+ self.errors = "Invalid parameters for %s" % self.name
+
+ def run(self, connection, max_end_time, args=None):
+ """
+ qemu needs help to reboot after running the debian installer
+ and typically the boot is quiet, so there is almost nothing to log.
+ """
+ base_image = self.get_namespace_data(action='prepare-empty-image', label='prepare-empty-image', key='output')
+ self.sub_command.append('-drive format=raw,file=%s' % base_image)
+ guest = self.get_namespace_data(action='apply-overlay-guest', label='guest', key='filename')
+ if guest:
+ self.logger.info("Extending command line for qcow2 test overlay")
+ self.sub_command.append('-drive format=qcow2,file=%s,media=disk' % (os.path.realpath(guest)))
+ # push the mount operation to the test shell pre-command to be run
+ # before the test shell tries to execute.
+ shell_precommand_list = []
+ mountpoint = self.get_namespace_data(action='test', label='results', key='lava_test_results_dir')
+ shell_precommand_list.append('mkdir %s' % mountpoint)
+ shell_precommand_list.append('mount -L LAVA %s' % mountpoint)
+ self.set_namespace_data(action='test', label='lava-test-shell', key='pre-command-list', value=shell_precommand_list)
+
+ self.logger.info("Boot command: %s", ' '.join(self.sub_command))
+ shell = ShellCommand(' '.join(self.sub_command), self.timeout, logger=self.logger)
+ if shell.exitstatus:
+ raise JobError("%s command exited %d: %s" % (self.sub_command, shell.exitstatus, shell.readlines()))
+ self.logger.debug("started a shell command")
+
+ shell_connection = ShellSession(self.job, shell)
+ shell_connection = super(IsoRebootAction, self).run(shell_connection, max_end_time, args)
+ shell_connection.prompt_str = [INSTALLER_QUIET_MSG]
+ self.wait(shell_connection)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=shell_connection)
+ return shell_connection
diff --git a/lava_dispatcher/actions/boot/kexec.py b/lava_dispatcher/actions/boot/kexec.py
new file mode 100644
index 000000000..827e42e31
--- /dev/null
+++ b/lava_dispatcher/actions/boot/kexec.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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>.
+
+
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.actions.boot import AutoLoginAction
+
+
+class BootKExec(Boot):
+ """
+ Expects a shell session, checks for kexec executable and
+ prepares the arguments to run kexec,
+ """
+
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(BootKExec, self).__init__(parent)
+ self.action = BootKexecAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' in parameters:
+ if parameters['method'] == 'kexec':
+ return True, 'accepted'
+ return False, '"method" was not in parameters, or "method" was not "kexec"'
+
+
+class BootKexecAction(BootAction):
+ """
+ Provide for auto_login parameters in this boot stanza and re-establish the connection after boot
+ """
+ def __init__(self):
+ super(BootKexecAction, self).__init__()
+ self.name = "kexec-boot"
+ self.summary = "kexec a new kernel"
+ self.description = "replace current kernel using kexec"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(KexecAction())
+ # Add AutoLoginAction unconditionally as this action does nothing if
+ # the configuration does not contain 'auto_login'
+ self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+class KexecAction(Action):
+ """
+ The files need to have been downloaded by a previous test action.
+ This action calls kexec to load the kernel ,execute it and then
+ attempts to reestablish the shell connection after boot.
+ """
+
+ def __init__(self):
+ super(KexecAction, self).__init__()
+ self.name = "call-kexec"
+ self.summary = "attempt to kexec new kernel"
+ self.description = "call kexec with specified arguments"
+ self.command = ''
+ self.load_command = ''
+
+ def validate(self):
+ super(KexecAction, self).validate()
+ self.command = self.parameters.get('command', '/sbin/kexec')
+ self.load_command = self.command[:] # local copy for idempotency
+ self.command += ' -e'
+ if 'kernel' in self.parameters:
+ self.load_command += ' --load %s' % self.parameters['kernel']
+ if 'dtb' in self.parameters:
+ self.load_command += ' --dtb %s' % self.parameters['dtb']
+ if 'initrd' in self.parameters:
+ self.load_command += ' --initrd %s' % self.parameters['initrd']
+ if 'options' in self.parameters:
+ for option in self.parameters['options']:
+ self.load_command += " %s" % option
+ if self.load_command == '/sbin/kexec':
+ self.errors = "Default kexec handler needs at least a kernel to pass to the --load command"
+
+ def run(self, connection, max_end_time, args=None):
+ """
+ If kexec fails, there is no real chance at diagnostics because the device will be hung.
+ Get the output prior to the call, in case this helps after the job fails.
+ """
+ connection = super(KexecAction, self).run(connection, max_end_time, args)
+ if 'kernel-config' in self.parameters:
+ cmd = "zgrep -i kexec %s |grep -v '^#'" % self.parameters['kernel-config']
+ self.logger.debug("Checking for kexec: %s", cmd)
+ connection.sendline(cmd)
+ connection.sendline(self.load_command)
+ self.wait(connection)
+ connection.prompt = self.parameters['boot_message']
+ connection.sendline(self.command)
+ return connection
diff --git a/lava_dispatcher/actions/boot/lxc.py b/lava_dispatcher/actions/boot/lxc.py
new file mode 100644
index 000000000..c9484e9a6
--- /dev/null
+++ b/lava_dispatcher/actions/boot/lxc.py
@@ -0,0 +1,154 @@
+# Copyright (C) 2015 Linaro Limited
+#
+# Author: Senthil Kumaran S <senthil.kumaran@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 time
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+ JobError,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.actions.boot.environment import (
+ ExportDeviceEnvironment,
+)
+from lava_dispatcher.connections.lxc import (
+ ConnectLxc,
+)
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.utils.shell import infrastructure_error
+
+
+class BootLxc(Boot):
+ """
+ Attaches to the lxc container.
+ """
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(BootLxc, self).__init__(parent)
+ self.action = BootLxcAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'method' in parameters:
+ if parameters['method'] == 'lxc':
+ return True, 'accepted'
+ return False, '"method" was not in parameters or "method" was not "lxc"'
+
+
+class BootLxcAction(BootAction):
+ """
+ Provide for auto_login parameters in this boot stanza and re-establish the
+ connection after boot.
+ """
+ def __init__(self):
+ super(BootLxcAction, self).__init__()
+ self.name = "lxc-boot"
+ self.summary = "lxc boot"
+ self.description = "lxc boot into the system"
+
+ def validate(self):
+ super(BootLxcAction, self).validate()
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(LxcStartAction())
+ self.internal_pipeline.add_action(ConnectLxc())
+ # Skip AutoLoginAction unconditionally as this action tries to parse kernel message
+ # self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+class LxcStartAction(Action):
+ """
+ This action calls lxc-start to get into the system.
+ """
+
+ def __init__(self):
+ super(LxcStartAction, self).__init__()
+ self.name = "boot-lxc"
+ self.summary = "attempt to boot"
+ self.description = "boot into lxc container"
+ self.sleep = 10
+
+ def validate(self):
+ super(LxcStartAction, self).validate()
+ self.errors = infrastructure_error('lxc-start')
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(LxcStartAction, self).run(connection, max_end_time, args)
+ lxc_name = self.get_namespace_data(action='lxc-create-action', label='lxc', key='name')
+ lxc_cmd = ['lxc-start', '-n', lxc_name, '-d']
+ command_output = self.run_command(lxc_cmd)
+ if command_output and command_output is not '':
+ raise JobError("Unable to start lxc container: %s" %
+ command_output) # FIXME: JobError needs a unit test
+ lxc_cmd = ['lxc-info', '-sH', '-n', lxc_name]
+ self.logger.debug("Wait until '%s' state becomes RUNNING", lxc_name)
+ while True:
+ command_output = self.run_command(lxc_cmd, allow_fail=True)
+ if command_output and 'RUNNING' in command_output.strip():
+ break
+ time.sleep(self.sleep) # poll every 10 seconds.
+ self.logger.info("'%s' state is RUNNING", lxc_name)
+ # Check if LXC got an IP address so that we are sure, networking is
+ # enabled and the LXC can update or install software.
+ lxc_cmd = ['lxc-info', '-iH', '-n', lxc_name]
+ self.logger.debug("Wait until '%s' gets an IP address", lxc_name)
+ while True:
+ command_output = self.run_command(lxc_cmd, allow_fail=True)
+ if command_output:
+ break
+ time.sleep(self.sleep) # poll every 10 seconds.
+ self.logger.info("'%s' IP address is: '%s'", lxc_name,
+ command_output.strip())
+ return connection
+
+
+class LxcStopAction(Action):
+ """
+ This action calls lxc-stop to stop the container.
+ """
+
+ def __init__(self):
+ super(LxcStopAction, self).__init__()
+ self.name = "lxc-stop"
+ self.summary = "stop lxc"
+ self.description = "stop the lxc container"
+
+ def validate(self):
+ super(LxcStopAction, self).validate()
+ self.errors = infrastructure_error('lxc-stop')
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(LxcStopAction, self).run(connection, max_end_time, args)
+ lxc_name = self.get_namespace_data(action='lxc-create-action',
+ label='lxc', key='name')
+ lxc_cmd = ['lxc-stop', '-k', '-n', lxc_name]
+ command_output = self.run_command(lxc_cmd)
+ if command_output and command_output is not '':
+ raise JobError("Unable to stop lxc container: %s" %
+ command_output) # FIXME: JobError needs a unit test
+ return connection
diff --git a/lava_dispatcher/actions/boot/minimal.py b/lava_dispatcher/actions/boot/minimal.py
new file mode 100644
index 000000000..fd627252d
--- /dev/null
+++ b/lava_dispatcher/actions/boot/minimal.py
@@ -0,0 +1,81 @@
+# Copyright (C) 2017 Linaro Limited
+#
+# Author: Dean Arnold <dean.arnold@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>.
+
+from lava_dispatcher.action import (
+ Pipeline,
+)
+from lava_dispatcher.actions.boot import (
+ AutoLoginAction,
+ BootAction,
+ OverlayUnpack,
+)
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.shell import ExpectShellSession
+
+
+class Minimal(Boot):
+
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(Minimal, self).__init__(parent)
+ self.action = MinimalBoot()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'minimal' not in device['actions']['boot']['methods']:
+ return False, '"minimal" was not in device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" was not in parameters'
+ if parameters['method'] != 'minimal':
+ return False, '"method" was not "minimal"'
+ return True, 'accepted'
+
+
+class MinimalBoot(BootAction):
+
+ def __init__(self):
+ super(MinimalBoot, self).__init__()
+ self.name = 'minimal-boot'
+ self.description = "connect and reset device"
+ self.summary = "connect and reset device"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(ResetDevice())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(MinimalBoot, self).run(connection, max_end_time, args)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/pyocd.py b/lava_dispatcher/actions/boot/pyocd.py
new file mode 100644
index 000000000..fd88c33e4
--- /dev/null
+++ b/lava_dispatcher/actions/boot/pyocd.py
@@ -0,0 +1,133 @@
+# Copyright (C) 2016 Linaro Limited
+#
+# Author: Tyler Baker <tyler.baker@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>.
+
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+ JobError,
+)
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.utils.shell import infrastructure_error
+from lava_dispatcher.utils.strings import substitute
+
+
+class PyOCD(Boot):
+
+ compatibility = 4 # FIXME: change this to 5 and update test cases
+
+ def __init__(self, parent, parameters):
+ super(PyOCD, self).__init__(parent)
+ self.action = BootPyOCD()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'pyocd' not in device['actions']['boot']['methods']:
+ return False, '"pyocd" was not in the device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" was not in parameters'
+ if parameters['method'] != 'pyocd':
+ return False, '"method" was not "pyocd"'
+ if 'board_id' not in device:
+ return False, '"board_id" is not in the device configuration'
+ return True, 'accepted'
+
+
+class BootPyOCD(BootAction):
+
+ def __init__(self):
+ super(BootPyOCD, self).__init__()
+ self.name = 'boot-pyocd-image'
+ self.description = "boot pyocd image with retry"
+ self.summary = "boot pyocd image with retry"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootPyOCDRetry())
+
+
+class BootPyOCDRetry(RetryAction):
+
+ def __init__(self):
+ super(BootPyOCDRetry, self).__init__()
+ self.name = 'boot-pyocd-image'
+ self.description = "boot pyocd image using the command line interface"
+ self.summary = "boot pyocd image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(FlashPyOCDAction())
+ self.internal_pipeline.add_action(ConnectDevice())
+
+
+class FlashPyOCDAction(Action):
+
+ def __init__(self):
+ super(FlashPyOCDAction, self).__init__()
+ self.name = "flash-pyocd"
+ self.description = "flash pyocd to boot the image"
+ self.summary = "flash pyocd to boot the image"
+ self.base_command = []
+ self.exec_list = []
+
+ def validate(self):
+ super(FlashPyOCDAction, self).validate()
+ boot = self.job.device['actions']['boot']['methods']['pyocd']
+ pyocd_binary = boot['parameters']['command']
+ self.errors = infrastructure_error(pyocd_binary)
+ self.base_command = [pyocd_binary]
+ self.base_command.extend(boot['parameters'].get('options', []))
+ if self.job.device['board_id'] == '0000000000':
+ self.errors = "board_id unset"
+ substitutions = {}
+ self.base_command.extend(['--board', self.job.device['board_id']])
+ namespace = self.parameters['namespace']
+ for action in self.data[namespace]['download-action'].keys():
+ pyocd_full_command = []
+ image_arg = self.get_namespace_data(action='download-action', label=action, key='image_arg')
+ action_arg = self.get_namespace_data(action='download-action', label=action, key='file')
+ if image_arg:
+ if not isinstance(image_arg, str):
+ self.errors = "image_arg is not a string (try quoting it)"
+ continue
+ substitutions["{%s}" % action] = action_arg
+ pyocd_full_command.extend(self.base_command)
+ pyocd_full_command.extend(substitute([image_arg], substitutions))
+ self.exec_list.append(pyocd_full_command)
+ else:
+ pyocd_full_command.extend(self.base_command)
+ pyocd_full_command.extend([action_arg])
+ self.exec_list.append(pyocd_full_command)
+ if len(self.exec_list) < 1:
+ self.errors = "No PyOCD command to execute"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(FlashPyOCDAction, self).run(connection, max_end_time, args)
+ for pyocd_command in self.exec_list:
+ pyocd = ' '.join(pyocd_command)
+ self.logger.info("PyOCD command: %s", pyocd)
+ if not self.run_command(pyocd.split(' ')):
+ raise JobError("%s command failed" % (pyocd.split(' ')))
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/qemu.py b/lava_dispatcher/actions/boot/qemu.py
new file mode 100644
index 000000000..ae55cd750
--- /dev/null
+++ b/lava_dispatcher/actions/boot/qemu.py
@@ -0,0 +1,254 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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 os
+from lava_dispatcher.action import (
+ Pipeline,
+ Action,
+ JobError,
+)
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.actions.boot import BootAction
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import (
+ ExpectShellSession,
+ ShellCommand,
+ ShellSession
+)
+from lava_dispatcher.utils.shell import which
+from lava_dispatcher.utils.strings import substitute
+from lava_dispatcher.utils.constants import SYS_CLASS_KVM
+from lava_dispatcher.utils.network import dispatcher_ip
+from lava_dispatcher.utils.filesystem import debian_package_version
+from lava_dispatcher.actions.boot import AutoLoginAction, OverlayUnpack
+
+# pylint: disable=too-many-instance-attributes,too-many-branches
+
+
+# FIXME: decide if 'media: tmpfs' is necessary or remove from YAML. Only removable needs 'media'
+class BootQEMU(Boot):
+ """
+ The Boot method prepares the command to run on the dispatcher but this
+ command needs to start a new connection and then allow AutoLogin, if
+ enabled, and then expect a shell session which can be handed over to the
+ test method. self.run_command is a blocking call, so Boot needs to use
+ a direct spawn call via ShellCommand (which wraps pexpect.spawn) then
+ hand this pexpect wrapper to subsequent actions as a shell connection.
+ """
+
+ compatibility = 4
+
+ def __init__(self, parent, parameters):
+ super(BootQEMU, self).__init__(parent)
+ self.action = BootQEMUImageAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ methods = device['actions']['boot']['methods']
+ if 'qemu' not in methods and 'qemu-nfs' not in methods:
+ return False, '"qemu" or "qemu-nfs" was not in the device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" was not in parameters'
+ if parameters['method'] not in ['qemu', 'qemu-nfs', 'monitor']:
+ return False, '"method" was not "qemu" or "qemu-nfs"'
+ return True, 'accepted'
+
+
+class BootQEMUImageAction(BootAction):
+
+ def __init__(self):
+ super(BootQEMUImageAction, self).__init__()
+ self.name = 'boot-image-retry'
+ self.description = "boot image with retry"
+ self.summary = "boot with retry"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootQemuRetry())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+
+class BootQemuRetry(RetryAction):
+
+ def __init__(self):
+ super(BootQemuRetry, self).__init__()
+ self.name = 'boot-qemu-image'
+ self.description = "boot image using QEMU command line"
+ self.summary = "boot QEMU image"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(CallQemuAction())
+
+
+class CallQemuAction(Action):
+
+ def __init__(self):
+ super(CallQemuAction, self).__init__()
+ self.name = "execute-qemu"
+ self.description = "call qemu to boot the image"
+ self.summary = "execute qemu to boot the image"
+ self.sub_command = []
+ self.substitutions = {}
+ self.commands = []
+ self.methods = None
+ self.nfsrootfs = None
+
+ def validate(self):
+ super(CallQemuAction, self).validate()
+
+ # 'arch' must be defined in job definition context.
+ try:
+ if self.job.parameters['context']['arch'] not in \
+ self.job.device['available_architectures']:
+ self.errors = "Non existing architecture specified in context arch parameter. Please check the device configuration for available options."
+ return
+ except KeyError:
+ self.errors = "Arch parameter must be set in the context section. Please check the device configuration for available architectures."
+ return
+ if self.job.parameters['context']['arch'] in ['amd64', 'x86_64']:
+ self.logger.info("qemu-system-x86, installed at version: %s" %
+ debian_package_version(pkg='qemu-system-x86', split=False))
+ if self.job.parameters['context']['arch'] in ['arm64', 'arm', 'armhf', 'aarch64']:
+ self.logger.info("qemu-system-arm, installed at version: %s" %
+ debian_package_version(pkg='qemu-system-arm', split=False))
+
+ if self.parameters['method'] in ['qemu', 'qemu-nfs']:
+ if 'prompts' not in self.parameters:
+ if self.test_has_shell(self.parameters):
+ self.errors = "Unable to identify boot prompts from job definition."
+ self.methods = self.job.device['actions']['boot']['methods']
+ method = self.parameters['method']
+ boot = self.methods['qemu'] if 'qemu' in self.methods else self.methods['qemu-nfs']
+ try:
+ if 'parameters' not in boot or 'command' not in boot['parameters']:
+ self.errors = "Invalid device configuration - missing parameters"
+ elif not boot['parameters']['command']:
+ self.errors = "No QEMU binary command found - missing context."
+ qemu_binary = which(boot['parameters']['command'])
+ self.sub_command = [qemu_binary]
+ self.sub_command.extend(boot['parameters'].get('options', []))
+ self.sub_command.extend(
+ ['%s' % item for item in boot['parameters'].get('extra', [])])
+ except AttributeError as exc:
+ self.errors = "Unable to parse device options: %s %s" % (
+ exc, self.job.device['actions']['boot']['methods'][method])
+ except (KeyError, TypeError):
+ self.errors = "Invalid parameters for %s" % self.name
+ namespace = self.parameters['namespace']
+ for label in self.data[namespace]['download-action'].keys():
+ if label in ['offset', 'available_loops', 'uefi', 'nfsrootfs']:
+ continue
+ image_arg = self.get_namespace_data(action='download-action', label=label, key='image_arg')
+ action_arg = self.get_namespace_data(action='download-action', label=label, key='file')
+ if not image_arg or not action_arg:
+ self.errors = "Missing image_arg for %s. " % label
+ continue
+ self.substitutions["{%s}" % label] = action_arg
+ self.commands.append(image_arg)
+ self.substitutions["{NFS_SERVER_IP}"] = dispatcher_ip(self.job.parameters['dispatcher'])
+ self.sub_command.extend(substitute(self.commands, self.substitutions))
+ if not self.sub_command:
+ self.errors = "No QEMU command to execute"
+ uefi_dir = self.get_namespace_data(action='deployimages', label='image', key='uefi_dir')
+ if uefi_dir:
+ self.sub_command.extend(['-L', uefi_dir, '-monitor', 'none'])
+
+ # Check for enable-kvm command line option in device configuration.
+ if method not in self.job.device['actions']['boot']['methods']:
+ self.errors = "Unknown boot method '%s'" % method
+ return
+
+ options = self.job.device['actions']['boot']['methods'][method]['parameters']['options']
+ if "-enable-kvm" in options:
+ # Check if the worker has kvm enabled.
+ if not os.path.exists(SYS_CLASS_KVM):
+ self.errors = "Device configuration contains -enable-kvm option but kvm module is not enabled."
+
+ def run(self, connection, max_end_time, args=None):
+ """
+ CommandRunner expects a pexpect.spawn connection which is the return value
+ of target.device.power_on executed by boot in the old dispatcher.
+
+ In the new pipeline, the pexpect.spawn is a ShellCommand and the
+ connection is a ShellSession. CommandRunner inside the ShellSession
+ turns the ShellCommand into a runner which the ShellSession uses via ShellSession.run()
+ to run commands issued *after* the device has booted.
+ pexpect.spawn is one of the raw_connection objects for a Connection class.
+ """
+ # initialise the first Connection object, a command line shell into the running QEMU.
+ guest = self.get_namespace_data(action='apply-overlay-guest', label='guest', key='filename')
+ # check for NFS
+ if 'qemu-nfs' in self.methods and self.parameters.get('media', None) == 'nfs':
+ self.logger.debug("Adding NFS arguments to kernel command line.")
+ root_dir = self.get_namespace_data(action='extract-rootfs', label='file', key='nfsroot')
+ self.substitutions["{NFSROOTFS}"] = root_dir
+ params = self.methods['qemu-nfs']['parameters']['append']
+ # console=ttyAMA0 root=/dev/nfs nfsroot=10.3.2.1:/var/lib/lava/dispatcher/tmp/dirname,tcp,hard,intr ip=dhcp
+ append = [
+ 'console=%s' % params['console'],
+ 'root=/dev/nfs',
+ '%s rw' % substitute([params['nfsrootargs']], self.substitutions)[0],
+ "%s" % params['ipargs']
+ ]
+ self.sub_command.append('--append')
+ self.sub_command.append('"%s"' % ' '.join(append))
+ elif guest:
+ self.logger.info("Extending command line for qcow2 test overlay")
+ # interface is ide by default in qemu
+ interface = self.job.device['actions']['deploy']['methods']['image']['parameters']['guest'].get('interface', 'ide')
+ self.sub_command.append('-drive format=qcow2,file=%s,media=disk,if=%s' %
+ (os.path.realpath(guest), interface))
+ # push the mount operation to the test shell pre-command to be run
+ # before the test shell tries to execute.
+ shell_precommand_list = []
+ mountpoint = self.get_namespace_data(action='test', label='results', key='lava_test_results_dir')
+ uuid = '/dev/disk/by-uuid/%s' % self.get_namespace_data(action='apply-overlay-guest', label='guest', key='UUID')
+ shell_precommand_list.append('mkdir %s' % mountpoint)
+ # prepare_guestfs always uses ext2
+ shell_precommand_list.append('mount %s -t ext2 %s' % (uuid, mountpoint))
+ # debug line to show the effect of the mount operation
+ # also allows time for kernel messages from the mount operation to be processed.
+ shell_precommand_list.append('ls -la %s/bin/lava-test-runner' % mountpoint)
+ self.set_namespace_data(action='test', label='lava-test-shell', key='pre-command-list', value=shell_precommand_list)
+
+ self.logger.info("Boot command: %s", ' '.join(self.sub_command))
+ shell = ShellCommand(' '.join(self.sub_command), self.timeout, logger=self.logger)
+ if shell.exitstatus:
+ raise JobError("%s command exited %d: %s" % (self.sub_command, shell.exitstatus, shell.readlines()))
+ self.logger.debug("started a shell command")
+
+ shell_connection = ShellSession(self.job, shell)
+ shell_connection = super(CallQemuAction, self).run(shell_connection, max_end_time, args)
+
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=shell_connection)
+ return shell_connection
+
+
+# FIXME: implement a QEMU protocol to monitor VM boots
diff --git a/lava_dispatcher/actions/boot/ssh.py b/lava_dispatcher/actions/boot/ssh.py
new file mode 100644
index 000000000..a4eee6357
--- /dev/null
+++ b/lava_dispatcher/actions/boot/ssh.py
@@ -0,0 +1,292 @@
+# Copyright (C) 2015 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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>.
+
+# pylint: disable=too-many-return-statements,too-many-instance-attributes
+
+import os
+import yaml
+from lava_dispatcher.action import Action, LAVABug, Pipeline, JobError
+from lava_dispatcher.logical import Boot, RetryAction
+from lava_dispatcher.actions.boot import AutoLoginAction
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.utils.shell import infrastructure_error
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.connections.ssh import ConnectSsh
+from lava_dispatcher.protocols.multinode import MultinodeProtocol
+
+
+class SshLogin(Boot):
+ """
+ Ssh boot strategy is a login process, without actually booting a kernel
+ but still needs AutoLoginAction.
+ """
+
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(SshLogin, self).__init__(parent)
+ self.action = SshAction()
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'ssh' not in device['actions']['boot']['methods']:
+ return False, '"ssh" not in device configuration boot methods'
+ if 'ssh' not in parameters['method']:
+ return False, '"ssh" not in "method"'
+ return True, 'accepted'
+
+
+class SshAction(RetryAction):
+ """
+ Simple action to wrap AutoLoginAction and ExpectShellSession
+ """
+ def __init__(self):
+ super(SshAction, self).__init__()
+ self.name = "login-ssh"
+ self.summary = "login over ssh"
+ self.description = "connect over ssh and ensure a shell is found"
+ self.section = 'boot'
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ scp = Scp('overlay')
+ self.internal_pipeline.add_action(scp)
+ self.internal_pipeline.add_action(PrepareSsh())
+ self.internal_pipeline.add_action(ConnectSsh())
+ self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+ self.internal_pipeline.add_action(ScpOverlayUnpack())
+
+
+class Scp(ConnectSsh):
+ """
+ Use the SSH connection options to copy files over SSH
+ One action per scp operation, just as with download action
+ Needs the reference into the common data for each file to copy
+ This is a Deploy action. lava-start is managed by the protocol,
+ when this action starts, the device is in the "receiving" state.
+ """
+ def __init__(self, key):
+ super(Scp, self).__init__()
+ self.name = "scp-deploy"
+ self.summary = "scp over the ssh connection"
+ self.description = "copy a file to a known device using scp"
+ self.key = key
+ self.scp = []
+
+ def validate(self):
+ super(Scp, self).validate()
+ params = self._check_params()
+ self.errors = infrastructure_error('scp')
+ if 'ssh' not in self.job.device['actions']['deploy']['methods']:
+ self.errors = "Unable to use %s without ssh deployment" % self.name
+ if 'ssh' not in self.job.device['actions']['boot']['methods']:
+ self.errors = "Unable to use %s without ssh boot" % self.name
+ if self.get_namespace_data(action='prepare-scp-overlay', label="prepare-scp-overlay", key=self.key):
+ self.primary = False
+ elif 'host' not in self.job.device['actions']['deploy']['methods']['ssh']:
+ self.errors = "Invalid device or job configuration, missing host."
+ if not self.primary and len(
+ self.get_namespace_data(action='prepare-scp-overlay', label="prepare-scp-overlay", key=self.key)) != 1:
+ self.errors = "Invalid number of host_keys"
+ if self.primary:
+ host_address = self.job.device['actions']['deploy']['methods']['ssh']['host']
+ if not host_address:
+ self.errors = "Unable to retrieve ssh_host address for primary connection."
+ if 'port' in self.job.device['actions']['deploy']['methods']['ssh']:
+ port = str(self.job.device['actions']['deploy']['methods']['ssh']['port'])
+ if not port.isdigit():
+ self.errors = "Port was set but was not a digit"
+ if self.valid:
+ self.scp.append('scp')
+ if 'options' in params:
+ self.scp.extend(params['options'])
+
+ def run(self, connection, max_end_time, args=None):
+ path = self.get_namespace_data(action='prepare-scp-overlay', label='scp-deploy', key=self.key)
+ if not path:
+ error_msg = "%s: could not find details of '%s'" % (self.name, self.key)
+ self.logger.error(error_msg)
+ raise JobError(error_msg)
+
+ overrides = self.get_namespace_data(action='prepare-scp-overlay', label="prepare-scp-overlay", key=self.key)
+ if self.primary:
+ host_address = self.job.device['actions']['deploy']['methods']['ssh']['host']
+ else:
+ self.logger.info("Retrieving common data for prepare-scp-overlay using %s", ','.join(overrides))
+ host_address = str(self.get_namespace_data(action='prepare-scp-overlay', label="prepare-scp-overlay", key=overrides[0]))
+ self.logger.debug("Using common data for host: %s", host_address)
+ if not host_address:
+ error_msg = "%s: could not find host for deployment using %s" % (self.name, self.key)
+ self.logger.error(error_msg)
+ raise JobError(error_msg)
+
+ destination = "%s-%s" % (self.job.job_id, os.path.basename(path))
+ command = self.scp[:] # local copy
+ # add the argument for setting the port (-P port)
+ command.extend(self.scp_port)
+ connection = super(Scp, self).run(connection, max_end_time, args)
+ if self.identity_file:
+ command.extend(['-i', self.identity_file])
+ # add arguments to ignore host key checking of the host device
+ command.extend(['-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no'])
+ # add the local file as source
+ command.append(path)
+ command_str = " ".join(str(item) for item in command)
+ self.logger.info("Copying %s using %s to %s", self.key, command_str, host_address)
+ # add the remote as destination, with :/ top level directory
+ command.extend(["%s@%s:/%s" % (self.ssh_user, host_address, destination)])
+ self.logger.info(yaml.dump(command))
+ self.run_command(command)
+ connection = super(Scp, self).run(connection, max_end_time, args)
+ self.results = {'success': 'ssh deployment'}
+ self.set_namespace_data(action=self.name, label='scp-overlay-unpack', key='overlay', value=destination)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class PrepareSsh(Action):
+ """
+ Sets the host for the ConnectSsh
+ """
+ def __init__(self):
+ super(PrepareSsh, self).__init__()
+ self.name = "prepare-ssh"
+ self.summary = "set the host address of the ssh connection"
+ self.description = "determine which address to use for primary or secondary connections"
+ self.primary = False
+
+ def validate(self):
+ if 'parameters' in self.parameters and 'hostID' in self.parameters['parameters']:
+ self.set_namespace_data(action=self.name, label='ssh-connection', key='host', value=True)
+ else:
+ self.set_namespace_data(action=self.name, label='ssh-connection', key='host', value=False)
+ self.primary = True
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(PrepareSsh, self).run(connection, max_end_time, args)
+ if not self.primary:
+ host_data = self.get_namespace_data(
+ action=MultinodeProtocol.name,
+ label=MultinodeProtocol.name,
+ key=self.parameters['parameters']['hostID'])
+ if not host_data:
+ raise JobError("Unable to retrieve %s - missing ssh deploy?" % self.parameters['parameters']['hostID'])
+ self.set_namespace_data(
+ action=self.name,
+ label='ssh-connection',
+ key='host_address',
+ value=host_data[self.parameters['parameters']['host_key']]
+ )
+ return connection
+
+
+class ScpOverlayUnpack(Action):
+
+ def __init__(self):
+ super(ScpOverlayUnpack, self).__init__()
+ self.name = "scp-overlay-unpack"
+ self.summary = "unpack the overlay on the remote device"
+ self.description = "unpack the overlay over an existing ssh connection"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(ScpOverlayUnpack, self).run(connection, max_end_time, args)
+ if not connection:
+ raise LAVABug("Cannot unpack, no connection available.")
+ filename = self.get_namespace_data(action='scp-deploy', label='scp-overlay-unpack', key='overlay')
+ tar_flags = self.get_namespace_data(action='scp-overlay', label='scp-overlay', key='tar_flags')
+ cmd = "tar %s -C / -xzf /%s" % (tar_flags, filename)
+ connection.sendline(cmd)
+ self.wait(connection)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class Schroot(Boot):
+
+ def __init__(self, parent, parameters):
+ super(Schroot, self).__init__(parent)
+ self.action = SchrootAction()
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if 'actions' not in device or 'boot' not in device['actions']:
+ return False, '"boot" was not in the device configuration actions'
+ if 'methods' not in device['actions']['boot']:
+ return False, '"methods" was not in the device config'
+ if 'schroot' not in device['actions']['boot']['methods']:
+ return False, '"schroot" was not in the device configuration boot methods'
+ if 'method' not in parameters:
+ return False, '"method" was not in parameters'
+ if 'schroot' not in parameters['method']:
+ return False, '"method" was not "schroot"'
+ return True, 'accepted'
+
+
+class SchrootAction(Action):
+ """
+ Extends the login to enter an existing schroot as a new schroot session
+ using the current connection.
+ Does not rely on ssh
+ """
+ def __init__(self):
+ super(SchrootAction, self).__init__()
+ self.name = "schroot-login"
+ self.summary = "enter specified schroot"
+ self.description = "enter schroot using existing connection"
+ self.section = 'boot'
+ self.schroot = None
+ self.command = None
+
+ def validate(self):
+ """
+ The unit test skips if schroot is not installed, the action marks the
+ pipeline as invalid if schroot is not installed.
+ """
+ if 'schroot' not in self.parameters:
+ return
+ if 'schroot' not in self.job.device['actions']['boot']['methods']:
+ self.errors = "No schroot support in device boot methods"
+ return
+ self.errors = infrastructure_error('schroot')
+ # device parameters are for ssh
+ params = self.job.device['actions']['boot']['methods']
+ if 'command' not in params['schroot']:
+ self.errors = "Missing schroot command in device configuration"
+ return
+ if 'name' not in params['schroot']:
+ self.errors = "Missing schroot name in device configuration"
+ return
+ self.schroot = params['schroot']['name']
+ self.command = params['schroot']['command']
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ return connection
+ self.logger.info("Entering %s schroot", self.schroot)
+ connection.prompt_str = "(%s)" % self.schroot
+ connection.sendline(self.command)
+ self.wait(connection)
+ return connection
diff --git a/lava_dispatcher/actions/boot/strategies.py b/lava_dispatcher/actions/boot/strategies.py
new file mode 100644
index 000000000..83d0ef212
--- /dev/null
+++ b/lava_dispatcher/actions/boot/strategies.py
@@ -0,0 +1,42 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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>.
+
+# List just the subclasses supported for this base strategy
+# imported by the parser to populate the list of subclasses.
+
+# pylint: disable=unused-import
+
+from lava_dispatcher.actions.boot import SecondaryShell
+from lava_dispatcher.actions.boot.cmsis_dap import CMSIS
+from lava_dispatcher.actions.boot.dfu import DFU
+from lava_dispatcher.actions.boot.docker import BootDocker
+from lava_dispatcher.actions.boot.fastboot import BootFastboot
+from lava_dispatcher.actions.boot.grub import Grub, GrubSequence
+from lava_dispatcher.actions.boot.iso import BootIsoInstaller
+from lava_dispatcher.actions.boot.ipxe import IPXE
+from lava_dispatcher.actions.boot.kexec import BootKExec
+from lava_dispatcher.actions.boot.lxc import BootLxc
+from lava_dispatcher.actions.boot.minimal import Minimal
+from lava_dispatcher.actions.boot.pyocd import PyOCD
+from lava_dispatcher.actions.boot.qemu import BootQEMU
+from lava_dispatcher.actions.boot.ssh import SshLogin, Schroot
+from lava_dispatcher.actions.boot.u_boot import UBoot
+from lava_dispatcher.actions.boot.uefi import UefiShell
+from lava_dispatcher.actions.boot.uefi_menu import UefiMenu
diff --git a/lava_dispatcher/actions/boot/u_boot.py b/lava_dispatcher/actions/boot/u_boot.py
new file mode 100644
index 000000000..58fdd5881
--- /dev/null
+++ b/lava_dispatcher/actions/boot/u_boot.py
@@ -0,0 +1,272 @@
+# Copyright (C) 2014 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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>.
+
+# List just the subclasses supported for this base strategy
+# imported by the parser to populate the list of subclasses.
+
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ LAVABug,
+ Pipeline,
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.actions.boot import (
+ BootAction,
+ AutoLoginAction,
+ BootloaderCommandOverlay,
+ BootloaderCommandsAction,
+ BootloaderSecondaryMedia,
+ OverlayUnpack,
+)
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.connections.lxc import ConnectLxc
+from lava_dispatcher.connections.serial import ConnectDevice
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.utils.strings import map_kernel_uboot
+
+
+class UBoot(Boot):
+ """
+ The UBoot method prepares the command to run on the dispatcher but this
+ command needs to start a new connection and then interrupt u-boot.
+ An expect shell session can then be handed over to the UBootAction.
+ self.run_command is a blocking call, so Boot needs to use
+ a direct spawn call via ShellCommand (which wraps pexpect.spawn) then
+ hand this pexpect wrapper to subsequent actions as a shell connection.
+ """
+
+ compatibility = 1
+
+ def __init__(self, parent, parameters):
+ super(UBoot, self).__init__(parent)
+ self.action = UBootAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if parameters['method'] != 'u-boot':
+ return False, '"method" was not "u-boot"'
+ if 'commands' not in parameters:
+ raise ConfigurationError("commands not specified in boot parameters")
+ if 'u-boot' in device['actions']['boot']['methods']:
+ return True, 'accepted'
+ else:
+ return False, '"u-boot" was not in the device configuration boot methods'
+
+
+class UBootAction(BootAction):
+ """
+ Wraps the Retry Action to allow for actions which precede
+ the reset, e.g. Connect.
+ """
+ def __init__(self):
+ super(UBootAction, self).__init__()
+ self.name = "uboot-action"
+ self.description = "interactive uboot action"
+ self.summary = "pass uboot commands"
+
+ def validate(self):
+ super(UBootAction, self).validate()
+ if 'type' in self.parameters:
+ self.logger.warning("Specifying a type in the boot action is deprecated. "
+ "Please specify the kernel type in the deploy parameters.")
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ # customize the device configuration for this job
+ self.internal_pipeline.add_action(UBootSecondaryMedia())
+ self.internal_pipeline.add_action(BootloaderCommandOverlay())
+ self.internal_pipeline.add_action(ConnectDevice())
+ self.internal_pipeline.add_action(UBootRetry())
+
+
+class UBootRetry(BootAction):
+
+ def __init__(self):
+ super(UBootRetry, self).__init__()
+ self.name = "uboot-retry"
+ self.description = "interactive uboot retry action"
+ self.summary = "uboot commands with retry"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ # establish a new connection before trying the reset
+ self.internal_pipeline.add_action(ResetDevice())
+ self.internal_pipeline.add_action(UBootInterrupt())
+ self.internal_pipeline.add_action(BootloaderCommandsAction())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+ def validate(self):
+ super(UBootRetry, self).validate()
+ self.set_namespace_data(
+ action=self.name,
+ label='bootloader_prompt',
+ key='prompt',
+ value=self.job.device['actions']['boot']['methods']['u-boot']['parameters']['bootloader_prompt']
+ )
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(UBootRetry, self).run(connection, max_end_time, args)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class UBootInterrupt(Action):
+ """
+ Support for interrupting the bootloader.
+ """
+ def __init__(self):
+ super(UBootInterrupt, self).__init__()
+ self.name = "u-boot-interrupt"
+ self.description = "interrupt u-boot"
+ self.summary = "interrupt u-boot to get a prompt"
+
+ def validate(self):
+ super(UBootInterrupt, self).validate()
+ if self.job.device.connect_command is '':
+ self.errors = "Unable to connect to device %s"
+ device_methods = self.job.device['actions']['boot']['methods']
+ if 'bootloader_prompt' not in device_methods['u-boot']['parameters']:
+ self.errors = "Missing bootloader prompt for device"
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ raise LAVABug("%s started without a connection already in use" % self.name)
+ connection = super(UBootInterrupt, self).run(connection, max_end_time, args)
+ device_methods = self.job.device['actions']['boot']['methods']
+ # device is to be put into a reset state, either by issuing 'reboot' or power-cycle
+ interrupt_prompt = device_methods['u-boot']['parameters'].get('interrupt_prompt', self.job.device.get_constant('uboot-autoboot-prompt'))
+ # interrupt_char can actually be a sequence of ASCII characters - sendline does not care.
+ interrupt_char = device_methods['u-boot']['parameters'].get('interrupt_char', self.job.device.get_constant('uboot-interrupt-character'))
+ # vendor u-boot builds may require one or more control characters
+ interrupt_control_chars = device_methods['u-boot']['parameters'].get('interrupt_ctrl_list', [])
+ self.logger.debug("Changing prompt to '%s'", interrupt_prompt)
+ connection.prompt_str = interrupt_prompt
+ self.wait(connection)
+ if interrupt_control_chars:
+ for char in interrupt_control_chars:
+ connection.sendcontrol(char)
+ else:
+ connection.sendline(interrupt_char)
+ return connection
+
+
+class UBootSecondaryMedia(BootloaderSecondaryMedia):
+ """
+ Idempotent action which sets the static data only used when this is a boot of secondary media
+ already deployed.
+ """
+ def __init__(self):
+ super(UBootSecondaryMedia, self).__init__()
+ self.name = "uboot-from-media"
+ self.summary = "set uboot strings for deployed media"
+ self.description = "let uboot know where to find the kernel in the image on secondary media"
+
+ def validate(self):
+ if 'media' not in self.job.device.get('parameters', []):
+ return
+ media_keys = self.job.device['parameters']['media'].keys()
+ if self.parameters['commands'] not in list(media_keys):
+ return
+ super(UBootSecondaryMedia, self).validate()
+ if 'kernel_type' not in self.parameters:
+ self.errors = "Missing kernel_type for secondary media boot"
+ self.logger.debug("Mapping kernel_type: %s", self.parameters['kernel_type'])
+ bootcommand = map_kernel_uboot(self.parameters['kernel_type'], self.job.device.get('parameters', None))
+ self.logger.debug("Using bootcommand: %s", bootcommand)
+ self.set_namespace_data(
+ action='uboot-prepare-kernel', label='kernel-type',
+ key='kernel-type', value=self.parameters.get('kernel_type', ''))
+ self.set_namespace_data(
+ action='uboot-prepare-kernel', label='bootcommand', key='bootcommand', value=bootcommand)
+
+ media_params = self.job.device['parameters']['media'][self.parameters['commands']]
+ if self.get_namespace_data(action='storage-deploy', label='u-boot', key='device') not in media_params:
+ self.errors = "%s does not match requested media type %s" % (
+ self.get_namespace_data(
+ action='storage-deploy', label='u-boot', key='device'), self.parameters['commands']
+ )
+ if not self.valid:
+ return
+ self.set_namespace_data(
+ action=self.name,
+ label='uuid',
+ key='boot_part',
+ value='%s:%s' % (
+ media_params[self.get_namespace_data(action='storage-deploy', label='u-boot', key='device')]['device_id'],
+ self.parameters['boot_part']
+ )
+ )
+
+
+class UBootEnterFastbootAction(BootAction):
+
+ def __init__(self):
+ super(UBootEnterFastbootAction, self).__init__()
+ self.name = "uboot-enter-fastboot"
+ self.description = "interactive uboot enter fastboot action"
+ self.summary = "uboot commands to enter fastboot mode"
+ self.params = {}
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job,
+ parameters=parameters)
+ # establish a new connection before trying the reset
+ self.internal_pipeline.add_action(ResetDevice())
+ # need to look for Hit any key to stop autoboot
+ self.internal_pipeline.add_action(UBootInterrupt())
+ self.internal_pipeline.add_action(ConnectLxc())
+
+ def validate(self):
+ super(UBootEnterFastbootAction, self).validate()
+ if 'u-boot' not in self.job.device['actions']['deploy']['methods']:
+ self.errors = "uboot method missing"
+
+ self.params = self.job.device['actions']['deploy']['methods']['u-boot']['parameters']
+ if 'commands' not in self.job.device['actions']['deploy']['methods']['u-boot']['parameters']['fastboot']:
+ self.errors = "uboot command missing"
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(UBootEnterFastbootAction, self).run(connection,
+ max_end_time,
+ args)
+ connection.prompt_str = self.params['bootloader_prompt']
+ self.logger.debug("Changing prompt to %s", connection.prompt_str)
+ self.wait(connection)
+ i = 1
+ commands = self.job.device['actions']['deploy']['methods']['u-boot']['parameters']['fastboot']['commands']
+
+ for line in commands:
+ connection.sendline(line, delay=self.character_delay)
+ if i != (len(commands)):
+ self.wait(connection)
+ i += 1
+
+ return connection
diff --git a/lava_dispatcher/actions/boot/uefi.py b/lava_dispatcher/actions/boot/uefi.py
new file mode 100644
index 000000000..79247fe1a
--- /dev/null
+++ b/lava_dispatcher/actions/boot/uefi.py
@@ -0,0 +1,195 @@
+# Copyright (C) 2017 Linaro Limited
+#
+# Author: Dean Birch <dean.birch@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>.
+
+from lava_dispatcher.action import (
+ Pipeline
+)
+from lava_dispatcher.actions.boot import (
+ AutoLoginAction,
+ BootloaderCommandOverlay,
+ OverlayUnpack,
+ BootloaderCommandsAction,
+ BootAction)
+
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.actions.boot.uefi_menu import UEFIMenuInterrupt, UefiMenuSelector
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.menus.menus import MenuInterrupt, MenuConnect
+from lava_dispatcher.power import (
+ ResetDevice
+)
+from lava_dispatcher.shell import ExpectShellSession
+from lava_dispatcher.utils.constants import UEFI_LINE_SEPARATOR
+
+
+class UefiShell(Boot):
+
+ compatibility = 3
+
+ def __init__(self, parent, parameters):
+ super(UefiShell, self).__init__(parent)
+ self.action = UefiShellAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if parameters['method'] != 'uefi':
+ return False, '"method" was not "uefi"'
+ if 'uefi' in device['actions']['boot']['methods']:
+ params = device['actions']['boot']['methods']['uefi']['parameters']
+ if not params:
+ return False, 'there were no parameters in the "uefi" device configuration boot method'
+ if 'shell_interrupt_string' not in params:
+ return False, '"shell_interrupt_string" was not in the uefi device configuration boot method parameters'
+ if 'shell_interrupt_prompt' in params and 'bootloader_prompt' in params:
+ return True, 'accepted'
+ return False, 'missing or invalid parameters in the uefi device configuration boot methods'
+
+
+class UefiShellAction(BootAction):
+ def __init__(self):
+ super(UefiShellAction, self).__init__()
+ self.name = "uefi-shell-main-action"
+ self.description = "UEFI shell boot action"
+ self.summary = "run UEFI shell to system"
+ self.shell_menu = []
+
+ def _skip_menu(self, parameters):
+ # shell_menu can be set to '' to indicate there is no menu.
+ if 'shell_menu' in parameters:
+ self.shell_menu = parameters['shell_menu']
+ elif 'shell_menu' in self.job.device['actions']['boot']['methods']['uefi']['parameters']:
+ self.shell_menu = self.job.device['actions']['boot']['methods']['uefi']['parameters']['shell_menu']
+
+ if self.shell_menu and isinstance(self.shell_menu, str):
+ return False
+ return True
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(BootloaderCommandOverlay())
+ self.internal_pipeline.add_action(MenuConnect())
+ self.internal_pipeline.add_action(ResetDevice())
+ # Newer firmware often needs no menu interaction, just press to drop to shell
+ if not self._skip_menu(parameters):
+ # Some older firmware, UEFI Shell has to be selected from a menu.
+ self.internal_pipeline.add_action(UefiShellMenuInterrupt())
+ self.internal_pipeline.add_action(UefiShellMenuSelector())
+ self.internal_pipeline.add_action(UefiShellInterrupt())
+ self.internal_pipeline.add_action(UefiBootloaderCommandsAction())
+ if self.has_prompts(parameters):
+ self.internal_pipeline.add_action(AutoLoginAction())
+ if self.test_has_shell(parameters):
+ self.internal_pipeline.add_action(ExpectShellSession())
+ if 'transfer_overlay' in parameters:
+ self.internal_pipeline.add_action(OverlayUnpack())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(UefiShellAction, self).run(connection, max_end_time, args)
+ connection.raw_connection.linesep = UEFI_LINE_SEPARATOR
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+ def validate(self):
+ super(UefiShellAction, self).validate()
+ params = self.job.device['actions']['boot']['methods']['uefi']['parameters']
+ self.set_namespace_data(
+ action=self.name,
+ label='bootloader_prompt',
+ key='prompt',
+ value=params['bootloader_prompt']
+ )
+
+
+class UefiShellMenuInterrupt(UEFIMenuInterrupt):
+ def __init__(self):
+ super(UefiShellMenuInterrupt, self).__init__()
+ self.name = 'uefi-shell-menu-interrupt'
+ self.summary = 'interrupt default boot and to menu'
+ self.description = 'interrupt default boot and to menu'
+ # Take parameters from the uefi method, not uefi menu.
+ self.method = 'uefi'
+
+
+class UefiBootloaderCommandsAction(BootloaderCommandsAction):
+ """
+ Same as BootloaderCommandsAction, but uses UEFI_LINE_SEPARATOR.
+ """
+ def line_separator(self):
+ return UEFI_LINE_SEPARATOR
+
+
+class UefiShellInterrupt(MenuInterrupt):
+ """
+ Support for interrupting the UEFI menu and dropping to the shell.
+ """
+ def __init__(self):
+ super(UefiShellInterrupt, self).__init__()
+ self.name = 'uefi-shell-interrupt'
+ self.summary = 'first uefi interrupt'
+ self.description = 'interrupt uefi menu to get to a shell'
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ self.logger.debug("%s called without active connection", self.name)
+ return
+ connection = super(UefiShellInterrupt, self).run(connection, max_end_time, args)
+ # param keys already checked in accepts() classmethod
+ params = self.job.device['actions']['boot']['methods']['uefi']['parameters']
+ connection.prompt_str = params['shell_interrupt_prompt']
+ self.wait(connection)
+ connection.raw_connection.send(params['shell_interrupt_string'])
+ # now move on to bootloader prompt match
+ return connection
+
+
+class UefiShellMenuSelector(UefiMenuSelector):
+ """
+ Special version of the UefiMenuSelector configured to drop to the shell
+ """
+ def __init__(self):
+ super(UefiShellMenuSelector, self).__init__()
+ self.name = 'uefi-shell-menu-selector'
+ self.summary = 'use uefi menu to drop to shell'
+ self.description = 'select uefi menu items to drop to a uefi shell'
+ # Take parameters from the uefi method, not uefi menu.
+ self.method_name = 'uefi'
+ # Default menu command name: drop to shell
+ self.commands = 'shell'
+
+ def validate(self):
+ params = self.job.device['actions']['boot']['methods'][self.method_name]['parameters']
+ if 'shell_menu' in self.parameters:
+ self.commands = self.parameters['shell_menu']
+ elif 'shell_menu' in params:
+ self.commands = params['shell_menu']
+
+ if self.commands in self.job.device['actions']['boot']['methods'][self.method_name]:
+ self.items = self.job.device['actions']['boot']['methods'][self.method_name][self.commands]
+ else:
+ self.errors = "Missing menu commands for %s" % self.commands
+ if 'menu_boot_message' in params:
+ self.boot_message = params['menu_boot_message']
+ super(UefiShellMenuSelector, self).validate()
+ if 'menu_prompt' in params:
+ self.selector.prompt = params['menu_prompt']
diff --git a/lava_dispatcher/actions/boot/uefi_menu.py b/lava_dispatcher/actions/boot/uefi_menu.py
new file mode 100644
index 000000000..80dd88f1c
--- /dev/null
+++ b/lava_dispatcher/actions/boot/uefi_menu.py
@@ -0,0 +1,278 @@
+# Copyright (C) 2015 Linaro Limited
+#
+# Author: Neil Williams <neil.williams@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>.
+
+
+from lava_dispatcher.action import (
+ Action,
+ ConfigurationError,
+ InfrastructureError,
+ Pipeline,
+)
+from lava_dispatcher.menus.menus import (
+ SelectorMenuAction,
+ MenuConnect,
+ MenuInterrupt,
+ MenuReset
+)
+from lava_dispatcher.logical import Boot
+from lava_dispatcher.power import ResetDevice
+from lava_dispatcher.protocols.lxc import LxcProtocol
+from lava_dispatcher.utils.strings import substitute
+from lava_dispatcher.utils.network import dispatcher_ip
+from lava_dispatcher.actions.boot import BootAction, AutoLoginAction
+from lava_dispatcher.actions.boot.environment import ExportDeviceEnvironment
+from lava_dispatcher.utils.constants import (
+ DEFAULT_UEFI_LABEL_CLASS,
+ LINE_SEPARATOR,
+ UEFI_LINE_SEPARATOR,
+)
+
+
+class UefiMenu(Boot):
+ """
+ The UEFI Menu strategy selects the specified options
+ and inserts relevant strings into the UEFI menu instead
+ of issuing commands over a shell-like serial connection.
+ """
+
+ def __init__(self, parent, parameters):
+ super(UefiMenu, self).__init__(parent)
+ self.action = UefiMenuAction()
+ self.action.section = self.action_type
+ self.action.job = self.job
+ parent.add_action(self.action, parameters)
+
+ @classmethod
+ def accepts(cls, device, parameters):
+ if parameters['method'] != 'uefi-menu':
+ return False, '"method" was not "uefi-menu"'
+ if 'uefi-menu' in device['actions']['boot']['methods']:
+ params = device['actions']['boot']['methods']['uefi-menu']['parameters']
+ if 'interrupt_prompt' in params and 'interrupt_string' in params:
+ return True, 'accepted'
+ else:
+ return False, '"interrupt_prompt" or "interrupt_string" was not in the device configuration uefi-menu boot method parameters'
+ return False, '"uefi-menu" was not in the device configuration boot methods'
+
+
+class UEFIMenuInterrupt(MenuInterrupt):
+
+ def __init__(self):
+ super(UEFIMenuInterrupt, self).__init__()
+ self.name = 'uefi-menu-interrupt'
+ self.summary = 'interrupt for uefi menu'
+ self.description = 'interrupt for uefi menu'
+ self.params = None
+ self.method = 'uefi-menu'
+
+ def validate(self):
+ super(UEFIMenuInterrupt, self).validate()
+ self.params = self.job.device['actions']['boot']['methods'][self.method]['parameters']
+ if 'interrupt_prompt' not in self.params:
+ self.errors = "Missing interrupt prompt"
+ if 'interrupt_string' not in self.params:
+ self.errors = "Missing interrupt string"
+
+ def run(self, connection, max_end_time, args=None):
+ if not connection:
+ self.logger.debug("%s called without active connection", self.name)
+ return
+ connection = super(UEFIMenuInterrupt, self).run(connection, max_end_time, args)
+ connection.prompt_str = self.params['interrupt_prompt']
+ self.wait(connection)
+ connection.raw_connection.send(self.params['interrupt_string'])
+ return connection
+
+
+class UefiMenuSelector(SelectorMenuAction): # pylint: disable=too-many-instance-attributes
+
+ def __init__(self):
+ super(UefiMenuSelector, self).__init__()
+ self.name = 'uefi-menu-selector'
+ self.summary = 'select options in the uefi menu'
+ self.description = 'select specified uefi menu items'
+ self.selector.prompt = "Start:"
+ self.method_name = 'uefi-menu'
+ self.commands = []
+ self.boot_message = None
+
+ def validate(self):
+ """
+ Setup the items and pattern based on the parameters for this
+ specific action, then let the base class complete the validation.
+ """
+ # pick up the uefi-menu structure
+ params = self.job.device['actions']['boot']['methods'][self.method_name]['parameters']
+ if ('item_markup' not in params or
+ 'item_class' not in params or 'separator' not in params):
+ self.errors = "Missing device parameters for UEFI menu operations"
+ return
+ if 'commands' not in self.parameters and not self.commands:
+ self.errors = "Missing commands in action parameters"
+ return
+ # UEFI menu cannot support command lists (due to renumbering issues)
+ # but needs to ignore those which may exist for use with Grub later.
+ if not self.commands and isinstance(self.parameters['commands'], str):
+ if self.parameters['commands'] not in self.job.device['actions']['boot']['methods'][self.method_name]:
+ self.errors = "Missing commands for %s" % self.parameters['commands']
+ return
+ self.commands = self.parameters['commands']
+ if not self.commands:
+ # ignore self.parameters['commands'][]
+ return
+ # pick up the commands for the specific menu
+ self.selector.item_markup = params['item_markup']
+ self.selector.item_class = params['item_class']
+ self.selector.separator = params['separator']
+ if 'label_class' in params:
+ self.selector.label_class = params['label_class']
+ else:
+ # label_class is problematic via jinja and yaml templating.
+ self.selector.label_class = DEFAULT_UEFI_LABEL_CLASS
+ self.selector.prompt = params['bootloader_prompt'] # initial uefi menu prompt
+ if 'boot_message' in params and not self.boot_message:
+ self.boot_message = params['boot_message'] # final prompt
+ if not self.items:
+ # pick up the commands specific to the menu implementation
+ if self.commands not in self.job.device['actions']['boot']['methods'][self.method_name]:
+ self.errors = "No boot configuration called '%s' for boot method '%s'" % (
+ self.commands,
+ self.method_name
+ )
+ return
+ self.items = self.job.device['actions']['boot']['methods'][self.method_name][self.commands]
+ # set the line separator for the UEFI on this device
+ if 'line_separator' in self.parameters:
+ uefi_type = self.parameters['line_separator']
+ else:
+ uefi_type = self.job.device['actions']['boot']['methods'][self.method_name].get('line_separator', 'dos')
+ if uefi_type == 'dos':
+ self.line_sep = UEFI_LINE_SEPARATOR
+ elif uefi_type == 'unix':
+ self.line_sep = LINE_SEPARATOR
+ else:
+ self.errors = "Unrecognised line separator configuration."
+ super(UefiMenuSelector, self).validate()
+
+ def run(self, connection, max_end_time, args=None):
+ lxc_active = any([protocol for protocol in self.job.protocols if protocol.name == LxcProtocol.name])
+ if self.job.device.pre_os_command and not lxc_active:
+ self.logger.info("Running pre OS command.")
+ command = self.job.device.pre_os_command
+ if not self.run_command(command.split(' '), allow_silent=True):
+ raise InfrastructureError("%s failed" % command)
+ if not connection:
+ self.logger.debug("Existing connection in %s", self.name)
+ return connection
+ connection.prompt_str = self.selector.prompt
+ connection.raw_connection.linesep = self.line_sep
+ self.logger.debug("Looking for %s", self.selector.prompt)
+ self.wait(connection)
+ connection = super(UefiMenuSelector, self).run(connection, max_end_time, args)
+ if self.boot_message:
+ self.logger.debug("Looking for %s", self.boot_message)
+ connection.prompt_str = self.boot_message
+ self.wait(connection)
+ self.set_namespace_data(action='shared', label='shared', key='connection', value=connection)
+ return connection
+
+
+class UefiSubstituteCommands(Action):
+
+ def __init__(self):
+ super(UefiSubstituteCommands, self).__init__()
+ self.name = 'uefi-commands'
+ self.summary = 'substitute job values into uefi commands'
+ self.description = 'set job-specific variables into the uefi menu commands'
+ self.items = None
+
+ def validate(self):
+ super(UefiSubstituteCommands, self).validate()
+ if self.parameters['commands'] not in self.job.device['actions']['boot']['methods']['uefi-menu']:
+ self.errors = "Missing commands for %s" % self.parameters['commands']
+ self.items = self.job.device['actions']['boot']['methods']['uefi-menu'][self.parameters['commands']]
+ for item in self.items:
+ if 'select' not in item:
+ self.errors = "Invalid device configuration for %s: %s" % (self.name, item)
+
+ def run(self, connection, max_end_time, args=None):
+ connection = super(UefiSubstituteCommands, self).run(connection, max_end_time, args)
+ ip_addr = dispatcher_ip(self.job.parameters['dispatcher'])
+ substitution_dictionary = {
+ '{SERVER_IP}': ip_addr,
+ '{RAMDISK}': self.get_namespace_data(action='compress-ramdisk', label='file', key='ramdisk'),
+ '{KERNEL}': self.get_namespace_data(action='download-action', label='file', key='kernel'),
+ '{DTB}': self.get_namespace_data(action='download-action', label='file', key='dtb'),
+ 'TEST_MENU_NAME': "LAVA %s test image" % self.parameters['commands']
+ }
+ nfs_address = self.get_namespace_data(action='persistent-nfs-overlay', label='nfs_address', key='nfsroot')
+ nfs_root = self.get_namespace_data(action='download-action', label='file', key='nfsrootfs')
+ if nfs_root:
+ substitution_dictionary['{NFSROOTFS}'] = self.get_namespace_data(action='extract-rootfs', label='file', key='nfsroot')
+ substitution_dictionary['{NFS_SERVER_IP}'] = ip_addr
+ elif nfs_address:
+ substitution_dictionary['{NFSROOTFS}'] = nfs_address
+ substitution_dictionary['{NFS_SERVER_IP}'] = self.get_namespace_data(
+ action='persistent-nfs-overlay', label='nfs_address', key='serverip')
+ for item in self.items:
+ if 'enter' in item['select']:
+ item['select']['enter'] = substitute([item['select']['enter']], substitution_dictionary)[0]
+ if 'items' in item['select']:
+ # items is already a list, so pass without wrapping in []
+ item['select']['items'] = substitute(item['select']['items'], substitution_dictionary)
+ return connection
+
+
+class UefiMenuAction(BootAction):
+
+ def __init__(self):
+ super(UefiMenuAction, self).__init__()
+ self.name = 'uefi-menu-action'
+ self.summary = 'interact with uefi menu'
+ self.description = 'interrupt and select uefi menu items'
+ self.method = 'uefi-menu'
+
+ def validate(self):
+ super(UefiMenuAction, self).validate()
+ self.set_namespace_data(
+ action=self.name,
+ label='bootloader_prompt',
+ key='prompt',
+ value=self.job.device['actions']['boot']['methods'][self.method]['parameters']['bootloader_prompt']
+ )
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ if 'commands' in parameters and 'fastboot' in parameters['commands']:
+ self.internal_pipeline.add_action(UefiSubstituteCommands())
+ self.internal_pipeline.add_action(UEFIMenuInterrupt())
+ self.internal_pipeline.add_action(UefiMenuSelector())
+ self.internal_pipeline.add_action(MenuReset())
+ self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())
+ else:
+ self.internal_pipeline.add_action(UefiSubstituteCommands())
+ self.internal_pipeline.add_action(MenuConnect())
+ self.internal_pipeline.add_action(ResetDevice())
+ self.internal_pipeline.add_action(UEFIMenuInterrupt())
+ self.internal_pipeline.add_action(UefiMenuSelector())
+ self.internal_pipeline.add_action(MenuReset())
+ self.internal_pipeline.add_action(AutoLoginAction())
+ self.internal_pipeline.add_action(ExportDeviceEnvironment())