blob: 522f9ebd76ccff8a7fdffbbc5a5e93064e9024c5 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Copyright (C) 2014 Intel Corporation
2#
3# Released under the MIT license (see COPYING.MIT)
4
5# This module adds support to testimage.bbclass to deploy images and run
6# tests using a "master image" - this is a "known good" image that is
7# installed onto the device as part of initial setup and will be booted into
8# with no interaction; we can then use it to deploy the image to be tested
9# to a second partition before running the tests.
10#
11# For an example master image, see core-image-testmaster
12# (meta/recipes-extended/images/core-image-testmaster.bb)
13
14import os
15import bb
16import traceback
17import time
18import subprocess
19
20import oeqa.targetcontrol
21import oeqa.utils.sshcontrol as sshcontrol
22import oeqa.utils.commands as commands
23from oeqa.utils import CommandError
24
25from abc import ABCMeta, abstractmethod
26
27class MasterImageHardwareTarget(oeqa.targetcontrol.BaseTarget):
28
29 __metaclass__ = ABCMeta
30
31 supported_image_fstypes = ['tar.gz', 'tar.bz2']
32
33 def __init__(self, d):
34 super(MasterImageHardwareTarget, self).__init__(d)
35
36 # target ip
37 addr = d.getVar("TEST_TARGET_IP", True) or bb.fatal('Please set TEST_TARGET_IP with the IP address of the machine you want to run the tests on.')
38 self.ip = addr.split(":")[0]
39 try:
40 self.port = addr.split(":")[1]
41 except IndexError:
42 self.port = None
43 bb.note("Target IP: %s" % self.ip)
44 self.server_ip = d.getVar("TEST_SERVER_IP", True)
45 if not self.server_ip:
46 try:
47 self.server_ip = subprocess.check_output(['ip', 'route', 'get', self.ip ]).split("\n")[0].split()[-1]
48 except Exception as e:
49 bb.fatal("Failed to determine the host IP address (alternatively you can set TEST_SERVER_IP with the IP address of this machine): %s" % e)
50 bb.note("Server IP: %s" % self.server_ip)
51
52 # test rootfs + kernel
53 self.image_fstype = self.get_image_fstype(d)
54 self.rootfs = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("IMAGE_LINK_NAME", True) + '.' + self.image_fstype)
55 self.kernel = os.path.join(d.getVar("DEPLOY_DIR_IMAGE", True), d.getVar("KERNEL_IMAGETYPE", False) + '-' + d.getVar('MACHINE', False) + '.bin')
56 if not os.path.isfile(self.rootfs):
57 # we could've checked that IMAGE_FSTYPES contains tar.gz but the config for running testimage might not be
58 # the same as the config with which the image was build, ie
59 # you bitbake core-image-sato with IMAGE_FSTYPES += "tar.gz"
60 # and your autobuilder overwrites the config, adds the test bits and runs bitbake core-image-sato -c testimage
61 bb.fatal("No rootfs found. Did you build the image ?\nIf yes, did you build it with IMAGE_FSTYPES += \"tar.gz\" ? \
62 \nExpected path: %s" % self.rootfs)
63 if not os.path.isfile(self.kernel):
64 bb.fatal("No kernel found. Expected path: %s" % self.kernel)
65
66 # master ssh connection
67 self.master = None
68 # if the user knows what they are doing, then by all means...
69 self.user_cmds = d.getVar("TEST_DEPLOY_CMDS", True)
70 self.deploy_cmds = None
71
72 # this is the name of the command that controls the power for a board
73 # e.g: TEST_POWERCONTROL_CMD = "/home/user/myscripts/powercontrol.py ${MACHINE} what-ever-other-args-the-script-wants"
74 # the command should take as the last argument "off" and "on" and "cycle" (off, on)
75 self.powercontrol_cmd = d.getVar("TEST_POWERCONTROL_CMD", True) or None
76 self.powercontrol_args = d.getVar("TEST_POWERCONTROL_EXTRA_ARGS", False) or ""
77
78 self.serialcontrol_cmd = d.getVar("TEST_SERIALCONTROL_CMD", True) or None
79 self.serialcontrol_args = d.getVar("TEST_SERIALCONTROL_EXTRA_ARGS", False) or ""
80
81 self.origenv = os.environ
82 if self.powercontrol_cmd or self.serialcontrol_cmd:
83 # the external script for controlling power might use ssh
84 # ssh + keys means we need the original user env
85 bborigenv = d.getVar("BB_ORIGENV", False) or {}
86 for key in bborigenv:
87 val = bborigenv.getVar(key, True)
88 if val is not None:
89 self.origenv[key] = str(val)
90
91 if self.powercontrol_cmd:
92 if self.powercontrol_args:
93 self.powercontrol_cmd = "%s %s" % (self.powercontrol_cmd, self.powercontrol_args)
94 if self.serialcontrol_cmd:
95 if self.serialcontrol_args:
96 self.serialcontrol_cmd = "%s %s" % (self.serialcontrol_cmd, self.serialcontrol_args)
97
98 def power_ctl(self, msg):
99 if self.powercontrol_cmd:
100 cmd = "%s %s" % (self.powercontrol_cmd, msg)
101 try:
102 commands.runCmd(cmd, assert_error=False, preexec_fn=os.setsid, env=self.origenv)
103 except CommandError as e:
104 bb.fatal(str(e))
105
106 def power_cycle(self, conn):
107 if self.powercontrol_cmd:
108 # be nice, don't just cut power
109 conn.run("shutdown -h now")
110 time.sleep(10)
111 self.power_ctl("cycle")
112 else:
113 status, output = conn.run("reboot")
114 if status != 0:
115 bb.error("Failed rebooting target and no power control command defined. You need to manually reset the device.\n%s" % output)
116
117 def _wait_until_booted(self):
118 ''' Waits until the target device has booted (if we have just power cycled it) '''
119 # Subclasses with better methods of determining boot can override this
120 time.sleep(120)
121
122 def deploy(self):
123 # base class just sets the ssh log file for us
124 super(MasterImageHardwareTarget, self).deploy()
125 self.master = sshcontrol.SSHControl(ip=self.ip, logfile=self.sshlog, timeout=600, port=self.port)
126 status, output = self.master.run("cat /etc/masterimage")
127 if status != 0:
128 # We're not booted into the master image, so try rebooting
129 bb.plain("%s - booting into the master image" % self.pn)
130 self.power_ctl("cycle")
131 self._wait_until_booted()
132
133 bb.plain("%s - deploying image on target" % self.pn)
134 status, output = self.master.run("cat /etc/masterimage")
135 if status != 0:
136 bb.fatal("No ssh connectivity or target isn't running a master image.\n%s" % output)
137 if self.user_cmds:
138 self.deploy_cmds = self.user_cmds.split("\n")
139 try:
140 self._deploy()
141 except Exception as e:
142 bb.fatal("Failed deploying test image: %s" % e)
143
144 @abstractmethod
145 def _deploy(self):
146 pass
147
148 def start(self, params=None):
149 bb.plain("%s - boot test image on target" % self.pn)
150 self._start()
151 # set the ssh object for the target/test image
152 self.connection = sshcontrol.SSHControl(self.ip, logfile=self.sshlog, port=self.port)
153 bb.plain("%s - start running tests" % self.pn)
154
155 @abstractmethod
156 def _start(self):
157 pass
158
159 def stop(self):
160 bb.plain("%s - reboot/powercycle target" % self.pn)
161 self.power_cycle(self.connection)
162
163
164class GummibootTarget(MasterImageHardwareTarget):
165
166 def __init__(self, d):
167 super(GummibootTarget, self).__init__(d)
168 # this the value we need to set in the LoaderEntryOneShot EFI variable
169 # so the system boots the 'test' bootloader label and not the default
170 # The first four bytes are EFI bits, and the rest is an utf-16le string
171 # (EFI vars values need to be utf-16)
172 # $ echo -en "test\0" | iconv -f ascii -t utf-16le | hexdump -C
173 # 00000000 74 00 65 00 73 00 74 00 00 00 |t.e.s.t...|
174 self.efivarvalue = r'\x07\x00\x00\x00\x74\x00\x65\x00\x73\x00\x74\x00\x00\x00'
175 self.deploy_cmds = [
176 'mount -L boot /boot',
177 'mkdir -p /mnt/testrootfs',
178 'mount -L testrootfs /mnt/testrootfs',
179 'modprobe efivarfs',
180 'mount -t efivarfs efivarfs /sys/firmware/efi/efivars',
181 'cp ~/test-kernel /boot',
182 'rm -rf /mnt/testrootfs/*',
183 'tar xvf ~/test-rootfs.%s -C /mnt/testrootfs' % self.image_fstype,
184 'printf "%s" > /sys/firmware/efi/efivars/LoaderEntryOneShot-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f' % self.efivarvalue
185 ]
186
187 def _deploy(self):
188 # make sure these aren't mounted
189 self.master.run("umount /boot; umount /mnt/testrootfs; umount /sys/firmware/efi/efivars;")
190 # from now on, every deploy cmd should return 0
191 # else an exception will be thrown by sshcontrol
192 self.master.ignore_status = False
193 self.master.copy_to(self.rootfs, "~/test-rootfs." + self.image_fstype)
194 self.master.copy_to(self.kernel, "~/test-kernel")
195 for cmd in self.deploy_cmds:
196 self.master.run(cmd)
197
198 def _start(self, params=None):
199 self.power_cycle(self.master)
200 # there are better ways than a timeout but this should work for now
201 time.sleep(120)