1# Copyright (c) 2019 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"""This file provides core logic for labstation verify/repair process.""" 6 7import logging 8 9from autotest_lib.client.common_lib import error 10from autotest_lib.server.hosts import base_label 11from autotest_lib.server.hosts import cros_label 12from autotest_lib.server.cros import autoupdater 13from autotest_lib.server.hosts import labstation_repair 14from autotest_lib.server.cros import provision 15from autotest_lib.server.hosts import base_servohost 16from autotest_lib.client.cros import constants as client_constants 17from autotest_lib.server.cros.dynamic_suite import constants as ds_constants 18from autotest_lib.server.cros.dynamic_suite import tools 19from autotest_lib.client.common_lib import lsbrelease_utils 20from autotest_lib.client.common_lib.cros import dev_server 21from autotest_lib.server import utils as server_utils 22 23 24class LabstationHost(base_servohost.BaseServoHost): 25 """Labstation specific host class""" 26 27 # Threshold we decide to ignore a in_use file lock. In minutes 28 IN_USE_FILE_EXPIRE_MINS = 120 29 30 # Uptime threshold to perform a labstation reboot, this is to prevent a 31 # broken DUT keep trying to reboot a labstation. In hours 32 UP_TIME_THRESH_HOLD_HOURS = 24 33 34 VERSION_PREFIX = provision.CROS_VERSION_PREFIX 35 36 @staticmethod 37 def check_host(host, timeout=10): 38 """ 39 Check if the given host is a labstation host. 40 41 @param host: An ssh host representing a device. 42 @param timeout: The timeout for the run command. 43 44 @return: True if the host device is labstation. 45 46 @raises AutoservRunError: If the command failed. 47 @raises AutoservSSHTimeout: Ssh connection has timed out. 48 49 """ 50 try: 51 result = host.run( 52 'grep -q labstation /etc/lsb-release', 53 ignore_status=True, timeout=timeout) 54 except (error.AutoservRunError, error.AutoservSSHTimeout): 55 return False 56 return result.exit_status == 0 57 58 59 def _initialize(self, hostname, *args, **dargs): 60 super(LabstationHost, self)._initialize(hostname=hostname, 61 *args, **dargs) 62 self._repair_strategy = ( 63 labstation_repair.create_labstation_repair_strategy()) 64 self.labels = base_label.LabelRetriever(cros_label.LABSTATION_LABELS) 65 logging.info('adding fake host_info_store to LabstationHost') 66 try: 67 host_info = self.host_info_store.get() 68 host_info.stable_versions['servo-cros'] = host_info.stable_versions['cros'] 69 self.set_dut_host_info(host_info) 70 except Exception as e: 71 logging.exception(e) 72 73 74 def is_reboot_requested(self): 75 """Check if a reboot is requested for this labstation, the reboot can 76 either be requested from labstation or DUTs. For request from DUTs we 77 only process it if uptime longer than a threshold because we want 78 to prevent a broken servo keep its labstation in reboot cycle. 79 80 @returns True if a reboot is required, otherwise False 81 """ 82 if self._check_update_status() == autoupdater.UPDATER_NEED_REBOOT: 83 logging.info('Labstation reboot requested from labstation for' 84 ' update image') 85 return True 86 87 if not self._validate_uptime(): 88 logging.info('Ignoring DUTs reboot request because %s was' 89 ' rebooted in last 24 hours.', self.hostname) 90 return False 91 92 cmd = 'find %s*%s' % (self.TEMP_FILE_DIR, self.REBOOT_FILE_POSTFIX) 93 output = self.run(cmd, ignore_status=True).stdout 94 if output: 95 in_use_file_list = output.strip().split('\n') 96 logging.info('%s DUT(s) are currently requesting to' 97 ' reboot labstation.', len(in_use_file_list)) 98 return True 99 else: 100 return False 101 102 103 def try_reboot(self): 104 """Try to reboot the labstation if it's safe to do(no servo in use, 105 and not processing updates), and cleanup reboot control file. 106 """ 107 if (self._is_servo_in_use() or self._check_update_status() 108 in autoupdater.UPDATER_PROCESSING_UPDATE): 109 logging.info('Aborting reboot action because some DUT(s) are' 110 ' currently using servo(s) or' 111 ' labstation update is in processing.') 112 return 113 self._servo_host_reboot() 114 logging.info('Cleaning up reboot control files.') 115 self._cleanup_post_reboot() 116 117 118 def get_labels(self): 119 """Return the detected labels on the host.""" 120 return self.labels.get_labels(self) 121 122 123 def get_os_type(self): 124 return 'labstation' 125 126 127 def prepare_for_update(self): 128 """Prepares the DUT for an update. 129 Subclasses may override this to perform any special actions 130 required before updating. 131 """ 132 pass 133 134 135 def _get_lsb_release_content(self): 136 """Return the content of lsb-release file of host.""" 137 return self.run( 138 'cat "%s"' % client_constants.LSB_RELEASE).stdout.strip() 139 140 141 def get_release_version(self): 142 """Get the value of attribute CHROMEOS_RELEASE_VERSION from lsb-release. 143 144 @returns The version string in lsb-release, under attribute 145 CHROMEOS_RELEASE_VERSION. 146 """ 147 return lsbrelease_utils.get_chromeos_release_version( 148 lsb_release_content=self._get_lsb_release_content()) 149 150 def verify_job_repo_url(self, tag=''): 151 """ 152 Make sure job_repo_url of this host is valid. 153 154 Eg: The job_repo_url "http://lmn.cd.ab.xyx:8080/static/\ 155 lumpy-release/R29-4279.0.0/autotest/packages" claims to have the 156 autotest package for lumpy-release/R29-4279.0.0. If this isn't the case, 157 download and extract it. If the devserver embedded in the url is 158 unresponsive, update the job_repo_url of the host after staging it on 159 another devserver. 160 161 @param job_repo_url: A url pointing to the devserver where the autotest 162 package for this build should be staged. 163 @param tag: The tag from the server job, in the format 164 <job_id>-<user>/<hostname>, or <hostless> for a server job. 165 166 @raises DevServerException: If we could not resolve a devserver. 167 @raises AutoservError: If we're unable to save the new job_repo_url as 168 a result of choosing a new devserver because the old one failed to 169 respond to a health check. 170 @raises urllib2.URLError: If the devserver embedded in job_repo_url 171 doesn't respond within the timeout. 172 """ 173 info = self.host_info_store.get() 174 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 175 if not job_repo_url: 176 logging.warning('No job repo url set on host %s', self.hostname) 177 return 178 179 logging.info('Verifying job repo url %s', job_repo_url) 180 devserver_url, image_name = tools.get_devserver_build_from_package_url( 181 job_repo_url) 182 183 ds = dev_server.ImageServer(devserver_url) 184 185 logging.info('Staging autotest artifacts for %s on devserver %s', 186 image_name, ds.url()) 187 188 ds.stage_artifacts(image_name, ['autotest_packages']) 189 190 191 def host_version_prefix(self, image): 192 """Return version label prefix. 193 194 In case the CrOS provisioning version is something other than the 195 standard CrOS version e.g. CrOS TH version, this function will 196 find the prefix from provision.py. 197 198 @param image: The image name to find its version prefix. 199 @returns: A prefix string for the image type. 200 """ 201 return provision.get_version_label_prefix(image) 202 203 204 def stage_server_side_package(self, image=None): 205 """Stage autotest server-side package on devserver. 206 207 @param image: Full path of an OS image to install or a build name. 208 209 @return: A url to the autotest server-side package. 210 211 @raise: error.AutoservError if fail to locate the build to test with, or 212 fail to stage server-side package. 213 """ 214 # If enable_drone_in_restricted_subnet is False, do not set hostname 215 # in devserver.resolve call, so a devserver in non-restricted subnet 216 # is picked to stage autotest server package for drone to download. 217 hostname = self.hostname 218 if not server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 219 hostname = None 220 if image: 221 image_name = tools.get_build_from_image(image) 222 if not image_name: 223 raise error.AutoservError( 224 'Failed to parse build name from %s' % image) 225 ds = dev_server.ImageServer.resolve(image_name, hostname) 226 else: 227 info = self.host_info_store.get() 228 job_repo_url = info.attributes.get(ds_constants.JOB_REPO_URL, '') 229 if job_repo_url: 230 devserver_url, image_name = ( 231 tools.get_devserver_build_from_package_url(job_repo_url)) 232 # If enable_drone_in_restricted_subnet is True, use the 233 # existing devserver. Otherwise, resolve a new one in 234 # non-restricted subnet. 235 if server_utils.ENABLE_DRONE_IN_RESTRICTED_SUBNET: 236 ds = dev_server.ImageServer(devserver_url) 237 else: 238 ds = dev_server.ImageServer.resolve(image_name) 239 elif info.build is not None: 240 ds = dev_server.ImageServer.resolve(info.build, hostname) 241 image_name = info.build 242 else: 243 raise error.AutoservError( 244 'Failed to stage server-side package. The host has ' 245 'no job_repo_url attribute or cros-version label.') 246 247 ds.stage_artifacts(image_name, ['autotest_server_package']) 248 return '%s/static/%s/%s' % (ds.url(), image_name, 249 'autotest_server_package.tar.bz2') 250 251 252 def repair(self): 253 """Attempt to repair a labstation. 254 """ 255 message = 'Beginning repair for host %s board %s model %s' 256 info = self.host_info_store.get() 257 message %= (self.hostname, info.board, info.model) 258 self.record('INFO', None, None, message) 259 self._repair_strategy.repair(self) 260 261 262 def _validate_uptime(self): 263 return (float(self.check_uptime()) > 264 self.UP_TIME_THRESH_HOLD_HOURS * 3600) 265 266 267 def _is_servo_in_use(self): 268 """Determine if there are any DUTs currently running task that uses 269 servo, only files that has been touched within pre-set threshold of 270 minutes counts. 271 272 @returns True if any DUTs is using servos, otherwise False. 273 """ 274 cmd = 'find %s*%s -mmin -%s' % (self.TEMP_FILE_DIR, 275 self.LOCK_FILE_POSTFIX, 276 self.IN_USE_FILE_EXPIRE_MINS) 277 result = self.run(cmd, ignore_status=True) 278 return bool(result.stdout) 279 280 281 def _cleanup_post_reboot(self): 282 """Clean up all xxxx_reboot file after reboot.""" 283 cmd = 'rm %s*%s' % (self.TEMP_FILE_DIR, self.REBOOT_FILE_POSTFIX) 284 self.run(cmd, ignore_status=True) 285