def locate_devices(self, images): """Locate device for each image in the given images list. @param images: A list of tuples of (build, serial). serial could be None if it's not specified. Following are some examples: [('branch1/shamu-userdebug/100', None), ('branch1/shamu-userdebug/100', None)] [('branch1/hammerhead-userdebug/100', 'XZ123'), ('branch1/hammerhead-userdebug/200', None)] where XZ123 is serial of one of the hammerheads connected to the testbed. @return: A dictionary of (serial, build). Note that build here should not have a serial specified in it. @raise InstallError: If not enough duts are available to install the given images. Or there are more duts with the same board than the images list specified. """ # The map between serial and build to install in that dut. serial_build_pairs = {} builds_without_serial = [ build for build, serial in images if not serial ] for build, serial in images: if serial: serial_build_pairs[serial] = build # Return the mapping if all builds have serial specified. if not builds_without_serial: return serial_build_pairs # serials grouped by the board of duts. duts_by_name = {} for serial, host in self.get_adb_devices().iteritems(): # Excluding duts already assigned to a build. if serial in serial_build_pairs: continue aliases = host.get_device_aliases() for alias in aliases: duts_by_name.setdefault(alias, []).append(serial) # Builds grouped by the board name. builds_by_name = {} for build in builds_without_serial: match = re.match(adb_host.BUILD_REGEX, build) if not match: raise error.InstallError( 'Build %s is invalid. Failed to parse ' 'the board name.' % build) name = match.group('BUILD_TARGET') builds_by_name.setdefault(name, []).append(build) # Pair build with dut with matching board. for name, builds in builds_by_name.iteritems(): duts = duts_by_name.get(name, []) if len(duts) != len(builds): raise error.InstallError( 'Expected number of DUTs for name %s is %d, got %d' % (name, len(builds), len(duts) if duts else 0)) serial_build_pairs.update(dict(zip(duts, builds))) return serial_build_pairs
def _parse_image(self, image_string): """Parse the image string to a dictionary. Sample value of image_string: Provision dut with serial ZX1G2 to build `branch1/shamu-userdebug/111`, and provision another shamu with build `branch2/shamu-userdebug/222` branch1/shamu-userdebug/111:ZX1G2,branch2/shamu-userdebug/222 Provision 10 shamu with build `branch1/shamu-userdebug/LATEST` branch1/shamu-userdebug/LATEST#10 @param image_string: A comma separated string of images. The image name is in the format of branch/target/build_id[:serial]. Serial is optional once testbed machine_install supports allocating DUT based on board. @returns: A list of tuples of (build, serial). serial could be None if it's not specified. """ images = [] for image in image_string.split(','): match = re.match(_IMAGE_NAME_PATTERN, image) # The image string cannot specify both serial and count. if not match or (match.group(2) and match.group(3)): raise error.InstallError( 'Image name of "%s" has invalid format. It should ' 'follow naming convention of ' 'branch/target/build_id[:serial][#count]', image) if match.group(3): images.extend([(match.group(1), None)] * int(match.group(3))) else: images.append((match.group(1), match.group(2))) return images
def machine_install(self): """Install the DUT. @returns The name of the image installed. """ if not self._parser.options.image: raise error.InstallError('No image string is provided to test bed.') images = self._parse_image(self._parser.options.image) arguments = [] for serial, build in self.locate_devices(images).iteritems(): logging.info('Installing build %s on DUT with serial %s.', build, serial) host = self.get_adb_devices()[serial] build_url, _ = host.stage_build_for_install(build) arguments.append({'host': host, 'build_url': build_url}) thread_pool = pool.ThreadPool(_POOL_SIZE) thread_pool.map(self._install_device, arguments) thread_pool.close() return self._parser.options.image
def machine_install(self, build_url=None, build_local_path=None, wipe=True, flash_all=False, os_type=None): """Install the DUT. @param build_url: The url to use for downloading Android artifacts. pattern: http://$devserver:###/static/$build. If build_url is set to None, the code may try _parser.options.image to do the installation. If none of them is set, machine_install will fail. @param build_local_path: The path to a local directory that contains the image files needed to provision the device. @param wipe: No-op @param flash_all: No-op @param os_type: OS to install (overrides label). @returns A tuple of (image_name, host_attributes). image_name is the name of image installed, e.g., git_mnc-release/shamu-userdebug/1234 host_attributes is a dictionary of (attribute, value), which can be saved to afe_host_attributes table in database. This method returns a dictionary with a single entry of `job_repo_url_[adb_serial]`: devserver_url, where devserver_url is a url to the build staged on devserver. """ os_type = os_type or self.get_os_type() if not build_url and self._parser.options.image: build_url, _ = self.stage_build_for_install( self._parser.options.image, os_type=os_type) if os_type == OS_TYPE_EMULATED_BRILLO: self.setup_brillo_emulator( build_url=build_url, build_local_path=build_local_path) self.ensure_adb_mode() else: raise error.InstallError( 'Installation of os type %s is not supported.' % os_type) return (build_url.split('static/')[-1], {self.job_repo_url_attribute: build_url})
def _parse_image(self, image_string): """Parse the image string to a dictionary. Sample value of image_string: branch1/shamu-userdebug/LATEST:ZX1G2,branch2/shamu-userdebug/LATEST @param image_string: A comma separated string of images. The image name is in the format of branch/target/build_id[:serial]. Serial is optional once testbed machine_install supports allocating DUT based on board. @returns: A list of tuples of (build, serial). serial could be None if it's not specified. """ images = [] for image in image_string.split(','): match = re.match(_IMAGE_NAME_PATTERN, image) if not match: raise error.InstallError( 'Image name of "%s" has invalid format. It should ' 'follow naming convention of ' 'branch/target/build_id[:serial]', image) images.append((match.group(1), match.group(2))) return images
def testTriggerUpdate(self): """Tests that we correctly handle updater errors.""" update_url = 'http://server/test/url' self.host = self.mox.CreateMockAnything() self.mox.StubOutWithMock(self.host, 'run') self.mox.StubOutWithMock(autoupdater.ChromiumOSUpdater, 'get_last_update_error') self.host.hostname = 'test_host' updater_control_bin = '/usr/bin/update_engine_client' test_url = 'http://server/test/url' expected_wait_cmd = ('%s -status | grep CURRENT_OP' % updater_control_bin) expected_cmd = ('%s --check_for_update --omaha_url=%s' % (updater_control_bin, test_url)) self.mox.StubOutWithMock(time, "sleep") UPDATE_ENGINE_RETRY_WAIT_TIME = 5 # Generic SSH Error. cmd_result_255 = self.mox.CreateMockAnything() cmd_result_255.exit_status = 255 # Command Failed Error cmd_result_1 = self.mox.CreateMockAnything() cmd_result_1.exit_status = 1 # Error 37 cmd_result_37 = self.mox.CreateMockAnything() cmd_result_37.exit_status = 37 updater = autoupdater.ChromiumOSUpdater(update_url, host=self.host) # (SUCCESS) Expect one wait command and one status command. self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd) # (SUCCESS) Test with one retry to wait for update-engine. self._host_run_for_update(expected_wait_cmd, exception=error.AutoservRunError( 'non-zero status', cmd_result_1)) time.sleep(UPDATE_ENGINE_RETRY_WAIT_TIME) self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd) # (SUCCESS) One-time SSH timeout, then success on retry. self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd, exception=error.AutoservSSHTimeout( 'ssh timed out', cmd_result_255)) self._host_run_for_update(expected_cmd) # (SUCCESS) One-time ERROR 37, then success. self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd, exception=error.AutoservRunError( 'ERROR_CODE=37', cmd_result_37)) self._host_run_for_update(expected_cmd) # (FAILURE) Bad status of update engine. self._host_run_for_update(expected_wait_cmd) self._host_run_for_update( expected_cmd, bad_update_status=True, exception=error.InstallError('host is not in installable state')) # (FAILURE) Two-time SSH timeout. self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd, exception=error.AutoservSSHTimeout( 'ssh timed out', cmd_result_255)) self._host_run_for_update(expected_cmd, exception=error.AutoservSSHTimeout( 'ssh timed out', cmd_result_255)) # (FAILURE) SSH Permission Error self._host_run_for_update(expected_wait_cmd) self._host_run_for_update( expected_cmd, exception=error.AutoservSshPermissionDeniedError( 'no permission', cmd_result_255)) # (FAILURE) Other ssh failure self._host_run_for_update(expected_wait_cmd) self._host_run_for_update( expected_cmd, exception=error.AutoservSshPermissionDeniedError( 'no permission', cmd_result_255)) # (FAILURE) Other error self._host_run_for_update(expected_wait_cmd) self._host_run_for_update(expected_cmd, exception=error.AutoservRunError( "unknown error", cmd_result_1)) self.mox.ReplayAll() # Expect success updater.trigger_update() updater.trigger_update() updater.trigger_update() updater.trigger_update() # Expect errors as listed above self.assertRaises(autoupdater.RootFSUpdateError, updater.trigger_update) self.assertRaises(autoupdater.RootFSUpdateError, updater.trigger_update) self.assertRaises(autoupdater.RootFSUpdateError, updater.trigger_update) self.assertRaises(autoupdater.RootFSUpdateError, updater.trigger_update) self.assertRaises(autoupdater.RootFSUpdateError, updater.trigger_update) self.mox.VerifyAll()
def machine_install(self, image=None): """Install the DUT. @param image: Image we want to install on this testbed, e.g., `branch1/shamu-eng/1001,branch2/shamu-eng/1002` @returns A tuple of (the name of the image installed, None), where None is a placeholder for update_url. Testbed does not have a single update_url, thus it's set to None. @returns A tuple of (image_name, host_attributes). image_name is the name of images installed, e.g., `branch1/shamu-eng/1001,branch2/shamu-eng/1002` host_attributes is a dictionary of (attribute, value), which can be saved to afe_host_attributes table in database. This method returns a dictionary with entries of job_repo_urls for each provisioned devices: `job_repo_url_[adb_serial]`: devserver_url, where devserver_url is a url to the build staged on devserver. For example: {'job_repo_url_XZ001': 'http://10.1.1.3/branch1/shamu-eng/1001', 'job_repo_url_XZ002': 'http://10.1.1.3/branch2/shamu-eng/1002'} """ image = image or self._parser.options.image if not image: raise error.InstallError( 'No image string is provided to test bed.') images = self._parse_image(image) host_attributes = {} # Change logging formatter to include thread name. This is to help logs # from each provision runs have the dut's serial, which is set as the # thread name. logging_config.add_threadname_in_log() serial_build_map = self.locate_devices(images) build_url, build_local_path, teststation = self._stage_shared_build( serial_build_map) thread_pool = None try: arguments = [] for serial, build in serial_build_map.iteritems(): logging.info('Installing build %s on DUT with serial %s.', build, serial) host = self.get_adb_devices()[serial] if build_url: device_build_url = build_url else: device_build_url, _ = host.stage_build_for_install(build) arguments.append({ 'host': host, 'build_url': device_build_url, 'build_local_path': build_local_path }) attribute_name = '%s_%s' % (constants.JOB_REPO_URL, host.adb_serial) host_attributes[attribute_name] = device_build_url thread_pool = pool.ThreadPool(_POOL_SIZE) thread_pool.map(self._install_device, arguments) thread_pool.close() except Exception as err: logging.error(err.message) finally: if thread_pool: thread_pool.join() if build_local_path: logging.debug('Clean up build artifacts %s:%s', teststation.hostname, build_local_path) teststation.run('rm -rf %s' % build_local_path) return image, host_attributes
def locate_devices(self, images): """Locate device for each image in the given images list. If the given images all have no serial associated and have the same image for the same board, testbed will assign all devices with the desired board to the image. This allows tests to randomly pick devices to run. As an example, a testbed with 4 devices, 2 for board_1 and 2 for board_2. If the given images value is: [('board_1_build', None), ('board_2_build', None)] The testbed will return following device allocation: {'serial_1_board_1': 'board_1_build', 'serial_2_board_1': 'board_1_build', 'serial_1_board_2': 'board_2_build', 'serial_2_board_2': 'board_2_build', } That way, all board_1 duts will be installed with board_1_build, and all board_2 duts will be installed with board_2_build. Test can pick any dut from board_1 duts and same applies to board_2 duts. @param images: A list of tuples of (build, serial). serial could be None if it's not specified. Following are some examples: [('branch1/shamu-userdebug/100', None), ('branch1/shamu-userdebug/100', None)] [('branch1/hammerhead-userdebug/100', 'XZ123'), ('branch1/hammerhead-userdebug/200', None)] where XZ123 is serial of one of the hammerheads connected to the testbed. @return: A dictionary of (serial, build). Note that build here should not have a serial specified in it. @raise InstallError: If not enough duts are available to install the given images. Or there are more duts with the same board than the images list specified. """ # The map between serial and build to install in that dut. serial_build_pairs = {} builds_without_serial = [ build for build, serial in images if not serial ] for build, serial in images: if serial: serial_build_pairs[serial] = build # Return the mapping if all builds have serial specified. if not builds_without_serial: return serial_build_pairs # serials grouped by the board of duts. duts_by_name = {} for serial, host in self.get_adb_devices().iteritems(): # Excluding duts already assigned to a build. if serial in serial_build_pairs: continue aliases = host.get_device_aliases() for alias in aliases: duts_by_name.setdefault(alias, []).append(serial) # Builds grouped by the board name. builds_by_name = {} for build in builds_without_serial: match = re.match(adb_host.BUILD_REGEX, build) if not match: raise error.InstallError( 'Build %s is invalid. Failed to parse ' 'the board name.' % build) name = match.group('BUILD_TARGET') builds_by_name.setdefault(name, []).append(build) # Pair build with dut with matching board. for name, builds in builds_by_name.iteritems(): duts = duts_by_name.get(name, []) if len(duts) < len(builds): raise error.InstallError( 'Expected number of DUTs for name %s is %d, got %d' % (name, len(builds), len(duts) if duts else 0)) elif len(duts) == len(builds): serial_build_pairs.update(dict(zip(duts, builds))) else: # In this cases, available dut number is greater than the number # of builds. if len(set(builds)) > 1: raise error.InstallError( 'Number of available DUTs are greater than builds ' 'needed, testbed cannot allocate DUTs for testing ' 'deterministically.') # Set all DUTs to the same build. for serial in duts: serial_build_pairs[serial] = builds[0] return serial_build_pairs