1# Lint as: python2, python3 2# Copyright (c) 2011 The Chromium OS Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7Factory install tests. 8 9FactoryInstallTest is an abstract superclass; factory_InstallVM and 10factory_InstallServo are two concrete implementations. 11 12Subclasses of FactoryInstallTest supports the following flags: 13 14 factory_install_image: (required) path to factory install shim 15 factory_test_image: (required) path to factory test image 16 test_image: (required) path to ChromeOS test image 17 miniomaha_port: port for miniomaha 18 debug_make_factory_package: whether to re-make the factory package before 19 running tests (defaults to true; may be set to false for debugging 20 only) 21""" 22 23import glob, logging, os, re, shutil, socket, sys, six.moves._thread, time, traceback 24from abc import abstractmethod 25from six import StringIO 26 27from autotest_lib.client.bin import utils as client_utils 28from autotest_lib.client.common_lib import error 29from autotest_lib.server import test, utils 30 31 32# How long to wait for the mini-Omaha server to come up. 33_MINIOMAHA_TIMEOUT_SEC = 50 34 35# Path to make_factory_package.sh within the source root. 36_MAKE_FACTORY_PACKAGE_PATH = \ 37 "platform/factory-utils/factory_setup/make_factory_package.sh" 38 39# Path to miniomaha.py within the source root. 40_MINIOMAHA_PATH = "platform/factory-utils/factory_setup/miniomaha.py" 41 42# Sleep interval for nontrivial operations (like rsyncing). 43_POLL_SLEEP_INTERVAL_SEC = 2 44 45# The hwid_updater script (run in the factory install shim). This is a format 46# string with a single argument (the name of the HWID cfg). 47_HWID_UPDATER_SH_TEMPLATE = """ 48echo Running hwid_updater "$@" >&2 49set -ex 50MOUNT_DIR=$(mktemp -d --tmpdir) 51mount "$1" "$MOUNT_DIR" 52ls -l "$MOUNT_DIR" 53mkdir -p "$MOUNT_DIR/dev_image/share/chromeos-hwid" 54echo %s > "$MOUNT_DIR/dev_image/share/chromeos-hwid/cfg" 55umount "$MOUNT_DIR" 56""" 57 58 59class FactoryInstallTest(test.test): 60 """ 61 Factory install VM tests. 62 63 See file-level docstring for details. 64 """ 65 66 version = 1 67 68 # How long to wait for the factory tests to install. 69 FACTORY_INSTALL_TIMEOUT_SEC = 1800 70 71 # How long to wait for the factory test image to come up. 72 WAIT_UP_TIMEOUT_SEC = 30 73 74 # How long to wait for the factory tests to run. 75 FACTORY_TEST_TIMEOUT_SEC = 240 76 77 # How long to wait for the ChromeOS image to run. 78 FIRST_BOOT_TIMEOUT_SEC = 480 79 80 # 81 # Abstract functions that must be overridden by subclasses. 82 # 83 84 @abstractmethod 85 def get_hwid_cfg(self): 86 """ 87 Returns the HWID cfg, used to select a test list. 88 """ 89 pass 90 91 @abstractmethod 92 def run_factory_install(self, shim_image): 93 """ 94 Performs the factory install and starts the factory tests. 95 96 When this returns, the DUT should be starting up (or have already 97 started up) in factory test mode. 98 """ 99 pass 100 101 @abstractmethod 102 def get_dut_client(self): 103 """ 104 Returns a client (subclass of CrosHost) to control the DUT. 105 """ 106 pass 107 108 @abstractmethod 109 def reboot_for_wipe(self): 110 """ 111 Reboots the machine after preparing to wipe the hard drive. 112 """ 113 pass 114 115 # 116 # Utility methods that may be used by subclasses. 117 # 118 119 def src_root(self): 120 """ 121 Returns the CrOS source root. 122 """ 123 return os.path.join(os.environ["CROS_WORKON_SRCROOT"], "src") 124 125 def parse_boolean(self, val): 126 """ 127 Parses a string as a Boolean value. 128 """ 129 # Insist on True or False, because (e.g.) bool('false') == True. 130 if str(val) not in ["True", "False"]: 131 raise error.TestError("Not a boolean: '%s'" % val) 132 return str(val) == "True" 133 134 # 135 # Private utility methods. 136 # 137 138 def _modify_file(self, path, func): 139 """ 140 Modifies a file as the root user. 141 142 @param path: The path to the file to modify. 143 @param func: A function that will be invoked with a single argument 144 (the current contents of the file, or None if the file does not 145 exist) and which should return the new contents. 146 """ 147 if os.path.exists(path): 148 contents = utils.system_output("sudo cat %s" % path) 149 else: 150 contents = func(None) 151 152 utils.run("sudo dd of=%s" % path, stdin=func(contents)) 153 154 def _mount_partition(self, image, index): 155 """ 156 Mounts a partition of an image temporarily using loopback. 157 158 The partition will be automatically unmounted when the test exits. 159 160 @param image: The image to mount. 161 @param index: The partition number to mount. 162 @return: The mount point. 163 """ 164 mount_point = os.path.join(self.tmpdir, 165 "%s_%d" % (image, index)) 166 if not os.path.exists(mount_point): 167 os.makedirs(mount_point) 168 common_args = "cgpt show -i %d %s" % (index, image) 169 offset = int(utils.system_output(common_args + " -b")) * 512 170 size = int(utils.system_output(common_args + " -s")) * 512 171 utils.run("sudo mount -o rw,loop,offset=%d,sizelimit=%d %s %s" % ( 172 offset, size, image, mount_point)) 173 self.cleanup_tasks.append(lambda: self._umount_partition(mount_point)) 174 return mount_point 175 176 def _umount_partition(self, mount_point): 177 """ 178 Unmounts the mount at the given mount point. 179 180 Also deletes the mount point directory. Does not raise an 181 exception if the mount point does not exist or the mount fails. 182 """ 183 if os.path.exists(mount_point): 184 utils.run("sudo umount -d %s" % mount_point) 185 os.rmdir(mount_point) 186 187 def _make_factory_package(self, factory_test_image, test_image): 188 """ 189 Makes the factory package. 190 """ 191 # Create a pseudo-HWID-updater that merely sets the HWID to "vm" or 192 # "servo" so that the appropriate test list will run. (This gets run by 193 # the factory install shim.) 194 hwid_updater = os.path.join(self.tmpdir, "hwid_updater.sh") 195 with open(hwid_updater, "w") as f: 196 f.write(_HWID_UPDATER_SH_TEMPLATE % self.get_hwid_cfg()) 197 198 utils.run("%s --factory=%s --release=%s " 199 "--firmware_updater=none --hwid_updater=%s " % 200 (os.path.join(self.src_root(), _MAKE_FACTORY_PACKAGE_PATH), 201 factory_test_image, test_image, hwid_updater)) 202 203 def _start_miniomaha(self): 204 """ 205 Starts a mini-Omaha server and drains its log output. 206 """ 207 def is_miniomaha_up(): 208 try: 209 utils.urlopen( 210 "http://localhost:%d" % self.miniomaha_port).read() 211 return True 212 except: 213 return False 214 215 assert not is_miniomaha_up() 216 217 self.miniomaha_output = os.path.join(self.outputdir, "miniomaha.out") 218 219 # TODO(jsalz): Add cwd to BgJob rather than including the 'cd' in the 220 # command. 221 bg_job = utils.BgJob( 222 "cd %s; exec ./%s --port=%d --factory_config=miniomaha.conf" 223 % (os.path.join(self.src_root(), 224 os.path.dirname(_MINIOMAHA_PATH)), 225 os.path.basename(_MINIOMAHA_PATH), 226 self.miniomaha_port), verbose=True, 227 stdout_tee=utils.TEE_TO_LOGS, 228 stderr_tee=open(self.miniomaha_output, "w")) 229 self.cleanup_tasks.append(lambda: utils.nuke_subprocess(bg_job.sp)) 230 six.moves._thread.start_new_thread(utils.join_bg_jobs, ([bg_job],)) 231 232 client_utils.poll_for_condition(is_miniomaha_up, 233 timeout=_MINIOMAHA_TIMEOUT_SEC, 234 desc="Miniomaha server") 235 236 def _prepare_factory_install_shim(self, factory_install_image): 237 # Make a copy of the factory install shim image (to use as hdb). 238 modified_image = os.path.join(self.tmpdir, "shim.bin") 239 logging.info("Creating factory install image: %s", modified_image) 240 shutil.copyfile(factory_install_image, modified_image) 241 242 # Mount partition 1 of the modified_image and set the mini-Omaha server. 243 mount = self._mount_partition(modified_image, 1) 244 self._modify_file( 245 os.path.join(mount, "dev_image/etc/lsb-factory"), 246 lambda contents: re.sub( 247 r"^(CHROMEOS_(AU|DEV)SERVER)=.+", 248 r"\1=http://%s:%d/update" % ( 249 socket.gethostname(), self.miniomaha_port), 250 contents, 251 re.MULTILINE)) 252 self._umount_partition(mount) 253 254 return modified_image 255 256 def _run_factory_tests_and_prepare_wipe(self): 257 """ 258 Runs the factory tests and prepares the machine for wiping. 259 """ 260 dut_client = self.get_dut_client() 261 if not dut_client.wait_up(FactoryInstallTest.WAIT_UP_TIMEOUT_SEC): 262 raise error.TestFail("DUT never came up to run factory tests") 263 264 # Poll the factory log, and wait for the factory_Review test to become 265 # active. 266 local_factory_log = os.path.join(self.outputdir, "factory.log") 267 remote_factory_log = "/var/log/factory.log" 268 269 # Wait for factory.log file to exist 270 dut_client.run( 271 "while ! [ -e %s ]; do sleep 1; done" % remote_factory_log, 272 timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC) 273 274 status_map = {} 275 276 def wait_for_factory_logs(): 277 dut_client.get_file(remote_factory_log, local_factory_log) 278 data = open(local_factory_log).read() 279 new_status_map = dict( 280 re.findall(r"status change for (\S+) : \S+ -> (\S+)", data)) 281 if status_map != new_status_map: 282 logging.info("Test statuses: %s", status_map) 283 # Can't assign directly since it's in a context outside 284 # this function. 285 status_map.clear() 286 status_map.update(new_status_map) 287 return status_map.get("factory_Review.z") == "ACTIVE" 288 289 client_utils.poll_for_condition( 290 wait_for_factory_logs, 291 timeout=FactoryInstallTest.FACTORY_TEST_TIMEOUT_SEC, 292 sleep_interval=_POLL_SLEEP_INTERVAL_SEC, 293 desc="Factory logs") 294 295 # All other statuses should be "PASS". 296 expected_status_map = { 297 "memoryrunin": "PASS", 298 "factory_Review.z": "ACTIVE", 299 "factory_Start.e": "PASS", 300 "hardware_SAT.memoryrunin_s1": "PASS", 301 } 302 if status_map != expected_status_map: 303 raise error.TestFail("Expected statuses of %s but found %s" % ( 304 expected_status_map, status_map)) 305 306 dut_client.run("cd /usr/local/factory/bin; " 307 "./gooftool --prepare_wipe --verbose") 308 309 def _complete_install(self): 310 """ 311 Completes the install, resulting in a full ChromeOS image. 312 """ 313 # Restart the SSH client: with a new OS, some configuration 314 # properties (e.g., availability of rsync) may have changed. 315 dut_client = self.get_dut_client() 316 317 if not dut_client.wait_up(FactoryInstallTest.FIRST_BOOT_TIMEOUT_SEC): 318 raise error.TestFail("DUT never came up after install") 319 320 # Check lsb-release to make sure we have a real live ChromeOS image 321 # (it should be the test build). 322 lsb_release = os.path.join(self.tmpdir, "lsb-release") 323 dut_client.get_file("/etc/lsb-release", lsb_release) 324 expected_re = r"^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build" 325 data = open(lsb_release).read() 326 assert re.search( 327 "^CHROMEOS_RELEASE_DESCRIPTION=.*Test Build", data, re.MULTILINE), ( 328 "Didn't find expected regular expression %s in lsb-release: " % ( 329 expected_re, data)) 330 logging.info("Install succeeded! lsb-release is:\n%s", data) 331 332 dut_client.halt() 333 if not dut_client.wait_down( 334 timeout=FactoryInstallTest.WAIT_UP_TIMEOUT_SEC): 335 raise error.TestFail("Client never went down after ChromeOS boot") 336 337 # 338 # Autotest methods. 339 # 340 341 def setup(self): 342 self.cleanup_tasks = [] 343 self.ssh_tunnel_port = utils.get_unused_port() 344 345 def run_once(self, factory_install_image, factory_test_image, test_image, 346 miniomaha_port=None, debug_make_factory_package=True, 347 **args): 348 """ 349 Runs the test once. 350 351 See the file-level comments for an explanation of the test arguments. 352 353 @param args: Must be empty (present as a check against misspelled 354 arguments on the command line) 355 """ 356 assert not args, "Unexpected arguments %s" % args 357 358 self.miniomaha_port = ( 359 int(miniomaha_port) if miniomaha_port else utils.get_unused_port()) 360 361 if self.parse_boolean(debug_make_factory_package): 362 self._make_factory_package(factory_test_image, test_image) 363 self._start_miniomaha() 364 shim_image = self._prepare_factory_install_shim(factory_install_image) 365 self.run_factory_install(shim_image) 366 self._run_factory_tests_and_prepare_wipe() 367 self.reboot_for_wipe() 368 self._complete_install() 369 370 def cleanup(self): 371 for task in self.cleanup_tasks: 372 try: 373 task() 374 except: 375 logging.info("Exception in cleanup task:") 376 traceback.print_exc(file=sys.stdout) 377