224 lines
8.4 KiB
Python
224 lines
8.4 KiB
Python
|
|
# Lint as: python2, python3
|
||
|
|
# Copyright 2022 The Chromium OS Authors. All rights reserved.
|
||
|
|
# Use of this source code is governed by a BSD-style license that can be
|
||
|
|
# found in the LICENSE file.
|
||
|
|
|
||
|
|
from __future__ import absolute_import
|
||
|
|
from __future__ import division
|
||
|
|
from __future__ import print_function
|
||
|
|
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import time
|
||
|
|
|
||
|
|
from autotest_lib.client.common_lib import error
|
||
|
|
from autotest_lib.server.cros.minios import minios_util
|
||
|
|
from autotest_lib.server.cros.update_engine import update_engine_test
|
||
|
|
|
||
|
|
|
||
|
|
class MiniOsTest(update_engine_test.UpdateEngineTest):
|
||
|
|
"""
|
||
|
|
Base class that sets up helper objects/functions for NBR tests.
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
_MINIOS_CLIENT_CMD = 'minios_client'
|
||
|
|
_MINIOS_KERNEL_FLAG = 'cros_minios'
|
||
|
|
|
||
|
|
# Period to wait for firmware screen in seconds.
|
||
|
|
# Value based on Brya, which is the slowest so far.
|
||
|
|
_FIRMWARE_SCREEN_TIMEOUT = 30
|
||
|
|
|
||
|
|
# Number of times to attempt booting into MiniOS.
|
||
|
|
_MINIOS_BOOT_MAX_ATTEMPTS = 3
|
||
|
|
|
||
|
|
# Timeout periods, given in seconds.
|
||
|
|
_MINIOS_SHUTDOWN_TIMEOUT = 30
|
||
|
|
|
||
|
|
# Number of seconds to wait for the host to boot into MiniOS. Should always
|
||
|
|
# be greater than `_FIRMWARE_SCREEN_TIMEOUT`.
|
||
|
|
_MINIOS_WAIT_UP_TIME_SECONDS = 120
|
||
|
|
|
||
|
|
# Version reported to OMAHA/NEBRASKA for recovery.
|
||
|
|
_RECOVERY_VERSION = '0.0.0.0'
|
||
|
|
|
||
|
|
# Files used by the tests.
|
||
|
|
_DEPENDENCY_DIRS = ['bin', 'lib', 'lib64', 'libexec']
|
||
|
|
_DEPENDENCY_INSTALL_DIR = '/usr/local'
|
||
|
|
_MINIOS_TEMP_STATEFUL_DIR = '/usr/local/tmp/stateful'
|
||
|
|
_STATEFUL_DEV_IMAGE_NAME = 'dev_image_new'
|
||
|
|
|
||
|
|
def initialize(self, host):
|
||
|
|
"""
|
||
|
|
Sets default variables for the test.
|
||
|
|
|
||
|
|
@param host: The DUT we will be running on.
|
||
|
|
|
||
|
|
"""
|
||
|
|
super(MiniOsTest, self).initialize(host)
|
||
|
|
self._nebraska = None
|
||
|
|
self._servo = host.servo
|
||
|
|
self._servo.initialize_dut()
|
||
|
|
|
||
|
|
def cleanup(self):
|
||
|
|
"""Clean up minios autotests."""
|
||
|
|
if self._nebraska:
|
||
|
|
self._nebraska.stop()
|
||
|
|
super(MiniOsTest, self).cleanup()
|
||
|
|
# Make sure to reboot DUT into CroS in case of failures.
|
||
|
|
self._host.reboot()
|
||
|
|
|
||
|
|
def _boot_minios(self):
|
||
|
|
"""Boot the DUT into MiniOS."""
|
||
|
|
# Turn off usbkey to avoid booting into usb-recovery image.
|
||
|
|
self._servo.switch_usbkey('off')
|
||
|
|
psc = self._servo.get_power_state_controller()
|
||
|
|
psc.power_off()
|
||
|
|
psc.power_on(psc.REC_ON)
|
||
|
|
self._host.test_wait_for_shutdown(self._MINIOS_SHUTDOWN_TIMEOUT)
|
||
|
|
logging.info('Waiting for firmware screen')
|
||
|
|
time.sleep(self._FIRMWARE_SCREEN_TIMEOUT)
|
||
|
|
|
||
|
|
# Attempt multiple times to boot into MiniOS. If all attempts fail then
|
||
|
|
# this is some kind of firmware issue. Since we failed to boot an OS use
|
||
|
|
# servo to reset the unit and then report test failure.
|
||
|
|
attempts = 0
|
||
|
|
minios_is_up = False
|
||
|
|
while not minios_is_up and attempts < self._MINIOS_BOOT_MAX_ATTEMPTS:
|
||
|
|
# Use Ctrl+R shortcut to boot 'MiniOS
|
||
|
|
logging.info('Try to boot MiniOS')
|
||
|
|
self._servo.ctrl_r()
|
||
|
|
minios_is_up = self._host.wait_up(
|
||
|
|
timeout=self._MINIOS_WAIT_UP_TIME_SECONDS,
|
||
|
|
host_is_down=True)
|
||
|
|
attempts += 1
|
||
|
|
|
||
|
|
if minios_is_up:
|
||
|
|
# If mainfw_type is recovery then we are in MiniOS.
|
||
|
|
mainfw_type = self._host.run_output('crossystem mainfw_type')
|
||
|
|
if mainfw_type != 'recovery':
|
||
|
|
raise error.TestError(
|
||
|
|
'Boot to MiniOS - invalid firmware: %s.' % mainfw_type)
|
||
|
|
# There are multiple types of recovery images, make sure we booted
|
||
|
|
# into minios.
|
||
|
|
pattern = r'\b%s\b' % self._MINIOS_KERNEL_FLAG
|
||
|
|
if not re.search(pattern, self._host.get_cmdline()):
|
||
|
|
raise error.TestError(
|
||
|
|
'Boot to MiniOS - recovery image is not minios.')
|
||
|
|
else:
|
||
|
|
# Try to not leave unit on recovery firmware screen.
|
||
|
|
self._host.power_cycle()
|
||
|
|
raise error.TestError('Boot to MiniOS failed.')
|
||
|
|
|
||
|
|
def _create_minios_hostlog(self):
|
||
|
|
"""Create the minios hostlog file.
|
||
|
|
|
||
|
|
To ensure the recovery was successful we need to compare the update
|
||
|
|
events against expected update events. This function creates the hostlog
|
||
|
|
for minios before the recovery reboots the DUT.
|
||
|
|
|
||
|
|
"""
|
||
|
|
# Check that update logs exist.
|
||
|
|
if len(self._get_update_engine_logs()) < 1:
|
||
|
|
err_msg = 'update_engine logs are missing. Cannot verify recovery.'
|
||
|
|
raise error.TestFail(err_msg)
|
||
|
|
|
||
|
|
# Download the logs instead of reading it over the network since it will
|
||
|
|
# disappear after MiniOS reboots the DUT.
|
||
|
|
logfile = os.path.join(self.resultsdir, 'minios_update_engine.log')
|
||
|
|
self._host.get_file(self._UPDATE_ENGINE_LOG, logfile)
|
||
|
|
logfile_content = None
|
||
|
|
with open(logfile) as f:
|
||
|
|
logfile_content = f.read()
|
||
|
|
minios_hostlog = os.path.join(self.resultsdir, 'hostlog_minios')
|
||
|
|
with open(minios_hostlog, 'w') as fp:
|
||
|
|
# There are four expected hostlog events during recovery.
|
||
|
|
extract_logs = self._extract_request_logs(logfile_content)
|
||
|
|
json.dump(extract_logs[-4:], fp)
|
||
|
|
return minios_hostlog
|
||
|
|
|
||
|
|
def _install_test_dependencies(self, public_bucket=False):
|
||
|
|
"""
|
||
|
|
Install test dependencies from a downloaded stateful archive.
|
||
|
|
|
||
|
|
@param public_bucket: True to download stateful from a public bucket.
|
||
|
|
|
||
|
|
"""
|
||
|
|
if not self._job_repo_url:
|
||
|
|
raise error.TestError('No job repo url set.')
|
||
|
|
|
||
|
|
statefuldev_url = self._stage_stateful(public_bucket)
|
||
|
|
logging.info('Installing dependencies from %s', statefuldev_url)
|
||
|
|
|
||
|
|
# Create destination directories.
|
||
|
|
minios_dev_image_dir = os.path.join(self._MINIOS_TEMP_STATEFUL_DIR,
|
||
|
|
self._STATEFUL_DEV_IMAGE_NAME)
|
||
|
|
install_dirs = [
|
||
|
|
os.path.join(self._DEPENDENCY_INSTALL_DIR, dir)
|
||
|
|
for dir in self._DEPENDENCY_DIRS
|
||
|
|
]
|
||
|
|
self._run(['mkdir', '-p', minios_dev_image_dir] + install_dirs)
|
||
|
|
# Symlink the install dirs into the staging destination.
|
||
|
|
for dir in install_dirs:
|
||
|
|
self._run(['ln', '-s', dir, minios_dev_image_dir])
|
||
|
|
|
||
|
|
# Generate the list of stateful archive members that we want to extract.
|
||
|
|
members = [
|
||
|
|
os.path.join(self._STATEFUL_DEV_IMAGE_NAME, dir)
|
||
|
|
for dir in self._DEPENDENCY_DIRS
|
||
|
|
]
|
||
|
|
try:
|
||
|
|
self._download_and_extract_stateful(statefuldev_url,
|
||
|
|
self._MINIOS_TEMP_STATEFUL_DIR,
|
||
|
|
members=members,
|
||
|
|
keep_symlinks=True)
|
||
|
|
except error.AutoservRunError as e:
|
||
|
|
err_str = 'Failed to install the test dependencies'
|
||
|
|
raise error.TestFail('%s: %s' % (err_str, str(e)))
|
||
|
|
|
||
|
|
self._setup_python_symlinks()
|
||
|
|
|
||
|
|
# Clean-up unused files to save memory.
|
||
|
|
self._run(['rm', '-rf', self._MINIOS_TEMP_STATEFUL_DIR])
|
||
|
|
|
||
|
|
def _setup_python_symlinks(self):
|
||
|
|
"""
|
||
|
|
Create symlinks in the root to point to all python paths in /usr/local
|
||
|
|
for stateful installed python to work. This is needed because Gentoo
|
||
|
|
creates wrappers with hardcoded paths to the root (e.g. python-exec).
|
||
|
|
|
||
|
|
"""
|
||
|
|
for path in self._DEPENDENCY_DIRS:
|
||
|
|
self._run([
|
||
|
|
'find',
|
||
|
|
os.path.join(self._DEPENDENCY_INSTALL_DIR, path),
|
||
|
|
'-maxdepth', '1', '\(', '-name', 'python*', '-o', '-name',
|
||
|
|
'portage', '\)', '-exec', 'ln', '-s', '{}',
|
||
|
|
os.path.join('/usr', path), '\;'
|
||
|
|
])
|
||
|
|
|
||
|
|
def _start_nebraska(self, payload_url=None):
|
||
|
|
"""
|
||
|
|
Initialize and start nebraska on the DUT.
|
||
|
|
|
||
|
|
@param payload_url: The payload to served by nebraska.
|
||
|
|
|
||
|
|
"""
|
||
|
|
if not self._nebraska:
|
||
|
|
self._nebraska = minios_util.NebraskaService(
|
||
|
|
self, self._host, payload_url)
|
||
|
|
self._nebraska.start()
|
||
|
|
|
||
|
|
def _verify_reboot(self, old_boot_id):
|
||
|
|
"""
|
||
|
|
Verify that the unit rebooted using the boot_id.
|
||
|
|
|
||
|
|
@param old_boot_id A boot id value obtained before the
|
||
|
|
reboot.
|
||
|
|
|
||
|
|
"""
|
||
|
|
self._host.test_wait_for_shutdown(self._MINIOS_SHUTDOWN_TIMEOUT)
|
||
|
|
self._host.test_wait_for_boot(old_boot_id)
|