blob: 60dec32cf3c775388cf181a58a26a14757634197 [file] [log] [blame]
# Copyright 2013 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Check release or test OS image version on internal storage.
Description
-----------
This test checks if Chrome OS test image or release image version in
'/etc/lsb-release' are greater than or equal to the value of argument
``min_version``, and/or checks if the version are smaller or equal to
``max_version``. If the version is too old or too new and argument
``reimage`` is set to True, download and apply either remote netboot
firmware or cros_payload components from factory server.
Note when use_netboot is specified, the DUT will reboot into netboot firmware
and start network image installation process to re-image.
Test Procedure
--------------
1. This test will first check if test image (when ``check_release_image`` is
False) or release image (when ``check_release_image`` is True) version >=
``min_version``, and/or version <= ``max_version`` If so, this test will
pass.
2. If ``reimage`` is set to False, this test will fail.
3. If ``require_space`` is set to True, this test will wait for the user presses
spacebar.
4. If ``use_netboot`` is set to True, this test will try to download
netboot firmware from factory server and flash into AP firmware. Otherwise,
download and install the selected component using ``cros_payload`` command.
If the needed components are not available on factory server, this test will
fail.
5. If ``use_netboot`` is set to True, The DUT will then reboot into netboot
firmware and start network image installation process to reimage.
Dependency
----------
- If argument ``reimage`` is set to True, factory server must be set up and be
ready for network image installation.
- If argument ``use_netboot`` is set to True, netboot firmware must be
available on factory server.
Examples
--------
To check test image version is greater than or equal to 9876.5.4, add this in
test list::
{
"pytest_name": "check_image_version",
"args": {
"min_version": "9876.5.4",
"reimage": false
}
}
To check test image version is exact the same as 9876.5.4, add this in test
list::
{
"pytest_name": "check_image_version",
"args": {
"min_version": "9876.5.4",
"max_version": "9876.5.4",
"reimage": false
}
}
Reimage if release image version is older than 9876.5.4 by flashing netboot
firmware and make pressing spacebar not needed::
{
"pytest_name": "check_image_version",
"args": {
"min_version": "9876.5.4",
"check_release_image": true,
"require_space": false
}
}
Reimage if test image version is greater than or equal to 9876.5.2012_12_21_2359
(loose format version) by flashing netboot firmware image on factory server::
{
"pytest_name": "check_image_version",
"args": {
"min_version": "9876.5.2012_12_21_2359",
"loose_version": true
}
}
Reimage if release image is older than the release_image version on factory
server using cros_payload::
{
"pytest_name": "check_image_version",
"args": {
"check_release_image": true,
"use_netboot": false
}
}
To re-install the DLCs extracted from a release image::
{
"pytest_name": "check_image_version",
"args": {
"check_release_image": true,
"use_netboot": false,
"reinstall_only_dlc": true
}
}
"""
from distutils import version
import logging
import os
import re
from cros.factory.device import device_utils
from cros.factory.test.i18n import _
from cros.factory.test import test_case
from cros.factory.test import test_ui
from cros.factory.test.utils import deploy_utils
from cros.factory.test.utils import update_utils
from cros.factory.testlog import testlog
from cros.factory.utils.arg_utils import Arg
from cros.factory.utils import type_utils
_RE_CROS_PAYLOAD_ERROR = re.compile(r'ERROR: .*')
_RE_BRANCHED_IMAGE_VERSION = re.compile(r'R\d+-(\d+\.\d+\.\d+)(?:-b\d+)*')
class CheckImageVersionTest(test_case.TestCase):
related_components = tuple()
ARGS = [
Arg('min_version', str,
('Minimum allowed test or release image version.'
'None to skip the check. If both min_version and max_version are set'
'to None, the check will follow the version on factory server.'),
default=None),
Arg('max_version', str,
('Maximum allowed test or release image version.'
'None to skip the check. If both min_version and max_version are set'
'to None, the check will follow the version on factory server.'),
default=None),
Arg('loose_version', bool, 'Allow any version number representation.',
default=False),
Arg('reimage', bool, 'True to re-image when image version mismatch.',
default=True),
Arg('require_space', bool,
'True to require a space key press before re-imaging.', default=True),
Arg('check_release_image', bool,
'True to check release image instead of test image.', default=False),
Arg('verify_rootfs', bool, 'True to verify rootfs before install image.',
default=True),
Arg('use_netboot', bool,
'True to image with netboot, otherwise cros_payload.', default=True),
Arg(
'reinstall_only_dlc', bool,
'True to reinstall only DLCs from the release image on factory '
'server. The release image version on factory server must match '
'with the release image version on DUT.', default=False)
]
ui_class = test_ui.ScrollableLogUI
def setUp(self):
self.dut = device_utils.CreateDUTInterface()
# yapf: disable
if self.args.reinstall_only_dlc: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.reinstall_reason = 'reinstall_only_dlc is set to true'
else:
self.reinstall_reason = 'Image version is incorrect'
self.dut_image_version = None
def WaitNetworkReady(self):
while not self.dut.status.eth_on:
# yapf: disable
self.ui.SetInstruction(_('Please connect to ethernet.')) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.Sleep(0.5)
def runTest(self):
self.dut_image_version = self.GetAndLogDUTImageVersion()
# yapf: disable
if self.args.reinstall_only_dlc: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
logging.info('Reinstall only DLCs...')
else:
# If this test stop unexpectedly during installing new image, we need to
# check image version and verify Root FS to ensure the DUT is
# successfully installed or not. The partition of Root FS is the last one
# to be written, so the image version from lsb-release and the
# verification of Root FS can ensure the result of installation.
if self.CheckImageVersion() and self.VerifyRootFs():
return
# yapf: disable
if not self.args.reimage: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.FailTask('Image version is incorrect. '
'Please re-image this device.')
# yapf: disable
if not self.args.use_netboot: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.assertTrue(
# yapf: disable
self.args.check_release_image, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'Please use netboot if you would like to re-image test image!')
# yapf: disable
if self.args.reinstall_only_dlc: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.min_version = self.server_image_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.max_version = self.server_image_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if not self.CheckImageVersion():
self.FailTask('The release image version on factory server must match '
'with the release image version on DUT! Otherwise, '
'finalize will fail when verifying the DLCs.')
# yapf: disable
if self.args.require_space: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.ui.SetInstruction( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
_('{reason}. Press space to reinstall.',
reason=self.reinstall_reason))
# yapf: disable
self.ui.WaitKeysOnce(test_ui.SPACE_KEY) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if self.args.use_netboot: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
component = update_utils.Components.netboot_firmware
destination = None
callback = self.NetbootCallback
else:
# yapf: disable
if self.args.reinstall_only_dlc: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
component = update_utils.Components.dlc_factory_cache
else:
component = update_utils.Components.release_image
destination = self.dut.partitions.rootdev
callback = None
self.ReImage(component, destination, callback)
def NetbootCallback(self, component, destination, url):
# TODO(hungte) Should we merge this with flash_netboot.py?
del url # Unused.
fw_path = os.path.join(destination, component)
# yapf: disable
self.ui.SetInstruction(_('Flashing {component}...', component=component)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
try:
if self.dut.link.IsLocal():
# yapf: disable
self.ui.PipeProcessOutputToUI( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
['flash_netboot', '-y', '-i', fw_path, '--no-reboot'])
else:
with self.dut.temp.TempFile() as temp_file:
self.dut.link.Push(fw_path, temp_file)
factory_par = deploy_utils.CreateFactoryTools(self.dut)
factory_par.CheckCall(
['flash_netboot', '-y', '-i', temp_file, '--no-reboot'], log=True)
self.dut.CheckCall(['reboot'], log=True)
except Exception:
self.FailTask('Error flashing netboot firmware!')
else:
self.FailTask(f'{self.reinstall_reason}. DUT is rebooting to reimage.')
def ReImage(self, component: update_utils.Components, destination, callback):
def ReInstallCallBack(line):
# Check the output of the `cros_payload`.
if _RE_CROS_PAYLOAD_ERROR.match(line):
raise Exception(f'Installation failed! Reason: {line}')
updater = update_utils.Updater(
# yapf: disable
component, spawn=lambda cmd: self.ui.PipeProcessOutputToUI( # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
cmd, callback=ReInstallCallBack))
if not updater.IsUpdateAvailable():
self.FailTask(f'{component} not available on factory server.')
# yapf: disable
self.ui.SetInstruction(_('Updating {component}....', component=component)) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
updater.PerformUpdate(destination=destination, callback=callback)
def CheckImageVersion(self):
def _GetExpectedVersion(expected_ver, cur_match, pattern, reimage):
if not expected_ver:
return expected_ver
expected_match = pattern.fullmatch(expected_ver)
if expected_match:
expected_ver = expected_match.group(1)
if reimage and bool(cur_match) ^ bool(expected_match):
logging.info('Attempt to re-image between different branch')
return expected_ver
# yapf: disable
if self.args.min_version is None and self.args.max_version is None: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# TODO(hungte) In future if we find it useful to reflash netboot for
# updating test image, we can add test_image to update_utils and enable
# fetching version here.
self.assertTrue(
# yapf: disable
self.args.check_release_image, # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
'Empty min_version and max_version only allowed for '
'check_release_image.')
# yapf: disable
self.args.min_version = self.server_image_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
self.args.max_version = self.server_image_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
if not self.args.min_version or not self.args.max_version: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
self.FailTask('Release image not available on factory server.')
# yapf: disable
expected_min = self.args.min_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
expected_max = self.args.max_version # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
# yapf: disable
version_format = (version.LooseVersion if self.args.loose_version else # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
version.StrictVersion)
ver = self.dut_image_version
logging.info('Using version format: %r', version_format.__name__)
logging.info(
'current version: %r, expected min version: %r, '
'expected max version %r', ver, expected_min, expected_max)
# For image built by tryjob, the image version will look like this:
# `R89-13600.271.0-b5006899`. We do not compare the sub-verion of tryjob
# image, which is `5006899` in this example, since it is meaningless.
# yapf: disable
ver_match = _RE_BRANCHED_IMAGE_VERSION.fullmatch(ver) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if ver_match:
ver = ver_match.group(1)
expected_min = _GetExpectedVersion(
# yapf: disable
expected_min, ver_match, _RE_BRANCHED_IMAGE_VERSION, self.args.reimage) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
expected_max = _GetExpectedVersion(
# yapf: disable
expected_max, ver_match, _RE_BRANCHED_IMAGE_VERSION, self.args.reimage) # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
if expected_min and expected_max:
if version_format(expected_min) > version_format(expected_max):
self.FailTask(
'Expected min version is greater than expected max version!')
if expected_min and version_format(ver) < version_format(expected_min):
return False
if expected_max and version_format(ver) > version_format(expected_max):
return False
return True
def VerifyRootFs(self):
# yapf: disable
if self.args.check_release_image and self.args.verify_rootfs: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
factory_tool = deploy_utils.CreateFactoryTools(self.dut)
exit_code = factory_tool.Call(
['gooftool', 'verify_rootfs', '--release_rootfs',
self.dut.partitions.RELEASE_ROOTFS.path])
return exit_code == 0
return True
def GetAndLogDUTImageVersion(self):
# yapf: disable
if self.args.check_release_image: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
ver = self.dut.info.release_image_version
name = 'release_image'
else:
ver = self.dut.info.factory_image_version
name = 'test_image'
if ver is None:
logging.warning('Can\'t find %s version on DUT!', name)
else:
logging.info('Current DUT %s version: %s', name, ver)
testlog.LogParam(name=name, value=ver)
return ver
@type_utils.LazyProperty
def server_image_version(self):
# yapf: disable
if not self.args.check_release_image: # type: ignore #TODO(b/338318729) Fixit! # pylint: disable=line-too-long
# yapf: enable
return None
self.WaitNetworkReady()
updater = update_utils.Updater(update_utils.Components.release_image)
# The 'release_image' components in cros_payload are using
# CHROMEOS_RELEASE_DESCRIPTION for version string.
# The version format looks like this:
# 'xxxxx.x.x (Official Build) dev-channel board'
ver = updater.GetUpdateVersion()
logging.info('Get image version from server: %s', ver)
# Return only the numeric part.
return ver.split()[0]