diff options
author | RĂ©mi Duraffort <remi.duraffort@linaro.org> | 2017-09-12 14:46:34 +0200 |
---|---|---|
committer | Neil Williams <neil.williams@linaro.org> | 2017-10-25 10:37:31 +0000 |
commit | ddf5d2fc956f3b261635cb9487a6bdf6f6cca1c7 (patch) | |
tree | 6c13da8c8f8b0946d2e493b3773efafbdd4e11e6 /lava_dispatcher/actions/boot | |
parent | 7dbdc2b65b039027540b77e986f62910d5620bc6 (diff) |
Remove v1 code
Change-Id: I098edd9edfeb968b303eaedc6afb6654e43b4b98
Diffstat (limited to 'lava_dispatcher/actions/boot')
-rw-r--r-- | lava_dispatcher/actions/boot/__init__.py | 615 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/cmsis_dap.py | 144 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/dfu.py | 159 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/docker.py | 140 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/environment.py | 69 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/fastboot.py | 356 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/grub.py | 313 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/ipxe.py | 165 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/iso.py | 212 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/kexec.py | 122 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/lxc.py | 154 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/minimal.py | 81 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/pyocd.py | 133 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/qemu.py | 254 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/ssh.py | 292 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/strategies.py | 42 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/u_boot.py | 272 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/uefi.py | 195 | ||||
-rw-r--r-- | lava_dispatcher/actions/boot/uefi_menu.py | 278 |
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()) |