• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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