summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNeil Williams <neil.williams@linaro.org>2014-10-28 11:25:29 (GMT)
committerNeil Williams <neil.williams@linaro.org>2014-11-21 15:08:52 (GMT)
commit7c2e28a832f4f1820d931717019fab4b0d4082b6 (patch)
tree2ae6a743daf9ae97741903d50dc9a40dc069c266
parent32962b3276f001ca640f8b07d0c6dcc70a065e23 (diff)
downloadlava-dispatcher-7c2e28a832f4f1820d931717019fab4b0d4082b6.tar.gz
lava-dispatcher-7c2e28a832f4f1820d931717019fab4b0d4082b6.tar.xz
Add AdjuvantAction prototype.
LAVA-1672: Allow a soft reboot success to skip the hard-reset PDU action. LAVA-1401: Port device configuration for panda-es, including local PDU commands. Change-Id: If19de3a8cf906ff1c1d72c60f53ce42518b47b17
-rw-r--r--lava_dispatcher/pipeline/action.py62
-rw-r--r--lava_dispatcher/pipeline/actions/boot/reset.py82
-rw-r--r--lava_dispatcher/pipeline/actions/boot/u_boot.py2
-rw-r--r--lava_dispatcher/pipeline/actions/deploy/tftp.py7
-rw-r--r--lava_dispatcher/pipeline/connection.py2
-rw-r--r--lava_dispatcher/pipeline/device.py3
-rw-r--r--lava_dispatcher/pipeline/device_types/panda-es.conf71
-rw-r--r--lava_dispatcher/pipeline/devices/panda-es-01.conf10
-rw-r--r--lava_dispatcher/pipeline/parser.py2
-rw-r--r--lava_dispatcher/pipeline/power.py196
-rw-r--r--lava_dispatcher/pipeline/shell.py6
-rw-r--r--lava_dispatcher/pipeline/test/sample_jobs/panda-nfs.yaml41
-rw-r--r--lava_dispatcher/pipeline/test/sample_jobs/panda-ramdisk.yaml42
-rw-r--r--lava_dispatcher/pipeline/test/test_defs.py2
-rw-r--r--lava_dispatcher/pipeline/test/test_job.py6
-rw-r--r--lava_dispatcher/pipeline/test/test_retries.py179
-rw-r--r--lava_dispatcher/pipeline/utils/shell.py9
17 files changed, 601 insertions, 121 deletions
diff --git a/lava_dispatcher/pipeline/action.py b/lava_dispatcher/pipeline/action.py
index b0b6fdd..dd3961f 100644
--- a/lava_dispatcher/pipeline/action.py
+++ b/lava_dispatcher/pipeline/action.py
@@ -712,37 +712,43 @@ class DiagnosticAction(Action): # pylint: disable=abstract-class-not-used
return connection
-class FinalizeAction(Action):
-
+class AdjuvantAction(Action):
+ """
+ Adjuvants are associative actions - partners and helpers which can be executed if
+ the initial Action determines a particular state.
+ Distinct from DiagnosticActions, Adjuvants execute within the normal flow of the
+ pipeline but support being skipped if the functionality is not required.
+ The default is that the Adjuvant is omitted. i.e. Requiring an adjuvant is an
+ indication that the device did not perform entirely as could be expected. One
+ example is when a soft reboot command fails, an Adjuvant can cause a power cycle
+ via the PDU.
+ """
def __init__(self):
- """
- The FinalizeAction is always added as the last Action in the top level pipeline by the parser.
- The tasks include finalising the connection (whatever is the last connection in the pipeline)
- and writing out the final pipeline structure containing the results as a logfile.
- """
- super(FinalizeAction, self).__init__()
- self.name = "finalize"
- self.summary = "finalize the job"
- self.description = "finish the process and cleanup"
+ super(AdjuvantAction, self).__init__()
+ self.adjuvant = False
+
+ @classmethod
+ def key(cls):
+ raise NotImplementedError("Base class has no key")
+
+ def validate(self):
+ super(AdjuvantAction, self).validate()
+ try:
+ self.key()
+ except NotImplementedError:
+ self.errors = "Adjuvant action without a key: %s" % self.name
def run(self, connection, args=None):
- """
- The pexpect.spawn here is the ShellCommand not the ShellSession connection object.
- So call the finalise() function of the connection which knows about the raw_connection inside.
- """
- if connection:
- connection.finalise()
- self.results = {'status': "Complete"}
- # FIXME: just write out a file, not put to stdout via logger.
- yaml_log = logging.getLogger("YAML")
- yaml_log.debug(yaml.dump(self.job.pipeline.describe()))
- # FIXME: detect a Cancel and set status as Cancel
- if self.job.pipeline.errors:
- self.results = {'status': "Incomplete"}
- yaml_log.debug("Status: Incomplete")
- yaml_log.debug(self.job.pipeline.errors)
- # from meliae import scanner
- # scanner.dump_all_objects('filename.json')
+ if not connection:
+ raise RuntimeError("Called %s without an active Connection" % self.name)
+ if not self.valid or self.key() not in self.data:
+ return connection
+ if self.data[self.key()]:
+ self.adjuvant = True
+ self.logger.debug("Adjuvant %s required" % self.name)
+ else:
+ self.logger.debug("Adjuvant %s skipped" % self.name)
+ return connection
class Deployment(object): # pylint: disable=abstract-class-not-used
diff --git a/lava_dispatcher/pipeline/actions/boot/reset.py b/lava_dispatcher/pipeline/actions/boot/reset.py
deleted file mode 100644
index d527ab8..0000000
--- a/lava_dispatcher/pipeline/actions/boot/reset.py
+++ /dev/null
@@ -1,82 +0,0 @@
-# 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.pipeline.action import Action, Pipeline
-from lava_dispatcher.pipeline.shell import ExpectShellSession
-
-
-class ResetDevice(Action):
- """
- Used within a RetryAction - first tries 'reboot' then
- tries PDU
- """
- # FIXME: extend to know the power state of the device
- # FIXME: extend to use PDU classes if reboot command fails
- def __init__(self):
- super(ResetDevice, self).__init__()
- self.name = "reboot-device"
- self.description = "reboot or power-cycle the device"
- self.summary = "reboot the device"
-
- def populate(self, parameters):
- self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
- # FIXME: decide how to use PDU if reboot fails.
- self.internal_pipeline.add_action(ExpectShellSession())
- self.internal_pipeline.add_action(RebootDevice())
-
-
-class RebootDevice(Action):
- """
- Issues the reboot command on the board
- """
- def __init__(self):
- super(RebootDevice, self).__init__()
- self.name = "soft-reboot"
- self.summary = "reboot command sent to device"
- self.description = "attempt to reboot the running device"
-
- def run(self, connection, args=None):
- if not connection:
- raise RuntimeError("Called %s without an active Connection" % self.name)
- connection.sendline("reboot")
- connection.wait()
- return connection
-
- # Looking for reboot messages or if they are missing, the U-Boot
- # message will also indicate the reboot is done.
- # match_id = connection.expect(
- # [pexpect.TIMEOUT, 'Restarting system.',
- # 'The system is going down for reboot NOW',
- # 'Will now restart', 'U-Boot'], timeout=120)
-
-
-class PDUReboot(Action):
- """
- Issues the PDU power cycle commands on the dispatcher
- """
- def __init__(self):
- super(PDUReboot, self).__init__()
- self.name = "pdu-reboot"
- self.summary = "hard reboot"
- self.description = "issue commands to PDU to power cycle device"
diff --git a/lava_dispatcher/pipeline/actions/boot/u_boot.py b/lava_dispatcher/pipeline/actions/boot/u_boot.py
index 09a5541..a70115f 100644
--- a/lava_dispatcher/pipeline/actions/boot/u_boot.py
+++ b/lava_dispatcher/pipeline/actions/boot/u_boot.py
@@ -34,7 +34,7 @@ from lava_dispatcher.pipeline.shell import (
ConnectDevice,
ExpectShellSession,
)
-from lava_dispatcher.pipeline.actions.boot.reset import ResetDevice
+from lava_dispatcher.pipeline.power import ResetDevice
from lava_dispatcher.pipeline.utils.constants import (
UBOOT_AUTOBOOT_PROMPT,
UBOOT_DEFAULT_CMD_TIMEOUT,
diff --git a/lava_dispatcher/pipeline/actions/deploy/tftp.py b/lava_dispatcher/pipeline/actions/deploy/tftp.py
index e55e85d..c538613 100644
--- a/lava_dispatcher/pipeline/actions/deploy/tftp.py
+++ b/lava_dispatcher/pipeline/actions/deploy/tftp.py
@@ -22,10 +22,11 @@
# imported by the parser to populate the list of subclasses.
import os
-from lava_dispatcher.pipeline.action import Pipeline, Deployment
+from lava_dispatcher.pipeline.action import Pipeline, Deployment, InfrastructureError
from lava_dispatcher.pipeline.actions.deploy import DeployAction
from lava_dispatcher.pipeline.actions.deploy.download import DownloaderAction
from lava_dispatcher.pipeline.actions.deploy.apply_overlay import PrepareOverlayTftp
+from lava_dispatcher.pipeline.utils.shell import which
from lava_dispatcher.pipeline.utils.filesystem import mkdtemp
@@ -96,6 +97,10 @@ class TftpAction(DeployAction):
if self.suffix:
self.data[self.name].setdefault('suffix', self.suffix)
self.data[self.name].setdefault('suffix', os.path.basename(self.tftp_dir))
+ try:
+ which("in.tftpd")
+ except InfrastructureError as exc:
+ self.errors = exc
def populate(self, parameters):
self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
diff --git a/lava_dispatcher/pipeline/connection.py b/lava_dispatcher/pipeline/connection.py
index a62c95b..5cb794a 100644
--- a/lava_dispatcher/pipeline/connection.py
+++ b/lava_dispatcher/pipeline/connection.py
@@ -190,6 +190,8 @@ class CommandRunner(object):
self.match = None
def change_prompt(self, string):
+ yaml_log = logging.getLogger("YAML")
+ yaml_log.debug("Changing prompt to %s" % string)
self._prompt_str = string
def wait_for_prompt(self, timeout=-1):
diff --git a/lava_dispatcher/pipeline/device.py b/lava_dispatcher/pipeline/device.py
index 2737ee7..7d7e96e 100644
--- a/lava_dispatcher/pipeline/device.py
+++ b/lava_dispatcher/pipeline/device.py
@@ -91,9 +91,10 @@ class NewDevice(object):
# FIXME: system paths need to be finalised.
# possible default system_config_path = "/usr/share/lava-dispatcher"
# FIXME: change to default-config once the old files are converted.
+ # FIXME: need a temporary or at least separate location in /etc/ and override support
default_config_path = os.path.join(os.path.dirname(__file__))
if not os.path.exists(os.path.join(default_config_path, 'devices', "%s.conf" % target)):
- raise RuntimeError("Unable to use new devices: %s" % default_config_path)
+ raise RuntimeError("Unable to find device: %s in %s" % (target, default_config_path))
defaults = NewDeviceDefaults()
# parameters dict will update if new settings are found, so repeat for customisation files when those exist
diff --git a/lava_dispatcher/pipeline/device_types/panda-es.conf b/lava_dispatcher/pipeline/device_types/panda-es.conf
new file mode 100644
index 0000000..151d500
--- /dev/null
+++ b/lava_dispatcher/pipeline/device_types/panda-es.conf
@@ -0,0 +1,71 @@
+# replacement device_type config for the beaglebone-black type
+
+# FIXME: remove redundancy and streamline
+
+parameters:
+ bootm:
+ kernel: '0x80200000'
+ ramdisk: '0x81600000'
+ dtb: '0x815f0000'
+ bootz:
+ kernel: '0x82000000'
+ ramdisk: '0x81000000'
+ dtb: '0x81f00000'
+
+actions:
+ deploy:
+ # list of deployment methods which this device supports
+ methods:
+ # - image # not ready yet
+ - tftp
+
+ boot:
+ # list of boot methods which this device supports.
+ methods:
+ - u-boot:
+ parameters:
+ bootloader_prompt: Panda
+ send_char: False
+ # interrupt: # character needed to interrupt u-boot, single whitespace by default
+ # method specific stanza
+ oe:
+ commands:
+ - mmc init
+ - mmc part 0
+ - setenv initrd_high "0xffffffff"
+ - setenv fdt_high "0xffffffff"
+ - setenv bootcmd 'fatload mmc 0:3 0x80200000 uImage; bootm 0x80200000'
+ - setenv bootargs ' console=tty0 console=ttyO2,115200n8
+ root=/dev/mmcblk0p5 rootwait ro earlyprintk fixrtc nocompcache
+ vram=48M omapfb.vram=0:24M'
+ - boot
+ nfs:
+ commands:
+ - setenv autoload no
+ - setenv initrd_high '0xffffffff'
+ - setenv fdt_high '0xffffffff'
+ - setenv kernel_addr_r '{KERNEL_ADDR}'
+ - setenv initrd_addr_r '{RAMDISK_ADDR}'
+ - setenv fdt_addr_r '{DTB_ADDR}'
+ - setenv loadkernel 'tftp ${kernel_addr_r} {KERNEL}'
+ - setenv loadinitrd 'tftp ${initrd_addr_r} {RAMDISK}; setenv initrd_size ${filesize}'
+ - setenv loadfdt 'tftp ${fdt_addr_r} {DTB}'
+ - setenv nfsargs 'setenv bootargs console=ttyO2,115200n8 root=/dev/nfs rw nfsroot={SERVER_IP}&#58;{NFSROOTFS}
+ ip=dhcp fixrtc nocompcache vram=48M omapfb.vram=0:24M mem=456M@0x80000000 mem=512M@0xA0000000 init=init'
+ - setenv bootcmd 'usb start; dhcp; setenv serverip {SERVER_IP}; run loadkernel; run loadinitrd; run loadfdt; run nfsargs; {BOOTX}'
+ - boot
+ ramdisk:
+ commands:
+ - setenv autoload no
+ - setenv initrd_high '0xffffffff'
+ - setenv fdt_high '0xffffffff'
+ - setenv kernel_addr_r '{KERNEL_ADDR}'
+ - setenv initrd_addr_r '{RAMDISK_ADDR}'
+ - setenv fdt_addr_r '{DTB_ADDR}'
+ - setenv loadkernel 'tftp ${kernel_addr_r} {KERNEL}'
+ - setenv loadinitrd 'tftp ${initrd_addr_r} {RAMDISK}; setenv initrd_size ${filesize}'
+ - setenv loadfdt 'tftp ${fdt_addr_r} {DTB}'
+ - setenv bootargs 'console=ttyO2,115200n8 root=/dev/ram0 fixrtc nocompcache vram=48M omapfb.vram=0:24M
+ mem=456M@0x80000000 mem=512M@0xA0000000 ip=dhcp init=init'
+ - setenv bootcmd 'usb start; dhcp; setenv serverip {SERVER_IP}; run loadkernel; run loadinitrd; run loadfdt; {BOOTX}'
+ - boot
diff --git a/lava_dispatcher/pipeline/devices/panda-es-01.conf b/lava_dispatcher/pipeline/devices/panda-es-01.conf
new file mode 100644
index 0000000..527b484
--- /dev/null
+++ b/lava_dispatcher/pipeline/devices/panda-es-01.conf
@@ -0,0 +1,10 @@
+# device specific configuration
+# test device only - remove and generate inside the test suite instead.
+
+device_type: panda-es
+hostname: panda-es-01
+commands:
+ connect: telnet droopy 4001
+ hard_reset: /usr/bin/pduclient --daemon snagglepuss --hostname pdu --command reboot --port 08
+ power_off: /usr/bin/pduclient --daemon snagglepuss --hostname pdu --command off --port 08
+ power_on: /usr/bin/pduclient --daemon snagglepuss --hostname pdu --command on --port 08
diff --git a/lava_dispatcher/pipeline/parser.py b/lava_dispatcher/pipeline/parser.py
index 7fe41f3..2ee7a17 100644
--- a/lava_dispatcher/pipeline/parser.py
+++ b/lava_dispatcher/pipeline/parser.py
@@ -27,11 +27,11 @@ from lava_dispatcher.pipeline.action import (
Action,
Deployment,
Boot,
- FinalizeAction,
LavaTest,
)
from lava_dispatcher.pipeline.actions.commands import CommandsAction # pylint: disable=unused-import
from lava_dispatcher.pipeline.deployment_data import get_deployment_data
+from lava_dispatcher.pipeline.power import FinalizeAction
# Bring in the strategy subclass lists, ignore pylint warnings.
import lava_dispatcher.pipeline.actions.deploy.strategies # pylint: disable=unused-import
import lava_dispatcher.pipeline.actions.boot.strategies # pylint: disable=unused-import
diff --git a/lava_dispatcher/pipeline/power.py b/lava_dispatcher/pipeline/power.py
new file mode 100644
index 0000000..fd5701c
--- /dev/null
+++ b/lava_dispatcher/pipeline/power.py
@@ -0,0 +1,196 @@
+# 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.
+
+
+import yaml
+import logging
+from lava_dispatcher.pipeline.action import (
+ Action,
+ Pipeline,
+ AdjuvantAction,
+ InfrastructureError,
+ JobError,
+ TestError,
+)
+from lava_dispatcher.pipeline.shell import ExpectShellSession
+
+
+class ResetDevice(Action):
+ """
+ Used within a RetryAction - first tries 'reboot' then
+ tries PDU
+ """
+ # FIXME: extend to know the power state of the device
+ def __init__(self):
+ super(ResetDevice, self).__init__()
+ self.name = "reboot-device"
+ self.description = "reboot or power-cycle the device"
+ self.summary = "reboot the device"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ # self.internal_pipeline.add_action(ExpectShellSession())
+ self.internal_pipeline.add_action(RebootDevice())
+ self.internal_pipeline.add_action(PDUReboot())
+
+
+class RebootDevice(Action):
+ """
+ Issues the reboot command on the board
+ """
+ def __init__(self):
+ super(RebootDevice, self).__init__()
+ self.name = "soft-reboot"
+ self.summary = "reboot command sent to device"
+ self.description = "attempt to reboot the running device"
+
+ def run(self, connection, args=None):
+ if not connection:
+ raise RuntimeError("Called %s without an active Connection" % self.name)
+ connection.prompt_str = 'The system is going down for reboot NOW'
+ connection.sendline("reboot")
+ self.results = {'status': "success"}
+ try:
+ connection.wait()
+ except TestError:
+ self.results = {'status': "failed"}
+ self.data[PDUReboot.key()] = True
+ connection.prompt_str = self.parameters['u-boot']['parameters']['bootloader_prompt']
+ return connection
+
+
+class PDUReboot(AdjuvantAction):
+ """
+ Issues the PDU power cycle command on the dispatcher
+ Raises InfrastructureError if either the command fails
+ (pdu client reports error) or if the connection times out
+ waiting for the device to reset.
+ """
+ def __init__(self):
+ self.name = PDUReboot.key()
+ super(PDUReboot, self).__init__()
+ self.summary = "hard reboot"
+ self.description = "issue commands to a PDU to power cycle a device"
+ self.command = None
+
+ @classmethod
+ def key(cls):
+ return 'pdu_reboot'
+
+ def validate(self):
+ super(PDUReboot, self).validate()
+ if 'commands' not in self.job.device.parameters:
+ return # no PDU commands
+ if 'hard_reset' not in self.job.device.parameters['commands']:
+ return # class will do nothing
+ self.command = self.job.device.parameters['commands']['hard_reset']
+
+ def run(self, connection, args=None):
+ connection = super(PDUReboot, self).run(connection, args)
+ if not self.adjuvant:
+ self.logger.debug("Skipping adjuvant %s" % self.key())
+ return connection
+ if not self._run_command(self.command.split(' ')):
+ raise InfrastructureError("%s failed" % self.command)
+ try:
+ connection.wait()
+ except TestError:
+ raise InfrastructureError("%s failed to reset device" % self.key())
+ self.data[PDUReboot.key()] = False
+ self.results = {'status': 'success'}
+ return connection
+
+
+class PowerOn(Action):
+ """
+ Issues the power on command via the PDU
+ """
+ def __init__(self):
+ super(PowerOn, self).__init__()
+ self.name = "power_on"
+ self.summary = "send power_on command"
+ self.description = "supply power to device"
+
+ def run(self, connection, args=None):
+ if 'power_on' in self.job.device.parameters['commands']:
+ command = self.job.device.parameters['commands']['power_on']
+ if not self._run_command(command.split(' ')):
+ raise InfrastructureError("%s command failed" % command)
+ return connection
+
+
+class PowerOff(Action):
+ """
+ Turns power off at the end of a job
+ """
+ def __init__(self):
+ super(PowerOff, self).__init__()
+ self.name = "power_off"
+ self.summary = "send power_off command"
+ self.description = "discontinue power to device"
+
+ def run(self, connection, args=None):
+ if 'power_off' in self.job.device.parameters['commands']:
+ command = self.job.device.parameters['commands']['power_off']
+ if not self._run_command(command.split(' ')):
+ raise InfrastructureError("%s command failed" % command)
+ return connection
+
+
+class FinalizeAction(Action):
+
+ def __init__(self):
+ """
+ The FinalizeAction is always added as the last Action in the top level pipeline by the parser.
+ The tasks include finalising the connection (whatever is the last connection in the pipeline)
+ and writing out the final pipeline structure containing the results as a logfile.
+ """
+ super(FinalizeAction, self).__init__()
+ self.name = "finalize"
+ self.summary = "finalize the job"
+ self.description = "finish the process and cleanup"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(job=self.job, parent=self, parameters=parameters)
+ self.internal_pipeline.add_action(PowerOff())
+
+ def run(self, connection, args=None):
+ """
+ The pexpect.spawn here is the ShellCommand not the ShellSession connection object.
+ So call the finalise() function of the connection which knows about the raw_connection inside.
+ """
+ connection = super(FinalizeAction, self).run(connection, args)
+ if connection:
+ connection.finalise()
+ yaml_log = logging.getLogger("YAML")
+ # FIXME: detect a Cancel and set status as Cancel
+ if self.job.pipeline.errors:
+ self.results = {'status': "Incomplete"}
+ yaml_log.debug("Status: Incomplete")
+ yaml_log.debug(self.job.pipeline.errors)
+ else:
+ self.results = {'status': "Complete"}
+ with open("%s/results.yaml" % self.job.parameters['output_dir'], 'w') as results:
+ results.write(yaml.dump(self.job.pipeline.describe()))
+ # from meliae import scanner
+ # scanner.dump_all_objects('filename.json')
diff --git a/lava_dispatcher/pipeline/shell.py b/lava_dispatcher/pipeline/shell.py
index dcd39d0..c797cb4 100644
--- a/lava_dispatcher/pipeline/shell.py
+++ b/lava_dispatcher/pipeline/shell.py
@@ -245,6 +245,10 @@ class ConnectDevice(Action):
raise JobError("%s command exited %d: %s" % (command, shell.exitstatus, shell.readlines()))
connection = ShellSession(self.job, shell)
connection.prompt_str = self.job.device.parameters['test_image_prompts']
- connection.wait()
+ # FIXME: some tests need this, some do not.
+ try:
+ connection.wait()
+ except TestError:
+ self.errors = "%s wait expired" % self.name
self.logger.debug("matched %s" % connection.match)
return connection
diff --git a/lava_dispatcher/pipeline/test/sample_jobs/panda-nfs.yaml b/lava_dispatcher/pipeline/test/sample_jobs/panda-nfs.yaml
new file mode 100644
index 0000000..93e98d1
--- /dev/null
+++ b/lava_dispatcher/pipeline/test/sample_jobs/panda-nfs.yaml
@@ -0,0 +1,41 @@
+# Sample JOB definition for a u-boot job
+
+device_type: panda
+
+job_name: uboot-panda-nfs
+job_timeout: 15m # timeout for the whole job (default: ??h)
+action_timeout: 5m # default timeout applied for each action; can be overriden in the action itself (default: ?h)
+priority: medium
+
+actions:
+
+ # needs to be a list of hashes to retain the order
+ - deploy:
+ timeout: 2m
+ to: tftp
+ kernel: http://images.validation.linaro.org/functional-test-images/panda/uImage
+ nfsrootfs: file:///home/linaro/chroots/jessie.tar.gz
+ dtb: http://images.validation.linaro.org/functional-test-images/panda/omap4-panda-es.dtb
+
+ - boot:
+ method: u-boot
+ commands: nfs
+ type: bootm
+
+ - test:
+ failure_retry: 3
+ name: kvm-basic-singlenode # is not present, use "test $N"
+ # only s, m & h are supported.
+ timeout: 5m # uses install:deps, so takes longer than singlenode01
+ definitions:
+ - repository: git://git.linaro.org/qa/test-definitions.git
+ from: git
+ path: ubuntu/smoke-tests-basic.yaml
+ name: smoke-tests
+ - repository: http://git.linaro.org/lava-team/lava-functional-tests.git
+ from: git
+ path: lava-test-shell/single-node/singlenode03.yaml
+ name: singlenode-advanced
+
+ - submit_results:
+ stream: /anonymous/codehelp/
diff --git a/lava_dispatcher/pipeline/test/sample_jobs/panda-ramdisk.yaml b/lava_dispatcher/pipeline/test/sample_jobs/panda-ramdisk.yaml
new file mode 100644
index 0000000..5b0c14b
--- /dev/null
+++ b/lava_dispatcher/pipeline/test/sample_jobs/panda-ramdisk.yaml
@@ -0,0 +1,42 @@
+# Sample JOB definition for a u-boot job
+
+device_type: panda
+
+job_name: uboot-pipeline
+job_timeout: 15m # timeout for the whole job (default: ??h)
+action_timeout: 5m # default timeout applied for each action; can be overriden in the action itself (default: ?h)
+priority: medium
+
+actions:
+
+ # needs to be a list of hashes to retain the order
+ - deploy:
+ timeout: 2m
+ to: tftp
+ kernel: http://images.validation.linaro.org/functional-test-images/panda/uImage
+ ramdisk: http://images.validation.linaro.org/functional-test-images/common/linaro-image-minimal-initramfs-genericarmv7a.cpio.gz.u-boot
+ ramdisk-type: u-boot
+ dtb: http://images.validation.linaro.org/functional-test-images/panda/omap4-panda-es.dtb
+
+ - boot:
+ method: u-boot
+ commands: ramdisk
+ type: bootm
+
+ - test:
+ failure_retry: 3
+ name: kvm-basic-singlenode # is not present, use "test $N"
+ # only s, m & h are supported.
+ timeout: 5m # uses install:deps, so takes longer than singlenode01
+ definitions:
+ - repository: git://git.linaro.org/qa/test-definitions.git
+ from: git
+ path: ubuntu/smoke-tests-basic.yaml
+ name: smoke-tests
+ - repository: http://git.linaro.org/lava-team/lava-functional-tests.git
+ from: git
+ path: lava-test-shell/single-node/singlenode02.yaml
+ name: singlenode-intermediate
+
+ - submit_results:
+ stream: /anonymous/codehelp/
diff --git a/lava_dispatcher/pipeline/test/test_defs.py b/lava_dispatcher/pipeline/test/test_defs.py
index 39f3e49..c845ea9 100644
--- a/lava_dispatcher/pipeline/test/test_defs.py
+++ b/lava_dispatcher/pipeline/test/test_defs.py
@@ -22,7 +22,7 @@ import os
import glob
import stat
import unittest
-from lava_dispatcher.pipeline.action import FinalizeAction
+from lava_dispatcher.pipeline.power import FinalizeAction
from lava_dispatcher.pipeline.actions.submit import SubmitResultsAction
from lava_dispatcher.pipeline.actions.test.shell import TestShellRetry
from lava_dispatcher.pipeline.test.test_basic import Factory
diff --git a/lava_dispatcher/pipeline/test/test_job.py b/lava_dispatcher/pipeline/test/test_job.py
index 85e288b..45d1882 100644
--- a/lava_dispatcher/pipeline/test/test_job.py
+++ b/lava_dispatcher/pipeline/test/test_job.py
@@ -170,7 +170,7 @@ class TestKVMBasicDeploy(unittest.TestCase):
apply_overlay = None
overlay = None
unmount = None
- self.assertEqual(len(self.job.pipeline.describe().values()), 31) # this will keep changing until KVM is complete.
+ self.assertEqual(len(self.job.pipeline.describe().values()), 32) # this will keep changing until KVM is complete.
for action in self.job.pipeline.actions:
if isinstance(action, DeployAction):
self.assertEqual(len(action.pipeline.children[action.pipeline]), 6)
@@ -356,7 +356,7 @@ class TestKVMQcow2Deploy(unittest.TestCase):
apply_overlay = None
overlay = None
unmount = None
- self.assertEqual(len(self.job.pipeline.describe().values()), 32) # this will keep changing until KVM is complete.
+ self.assertEqual(len(self.job.pipeline.describe().values()), 33) # this will keep changing until KVM is complete.
for action in self.job.pipeline.actions:
if isinstance(action, DeployAction):
self.assertEqual(len(action.pipeline.children[action.pipeline]), 7)
@@ -481,7 +481,7 @@ class TestKVMDownloadLocalDeploy(unittest.TestCase):
apply_overlay = None
overlay = None
unmount = None
- self.assertEqual(len(self.job.pipeline.describe().values()), 31) # this will keep changing until KVM is complete.
+ self.assertEqual(len(self.job.pipeline.describe().values()), 32) # this will keep changing until KVM is complete.
for action in self.job.pipeline.actions:
if isinstance(action, DeployAction):
self.assertEqual(len(action.pipeline.children[action.pipeline]), 6)
diff --git a/lava_dispatcher/pipeline/test/test_retries.py b/lava_dispatcher/pipeline/test/test_retries.py
index 4d09eff..27f0117 100644
--- a/lava_dispatcher/pipeline/test/test_retries.py
+++ b/lava_dispatcher/pipeline/test/test_retries.py
@@ -22,6 +22,7 @@
import unittest
from lava_dispatcher.pipeline.action import (
Action,
+ AdjuvantAction,
Pipeline,
RetryAction,
DiagnosticAction,
@@ -221,3 +222,181 @@ class TestAction(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertIsNone(fakepipeline.validate_actions())
with self.assertRaises(JobError):
fakepipeline.run_actions(None, None)
+
+
+class TestAdjuvant(unittest.TestCase): # pylint: disable=too-many-public-methods
+
+ class FakeJob(Job):
+
+ def __init__(self, parameters):
+ super(TestAdjuvant.FakeJob, self).__init__(parameters)
+
+ def validate(self, simulate=False):
+ self.pipeline.validate_actions()
+
+ class FakeDeploy(object):
+ """
+ Derived from object, *not* Deployment as this confuses python -m unittest discover
+ - leads to the FakeDeploy being called instead.
+ """
+ def __init__(self, parent):
+ self.__parameters__ = {}
+ self.pipeline = parent
+ self.job = parent.job
+ self.action = TestAdjuvant.FakeAction()
+
+ class FakeConnection(object):
+ def __init__(self):
+ self.name = "fake-connect"
+
+ class FakeDevice(object):
+ def __init__(self):
+ self.parameters = {}
+
+ class FakePipeline(Pipeline):
+
+ def __init__(self, parent=None, job=None):
+ super(TestAdjuvant.FakePipeline, self).__init__(parent, job)
+
+ class FailingAdjuvant(AdjuvantAction):
+ """
+ Added to the pipeline but only runs if FakeAction sets a suitable key.
+ """
+ def __init__(self):
+ super(TestAdjuvant.FailingAdjuvant, self).__init__()
+ self.name = "fake-adjuvant"
+ self.summary = "fake helper"
+ self.description = "fake adjuvant helper"
+
+ class FakeAdjuvant(AdjuvantAction):
+ """
+ Added to the pipeline but only runs if FakeAction sets a suitable key.
+ """
+ def __init__(self):
+ super(TestAdjuvant.FakeAdjuvant, self).__init__()
+ self.name = "fake-adjuvant"
+ self.summary = "fake helper"
+ self.description = "fake adjuvant helper"
+
+ @classmethod
+ def key(cls):
+ return "fake-key"
+
+ def run(self, connection, args=None):
+ connection = super(TestAdjuvant.FakeAdjuvant, self).run(connection, args)
+ if not self.valid:
+ raise RuntimeError("fakeadjuvant should be valid")
+ if self.data[self.key()]:
+ self.data[self.key()] = 'triggered'
+ if self.adjuvant:
+ self.data[self.key()] = 'base class trigger'
+ return connection
+
+ class FakeAction(Action):
+ """
+ Isolated Action which can be used to generate artificial exceptions.
+ """
+
+ def __init__(self):
+ super(TestAdjuvant.FakeAction, self).__init__()
+ self.count = 1
+ self.name = "fake-action"
+ self.summary = "fake action for unit tests"
+ self.description = "fake, do not use outside unit tests"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(TestAdjuvant.FakeAdjuvant())
+
+ def run(self, connection, args=None):
+ if connection:
+ raise RuntimeError("Fake action not meant to have a real connection")
+ connection = TestAdjuvant.FakeConnection()
+ self.count += 1
+ self.results = {'status': "failed"}
+ self.data[TestAdjuvant.FakeAdjuvant.key()] = True
+ return connection
+
+ class SafeAction(Action):
+ """
+ Isolated test action which does not trigger the adjuvant
+ """
+ def __init__(self):
+ super(TestAdjuvant.SafeAction, self).__init__()
+ self.name = "passing-action"
+ self.summary = "fake action without adjuvant"
+ self.description = "fake action runs without calling adjuvant"
+
+ def populate(self, parameters):
+ self.internal_pipeline = Pipeline(parent=self, job=self.job, parameters=parameters)
+ self.internal_pipeline.add_action(TestAdjuvant.FakeAdjuvant())
+
+ def run(self, connection, args=None):
+ if connection:
+ raise RuntimeError("Fake action not meant to have a real connection")
+ connection = TestAdjuvant.FakeConnection()
+ self.results = {'status': "passed"}
+ self.data[TestAdjuvant.FakeAdjuvant.key()] = False
+ return connection
+
+ def setUp(self):
+ self.parameters = {
+ "job_name": "fakejob",
+ 'output_dir': ".",
+ "actions": [
+ {
+ 'deploy': {
+ 'failure_retry': 3
+ },
+ 'boot': {
+ 'failure_retry': 4
+ },
+ 'test': {
+ 'failure_retry': 5
+ }
+ }
+ ]
+ }
+ self.fakejob = TestAdjuvant.FakeJob(self.parameters)
+
+ def test_adjuvant_key(self):
+ pipeline = TestAction.FakePipeline(job=self.fakejob)
+ pipeline.add_action(TestAdjuvant.FakeAction())
+ pipeline.add_action(TestAdjuvant.FailingAdjuvant())
+ self.fakejob.set_pipeline(pipeline)
+ self.fakejob.device = TestAdjuvant.FakeDevice()
+ with self.assertRaises(JobError):
+ self.fakejob.validate()
+
+ def test_adjuvant(self):
+ pipeline = TestAction.FakePipeline(job=self.fakejob)
+ pipeline.add_action(TestAdjuvant.FakeAction())
+ pipeline.add_action(TestAdjuvant.FakeAdjuvant())
+ self.fakejob.set_pipeline(pipeline)
+ self.fakejob.device = TestAdjuvant.FakeDevice()
+ actions = []
+ for action in self.fakejob.pipeline.actions:
+ actions.append(action.name)
+ self.assertIn('fake-action', actions)
+ self.assertIn('fake-adjuvant', actions)
+ self.assertEqual(self.fakejob.pipeline.actions[1].key(), TestAdjuvant.FakeAdjuvant.key())
+
+ def test_run_adjuvant_action(self):
+ pipeline = TestAction.FakePipeline(job=self.fakejob)
+ pipeline.add_action(TestAdjuvant.FakeAction())
+ pipeline.add_action(TestAdjuvant.FakeAdjuvant())
+ self.fakejob.set_pipeline(pipeline)
+ self.fakejob.device = TestAdjuvant.FakeDevice()
+ self.fakejob.run()
+ self.assertEqual(self.fakejob.context, {'fake-key': 'base class trigger'})
+
+ def test_run_action(self):
+ pipeline = TestAction.FakePipeline(job=self.fakejob)
+ pipeline.add_action(TestAdjuvant.SafeAction())
+ pipeline.add_action(TestAdjuvant.FakeAdjuvant())
+ self.fakejob.set_pipeline(pipeline)
+ self.fakejob.device = TestAdjuvant.FakeDevice()
+ self.fakejob.run()
+ self.assertNotEqual(self.fakejob.context, {'fake-key': 'triggered'})
+ self.assertNotEqual(self.fakejob.context, {'fake-key': 'base class trigger'})
+ self.assertEqual(self.fakejob.context, {'fake-key': False})
diff --git a/lava_dispatcher/pipeline/utils/shell.py b/lava_dispatcher/pipeline/utils/shell.py
index d46f89d..7937e8d 100644
--- a/lava_dispatcher/pipeline/utils/shell.py
+++ b/lava_dispatcher/pipeline/utils/shell.py
@@ -26,9 +26,14 @@ from lava_dispatcher.pipeline.action import InfrastructureError
def which(path, match=os.path.isfile):
"""
Simple replacement for the `which` command found on
- Debian based systems.
+ Debian based systems. Allows ordinary users to query
+ the PATH used at runtime.
"""
- for dirname in os.environ['PATH'].split(':'):
+ paths = os.environ['PATH'].split(':')
+ if os.getuid() != 0:
+ # avoid sudo - it may ask for a password on developer systems.
+ paths.extend(['/usr/local/sbin', '/usr/sbin', '/sbin'])
+ for dirname in paths:
candidate = os.path.join(dirname, path)
if match(candidate):
return candidate