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