blob: 9ac271f1d726e3481ba794373cff0de6f2c18d63 [file] [log] [blame]
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Methods and classes to interact with a nebraska instance."""
import base64
import logging
import multiprocessing
import os
import shutil
import subprocess
import urllib.parse
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import gob_util
from chromite.lib import osutils
from chromite.lib import path_util
from chromite.lib import remote_access
from chromite.lib import timeout_util
NEBRASKA_FILENAME = "nebraska.py"
# Error msg in loading shared libraries when running python command.
ERROR_MSG_IN_LOADING_LIB = "error while loading shared libraries"
class Error(Exception):
"""Base exception class of nebraska errors."""
class NebraskaStartupError(Error):
"""Thrown when the nebraska fails to start up."""
class NebraskaStopError(Error):
"""Thrown when the nebraska fails to stop."""
class RemoteNebraskaWrapper(multiprocessing.Process):
"""A wrapper for nebraska.py on a remote device.
We assume there is no chroot on the device, thus we do not launch
nebraska inside chroot.
"""
NEBRASKA_TIMEOUT = 30
KILL_TIMEOUT = 10
# Keep in sync with nebraska.py if not passing these directly to nebraska.
RUNTIME_ROOT = "/run/nebraska"
PID_FILE_PATH = os.path.join(RUNTIME_ROOT, "pid")
PORT_FILE_PATH = os.path.join(RUNTIME_ROOT, "port")
LOG_FILE_PATH = "/tmp/nebraska.log"
NEBRASKA_PATH = os.path.join("/usr/local/bin", NEBRASKA_FILENAME)
def __init__(
self,
remote_device,
nebraska_bin=None,
update_payloads_address=None,
update_metadata_dir=None,
install_payloads_address=None,
install_metadata_dir=None,
ignore_appid=False,
) -> None:
"""Initializes the nebraska wrapper.
Args:
remote_device: A remote_access.RemoteDevice object.
nebraska_bin: The path to the nebraska binary.
update_payloads_address: The root address where the payloads will be
served. it can either be a local address (file://) or a remote
address (http://)
update_metadata_dir: A directory where json files for payloads
required for update are located.
install_payloads_address: Same as update_payloads_address for
install operations.
install_metadata_dir: Similar to update_metadata_dir but for install
payloads.
ignore_appid: True to tell Nebraska to ignore the update request's
App ID. This allows mismatching the source and target version
boards. One specific use case is updating between <board> and
<board>-kernelnext images.
"""
super().__init__()
self._device = remote_device
self._hostname = remote_device.hostname
self._update_payloads_address = update_payloads_address
self._update_metadata_dir = update_metadata_dir
self._install_payloads_address = install_payloads_address
self._install_metadata_dir = install_metadata_dir
self._ignore_appid = ignore_appid
self._nebraska_bin = nebraska_bin or self.NEBRASKA_PATH
self._port_file = self.PORT_FILE_PATH
self._pid_file = self.PID_FILE_PATH
self._log_file = self.LOG_FILE_PATH
self._port = None
self._pid = None
def _RemoteCommand(self, *args, **kwargs):
"""Runs a remote shell command.
Args:
*args: See remote_access.RemoteDevice documentation.
**kwargs: See remote_access.RemoteDevice documentation.
"""
kwargs.setdefault("debug_level", logging.DEBUG)
return self._device.run(*args, **kwargs)
def _PortFileExists(self):
"""Checks whether the port file exists in the remove device or not."""
result = self._RemoteCommand(
["test", "-f", self._port_file], check=False
)
return result.returncode == 0
def _ReadPortNumber(self) -> None:
"""Reads the port number from the port file on the remote device."""
if not self.is_alive():
raise NebraskaStartupError(
"Nebraska is not alive, so no port file yet!"
)
try:
timeout_util.WaitForReturnTrue(
self._PortFileExists, period=5, timeout=self.NEBRASKA_TIMEOUT
)
except timeout_util.TimeoutError:
self.terminate()
raise NebraskaStartupError(
"Timeout (%s) waiting for remote nebraska"
" port_file" % self.NEBRASKA_TIMEOUT
)
self._port = int(
self._RemoteCommand(
["cat", self._port_file], capture_output=True
).stdout.strip()
)
def IsReady(self):
"""Returns True if nebraska is ready to accept requests."""
if not self.is_alive():
raise NebraskaStartupError("Nebraska is not alive, so not ready!")
url = "http://%s:%d/%s" % (
remote_access.LOCALHOST_IP,
self._port,
"health_check",
)
# Running curl through SSH because the port on the device is not
# accessible by default.
result = self._RemoteCommand(
["curl", url, "-o", "/dev/null"], check=False
)
return result.returncode == 0
def _WaitUntilStarted(self) -> None:
"""Wait until the nebraska has started."""
if not self._port:
self._ReadPortNumber()
try:
timeout_util.WaitForReturnTrue(
self.IsReady, timeout=self.NEBRASKA_TIMEOUT, period=5
)
except timeout_util.TimeoutError:
raise NebraskaStartupError("Nebraska did not start.")
self._pid = int(
self._RemoteCommand(
["cat", self._pid_file], capture_output=True
).stdout.strip()
)
logging.info("Started nebraska with pid %s", self._pid)
def run(self) -> None:
"""Launches a nebraska process on the device.
Starts a background nebraska and waits for it to finish.
"""
logging.info("Starting nebraska on %s", self._hostname)
if not self._update_metadata_dir:
raise NebraskaStartupError(
"Update metadata directory location is not passed."
)
cmd = [
self._nebraska_bin,
"--update-metadata",
self._update_metadata_dir,
]
if self._update_payloads_address:
cmd += ["--update-payloads-address", self._update_payloads_address]
if self._install_metadata_dir:
cmd += ["--install-metadata", self._install_metadata_dir]
if self._install_payloads_address:
cmd += [
"--install-payloads-address",
self._install_payloads_address,
]
if self._ignore_appid:
cmd += ["--ignore-appid"]
try:
self._RemoteCommand(cmd, stdout=True, stderr=subprocess.STDOUT)
except cros_build_lib.RunCommandError as err:
msg = "Remote nebraska failed (to start): %s" % str(err)
logging.error(msg)
raise NebraskaStartupError(msg)
def Start(self) -> None:
"""Starts the nebraska process remotely on the remote device."""
if self.is_alive():
logging.warning("Nebraska is already running, not running again.")
return
self.start()
self._WaitUntilStarted()
def Stop(self) -> None:
"""Stops the nebraska instance if its running.
Kills the nebraska instance with SIGTERM (and SIGKILL if SIGTERM fails).
"""
logging.debug("Stopping nebraska instance with pid %s", self._pid)
if self.is_alive():
self._RemoteCommand(["kill", str(self._pid)], check=False)
else:
logging.debug("Nebraska is not running, stopping nothing!")
return
self.join(self.KILL_TIMEOUT)
if self.is_alive():
logging.warning("Nebraska is unstoppable. Killing with SIGKILL.")
try:
self._RemoteCommand(["kill", "-9", str(self._pid)])
except cros_build_lib.RunCommandError as e:
raise NebraskaStopError("Unable to stop Nebraska: %s" % e)
def GetURL(
self,
ip=remote_access.LOCALHOST_IP,
critical_update=False,
no_update=False,
):
"""Returns the URL which the devserver is running on.
Args:
ip: The ip of running nebraska if different from localhost.
critical_update: Whether nebraska has to instruct the update_engine
that the update is a critical one or not.
no_update: Whether nebraska has to give a noupdate response even if
it detected an update.
Returns:
An HTTP URL that can be passed to the update_engine_client in
--omaha_url flag.
"""
query_dict = {}
if critical_update:
query_dict["critical_update"] = True
if no_update:
query_dict["no_update"] = True
query_string = urllib.parse.urlencode(query_dict)
return "http://%s:%d/update/%s" % (
ip,
self._port,
(("?%s" % query_string) if query_string else ""),
)
def PrintLog(self):
"""Print Nebraska log to stdout."""
if (
self._RemoteCommand(
["test", "-f", self._log_file], check=False
).returncode
!= 0
):
logging.error(
"Nebraska log file %s does not exist on the device.",
self._log_file,
)
return
result = self._RemoteCommand(
["cat", self._log_file], capture_output=True
)
output = "--- Start output from %s ---\n" % self._log_file
output += result.stdout
output += "--- End output from %s ---" % self._log_file
return output
def CollectLogs(self, target_log) -> None:
"""Copies the nebraska logs from the device.
Args:
target_log: The file to copy the log to from the device.
"""
try:
self._device.CopyFromDevice(self._log_file, target_log)
except (
remote_access.RemoteAccessException,
cros_build_lib.RunCommandError,
) as err:
logging.error(
"Failed to copy nebraska logs from device, ignoring: %s",
str(err),
)
def CheckNebraskaCanRun(self) -> None:
"""Checks to see if we can start nebraska.
If the stateful partition is corrupted, Python or other packages needed
for rootfs update may be missing on |device|.
This will also use `ldconfig` to update library paths on the target
device if it looks like that's causing problems, which is necessary
for base images.
Raise NebraskaStartupError if nebraska cannot start.
"""
# Try to capture the output from the command, so we can dump it in the
# case of errors. Note that this will not work if we were requested to
# redirect logs to a |log_file|.
cmd_kwargs = {"stdout": True, "stderr": subprocess.STDOUT}
cmd = ["python", self._nebraska_bin, "--help"]
logging.info("Checking if we can run nebraska on the device...")
try:
self._RemoteCommand(cmd, **cmd_kwargs)
except cros_build_lib.RunCommandError as e:
logging.warning("Cannot start nebraska.")
logging.warning(e.stderr)
if ERROR_MSG_IN_LOADING_LIB in str(e):
logging.info("Attempting to correct device library paths...")
try:
self._RemoteCommand(["ldconfig"], **cmd_kwargs)
self._RemoteCommand(cmd, **cmd_kwargs)
logging.info("Library path correction successful.")
return
except cros_build_lib.RunCommandError as e2:
logging.warning("Library path correction failed:")
logging.warning(e2.stderr)
raise NebraskaStartupError(e.stderr)
raise NebraskaStartupError(str(e))
@staticmethod
def GetNebraskaSrcFile(source_dir):
"""Returns path to nebraska source file.
nebraska is copied to source_dir, either from a local file or by
downloading from googlesource.com.
"""
assert os.path.isdir(source_dir), (
"%s must be a valid directory." % source_dir
)
nebraska_path = os.path.join(source_dir, NEBRASKA_FILENAME)
checkout = path_util.DetermineCheckout()
if checkout.type == path_util.CheckoutType.REPO:
# ChromeOS checkout. Copy existing file to destination.
local_src = os.path.join(
constants.SOURCE_ROOT,
"src",
"platform",
"dev",
"nebraska",
NEBRASKA_FILENAME,
)
assert os.path.isfile(local_src), "%s doesn't exist" % local_src
shutil.copy2(local_src, source_dir)
else:
# Download from googlesource.
nebraska_url_path = "%s/+/%s/%s?format=text" % (
"chromiumos/platform/dev-util",
"HEAD",
"nebraska/nebraska.py",
)
contents_b64 = gob_util.FetchUrl(
constants.EXTERNAL_GOB_HOST, nebraska_url_path
)
osutils.WriteFile(
nebraska_path, base64.b64decode(contents_b64).decode("utf-8")
)
return nebraska_path