1# -*- coding: utf-8 -*- 2# Copyright 2019 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"""Module containing methods and classes to interact with a nebraska instance. 7""" 8 9from __future__ import print_function 10 11import base64 12import os 13import shutil 14import multiprocessing 15import subprocess 16 17from six.moves import urllib 18 19from autotest_lib.utils.frozen_chromite.lib import constants 20from autotest_lib.utils.frozen_chromite.lib import cros_build_lib 21from autotest_lib.utils.frozen_chromite.lib import cros_logging as logging 22from autotest_lib.utils.frozen_chromite.lib import gob_util 23from autotest_lib.utils.frozen_chromite.lib import osutils 24from autotest_lib.utils.frozen_chromite.lib import path_util 25from autotest_lib.utils.frozen_chromite.lib import remote_access 26from autotest_lib.utils.frozen_chromite.lib import timeout_util 27 28 29NEBRASKA_FILENAME = 'nebraska.py' 30 31# Error msg in loading shared libraries when running python command. 32ERROR_MSG_IN_LOADING_LIB = 'error while loading shared libraries' 33 34 35class Error(Exception): 36 """Base exception class of nebraska errors.""" 37 38 39class NebraskaStartupError(Error): 40 """Thrown when the nebraska fails to start up.""" 41 42 43class NebraskaStopError(Error): 44 """Thrown when the nebraska fails to stop.""" 45 46 47class RemoteNebraskaWrapper(multiprocessing.Process): 48 """A wrapper for nebraska.py on a remote device. 49 50 We assume there is no chroot on the device, thus we do not launch 51 nebraska inside chroot. 52 """ 53 NEBRASKA_TIMEOUT = 30 54 KILL_TIMEOUT = 10 55 56 # Keep in sync with nebraska.py if not passing these directly to nebraska. 57 RUNTIME_ROOT = '/run/nebraska' 58 PID_FILE_PATH = os.path.join(RUNTIME_ROOT, 'pid') 59 PORT_FILE_PATH = os.path.join(RUNTIME_ROOT, 'port') 60 LOG_FILE_PATH = '/tmp/nebraska.log' 61 REQUEST_LOG_FILE_PATH = '/tmp/nebraska_request_log.json' 62 63 NEBRASKA_PATH = os.path.join('/usr/local/bin', NEBRASKA_FILENAME) 64 65 def __init__(self, remote_device, nebraska_bin=None, 66 update_payloads_address=None, update_metadata_dir=None, 67 install_payloads_address=None, install_metadata_dir=None, 68 ignore_appid=False): 69 """Initializes the nebraska wrapper. 70 71 Args: 72 remote_device: A remote_access.RemoteDevice object. 73 nebraska_bin: The path to the nebraska binary. 74 update_payloads_address: The root address where the payloads will be 75 served. it can either be a local address (file://) or a remote 76 address (http://) 77 update_metadata_dir: A directory where json files for payloads required 78 for update are located. 79 install_payloads_address: Same as update_payloads_address for install 80 operations. 81 install_metadata_dir: Similar to update_metadata_dir but for install 82 payloads. 83 ignore_appid: True to tell Nebraska to ignore the update request's 84 App ID. This allows mismatching the source and target version boards. 85 One specific use case is updating between <board> and 86 <board>-kernelnext images. 87 """ 88 super(RemoteNebraskaWrapper, self).__init__() 89 90 self._device = remote_device 91 self._hostname = remote_device.hostname 92 93 self._update_payloads_address = update_payloads_address 94 self._update_metadata_dir = update_metadata_dir 95 self._install_payloads_address = install_payloads_address 96 self._install_metadata_dir = install_metadata_dir 97 self._ignore_appid = ignore_appid 98 99 self._nebraska_bin = nebraska_bin or self.NEBRASKA_PATH 100 101 self._port_file = self.PORT_FILE_PATH 102 self._pid_file = self.PID_FILE_PATH 103 self._log_file = self.LOG_FILE_PATH 104 105 self._port = None 106 self._pid = None 107 108 def _RemoteCommand(self, *args, **kwargs): 109 """Runs a remote shell command. 110 111 Args: 112 *args: See remote_access.RemoteDevice documentation. 113 **kwargs: See remote_access.RemoteDevice documentation. 114 """ 115 kwargs.setdefault('debug_level', logging.DEBUG) 116 return self._device.run(*args, **kwargs) 117 118 def _PortFileExists(self): 119 """Checks whether the port file exists in the remove device or not.""" 120 result = self._RemoteCommand( 121 ['test', '-f', self._port_file], check=False) 122 return result.returncode == 0 123 124 def _ReadPortNumber(self): 125 """Reads the port number from the port file on the remote device.""" 126 if not self.is_alive(): 127 raise NebraskaStartupError('Nebraska is not alive, so no port file yet!') 128 129 try: 130 timeout_util.WaitForReturnTrue(self._PortFileExists, period=5, 131 timeout=self.NEBRASKA_TIMEOUT) 132 except timeout_util.TimeoutError: 133 self.terminate() 134 raise NebraskaStartupError('Timeout (%s) waiting for remote nebraska' 135 ' port_file' % self.NEBRASKA_TIMEOUT) 136 137 self._port = int(self._RemoteCommand( 138 ['cat', self._port_file], capture_output=True).output.strip()) 139 140 def IsReady(self): 141 """Returns True if nebraska is ready to accept requests.""" 142 if not self.is_alive(): 143 raise NebraskaStartupError('Nebraska is not alive, so not ready!') 144 145 url = 'http://%s:%d/%s' % (remote_access.LOCALHOST_IP, self._port, 146 'health_check') 147 # Running curl through SSH because the port on the device is not accessible 148 # by default. 149 result = self._RemoteCommand( 150 ['curl', url, '-o', '/dev/null'], check=False) 151 return result.returncode == 0 152 153 def _WaitUntilStarted(self): 154 """Wait until the nebraska has started.""" 155 if not self._port: 156 self._ReadPortNumber() 157 158 try: 159 timeout_util.WaitForReturnTrue(self.IsReady, 160 timeout=self.NEBRASKA_TIMEOUT, 161 period=5) 162 except timeout_util.TimeoutError: 163 raise NebraskaStartupError('Nebraska did not start.') 164 165 self._pid = int(self._RemoteCommand( 166 ['cat', self._pid_file], capture_output=True).output.strip()) 167 logging.info('Started nebraska with pid %s', self._pid) 168 169 def run(self): 170 """Launches a nebraska process on the device. 171 172 Starts a background nebraska and waits for it to finish. 173 """ 174 logging.info('Starting nebraska on %s', self._hostname) 175 176 if not self._update_metadata_dir: 177 raise NebraskaStartupError( 178 'Update metadata directory location is not passed.') 179 180 cmd = [ 181 'python', self._nebraska_bin, 182 '--update-metadata', self._update_metadata_dir, 183 ] 184 185 if self._update_payloads_address: 186 cmd += ['--update-payloads-address', self._update_payloads_address] 187 if self._install_metadata_dir: 188 cmd += ['--install-metadata', self._install_metadata_dir] 189 if self._install_payloads_address: 190 cmd += ['--install-payloads-address', self._install_payloads_address] 191 if self._ignore_appid: 192 cmd += ['--ignore-appid'] 193 194 try: 195 self._RemoteCommand(cmd, stdout=True, stderr=subprocess.STDOUT) 196 except cros_build_lib.RunCommandError as err: 197 msg = 'Remote nebraska failed (to start): %s' % str(err) 198 logging.error(msg) 199 raise NebraskaStartupError(msg) 200 201 def Start(self): 202 """Starts the nebraska process remotely on the remote device.""" 203 if self.is_alive(): 204 logging.warning('Nebraska is already running, not running again.') 205 return 206 207 self.start() 208 self._WaitUntilStarted() 209 210 def Stop(self): 211 """Stops the nebraska instance if its running. 212 213 Kills the nebraska instance with SIGTERM (and SIGKILL if SIGTERM fails). 214 """ 215 logging.debug('Stopping nebraska instance with pid %s', self._pid) 216 if self.is_alive(): 217 self._RemoteCommand(['kill', str(self._pid)], check=False) 218 else: 219 logging.debug('Nebraska is not running, stopping nothing!') 220 return 221 222 self.join(self.KILL_TIMEOUT) 223 if self.is_alive(): 224 logging.warning('Nebraska is unstoppable. Killing with SIGKILL.') 225 try: 226 self._RemoteCommand(['kill', '-9', str(self._pid)]) 227 except cros_build_lib.RunCommandError as e: 228 raise NebraskaStopError('Unable to stop Nebraska: %s' % e) 229 230 def GetURL(self, ip=remote_access.LOCALHOST_IP, 231 critical_update=False, no_update=False): 232 """Returns the URL which the devserver is running on. 233 234 Args: 235 ip: The ip of running nebraska if different than localhost. 236 critical_update: Whether nebraska has to instruct the update_engine that 237 the update is a critical one or not. 238 no_update: Whether nebraska has to give a noupdate response even if it 239 detected an update. 240 241 Returns: 242 An HTTP URL that can be passed to the update_engine_client in --omaha_url 243 flag. 244 """ 245 query_dict = {} 246 if critical_update: 247 query_dict['critical_update'] = True 248 if no_update: 249 query_dict['no_update'] = True 250 query_string = urllib.parse.urlencode(query_dict) 251 252 return ('http://%s:%d/update/%s' % 253 (ip, self._port, (('?%s' % query_string) if query_string else ''))) 254 255 def PrintLog(self): 256 """Print Nebraska log to stdout.""" 257 if self._RemoteCommand( 258 ['test', '-f', self._log_file], check=False).returncode != 0: 259 logging.error('Nebraska log file %s does not exist on the device.', 260 self._log_file) 261 return 262 263 result = self._RemoteCommand(['cat', self._log_file], capture_output=True) 264 output = '--- Start output from %s ---\n' % self._log_file 265 output += result.output 266 output += '--- End output from %s ---' % self._log_file 267 return output 268 269 def CollectLogs(self, target_log): 270 """Copies the nebraska logs from the device. 271 272 Args: 273 target_log: The file to copy the log to from the device. 274 """ 275 try: 276 self._device.CopyFromDevice(self._log_file, target_log) 277 except (remote_access.RemoteAccessException, 278 cros_build_lib.RunCommandError) as err: 279 logging.error('Failed to copy nebraska logs from device, ignoring: %s', 280 str(err)) 281 282 def CollectRequestLogs(self, target_log): 283 """Copies the nebraska logs from the device. 284 285 Args: 286 target_log: The file to write the log to. 287 """ 288 if not self.is_alive(): 289 return 290 291 request_log_url = 'http://%s:%d/requestlog' % (remote_access.LOCALHOST_IP, 292 self._port) 293 try: 294 self._RemoteCommand( 295 ['curl', request_log_url, '-o', self.REQUEST_LOG_FILE_PATH]) 296 self._device.CopyFromDevice(self.REQUEST_LOG_FILE_PATH, target_log) 297 except (remote_access.RemoteAccessException, 298 cros_build_lib.RunCommandError) as err: 299 logging.error('Failed to get requestlog from nebraska. ignoring: %s', 300 str(err)) 301 302 def CheckNebraskaCanRun(self): 303 """Checks to see if we can start nebraska. 304 305 If the stateful partition is corrupted, Python or other packages needed for 306 rootfs update may be missing on |device|. 307 308 This will also use `ldconfig` to update library paths on the target 309 device if it looks like that's causing problems, which is necessary 310 for base images. 311 312 Raise NebraskaStartupError if nebraska cannot start. 313 """ 314 315 # Try to capture the output from the command so we can dump it in the case 316 # of errors. Note that this will not work if we were requested to redirect 317 # logs to a |log_file|. 318 cmd_kwargs = {'capture_output': True, 'stderr': subprocess.STDOUT} 319 cmd = ['python', self._nebraska_bin, '--help'] 320 logging.info('Checking if we can run nebraska on the device...') 321 try: 322 self._RemoteCommand(cmd, **cmd_kwargs) 323 except cros_build_lib.RunCommandError as e: 324 logging.warning('Cannot start nebraska.') 325 logging.warning(e.result.error) 326 if ERROR_MSG_IN_LOADING_LIB in str(e): 327 logging.info('Attempting to correct device library paths...') 328 try: 329 self._RemoteCommand(['ldconfig'], **cmd_kwargs) 330 self._RemoteCommand(cmd, **cmd_kwargs) 331 logging.info('Library path correction successful.') 332 return 333 except cros_build_lib.RunCommandError as e2: 334 logging.warning('Library path correction failed:') 335 logging.warning(e2.result.error) 336 raise NebraskaStartupError(e.result.error) 337 338 raise NebraskaStartupError(str(e)) 339 340 @staticmethod 341 def GetNebraskaSrcFile(source_dir, force_download=False): 342 """Returns path to nebraska source file. 343 344 nebraska is copied to source_dir, either from a local file or by 345 downloading from googlesource.com. 346 347 Args: 348 force_download: True to always download nebraska from googlesource.com. 349 """ 350 assert os.path.isdir(source_dir), ('%s must be a valid directory.' 351 % source_dir) 352 353 nebraska_path = os.path.join(source_dir, NEBRASKA_FILENAME) 354 checkout = path_util.DetermineCheckout() 355 if checkout.type == path_util.CHECKOUT_TYPE_REPO and not force_download: 356 # ChromeOS checkout. Copy existing file to destination. 357 local_src = os.path.join(constants.SOURCE_ROOT, 'src', 'platform', 358 'dev', 'nebraska', NEBRASKA_FILENAME) 359 assert os.path.isfile(local_src), "%s doesn't exist" % local_src 360 shutil.copy2(local_src, source_dir) 361 else: 362 # Download from googlesource. 363 logging.info('Downloading nebraska from googlesource') 364 nebraska_url_path = '%s/+/%s/%s?format=text' % ( 365 'chromiumos/platform/dev-util', 'refs/heads/main', 366 'nebraska/nebraska.py') 367 contents_b64 = gob_util.FetchUrl(constants.EXTERNAL_GOB_HOST, 368 nebraska_url_path) 369 osutils.WriteFile(nebraska_path, 370 base64.b64decode(contents_b64).decode('utf-8')) 371 372 return nebraska_path 373