| # -*- coding: utf-8 -*- |
| # Copyright 2019 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. |
| |
| """Library containing functions to transfer files onto a remote device. |
| |
| Transfer Base class includes: |
| |
| ----Tranfer---- |
| * @retry functionality for all public transfer functions. |
| |
| LocalTransfer includes: |
| |
| ----Precheck--- |
| * Pre-check payload's existence before auto-update. |
| |
| ----Tranfer---- |
| * Transfer update-utils (nebraska, et. al.) package at first. |
| * Transfer rootfs update files if rootfs update is required. |
| * Transfer stateful update files if stateful update is required. |
| |
| LabTransfer includes: |
| |
| ----Precheck--- |
| * Pre-check payload's existence on the staging server before auto-update. |
| |
| ----Tranfer---- |
| * Download the update-utils (nebraska, et. al.) package onto the DUT directly |
| from the staging server at first. |
| * Download rootfs update files onto the DUT directly from the staging server |
| if rootfs update is required. |
| * Download stateful update files onto the DUT directly from the staging server |
| if stateful update is required. |
| """ |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| |
| import abc |
| import json |
| import os |
| import re |
| |
| import six |
| from six.moves import urllib |
| |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import metrics |
| from chromite.lib import nebraska_wrapper |
| from chromite.lib import osutils |
| from chromite.lib import retry_util |
| |
| # Naming conventions for global variables: |
| # Path on remote host with slash: REMOTE_XXX_PATH |
| # File on local server without slash: LOCAL_XXX_FILENAME |
| # Path on local server: LOCAL_XXX_PATH |
| |
| # Max number of the times for retry: |
| # 1. for transfer functions to be retried. |
| # 2. for some retriable commands to be retried. |
| _MAX_RETRY = 5 |
| |
| # The delay between retriable tasks. |
| _DELAY_SEC_FOR_RETRY = 5 |
| |
| # Update file names for rootfs+kernel and stateful partitions. |
| ROOTFS_FILENAME = 'update.gz' |
| STATEFUL_FILENAME = 'stateful.tgz' |
| |
| # Regular expression that is used to evaluate payload names to determine payload |
| # validity. |
| _PAYLOAD_PATTERN = r'payloads/chromeos_(?P<image_version>[^_]+)_.*' |
| |
| # File copying modes. |
| _RSYNC = 'rsync' |
| _SCP = 'scp' |
| |
| |
| class Error(Exception): |
| """A generic auto updater transfer error.""" |
| |
| |
| class ChromiumOSTransferError(Error): |
| """Thrown when there is a general transfer specific error.""" |
| |
| |
| def GetPayloadPropertiesFileName(payload): |
| """Returns the payload properties file given the path to the payload.""" |
| return payload + '.json' |
| |
| |
| class Transfer(six.with_metaclass(abc.ABCMeta, object)): |
| """Abstract Base Class that handles payload precheck and transfer.""" |
| |
| PAYLOAD_DIR_NAME = 'payloads' |
| |
| def __init__(self, device, payload_dir, tempdir, |
| payload_name, cmd_kwargs, device_payload_dir, |
| payload_mode='scp', transfer_stateful_update=True, |
| transfer_rootfs_update=True): |
| """Initialize Base Class for transferring payloads functionality. |
| |
| Args: |
| device: The ChromiumOSDevice to be updated. |
| payload_dir: The directory of payload(s). |
| tempdir: The temp directory in caller, not in the device. For example, |
| the tempdir for cros flash is /tmp/cros-flash****/, used to |
| temporarily keep files when transferring update-utils package, and |
| reserve nebraska and update engine logs. |
| payload_name: Filename of exact payload file to use for update. |
| cmd_kwargs: Keyword arguments that are sent along with the commands that |
| are run on the device. |
| device_payload_dir: Path to the payload directory in the device's work |
| directory. |
| payload_mode: The payload mode - it can be 'parallel' or 'scp'. |
| transfer_stateful_update: Whether to transfer payloads necessary for |
| stateful update. The default is True. |
| transfer_rootfs_update: Whether to transfer payloads necessary for |
| rootfs update. The default is True. |
| """ |
| self._device = device |
| self._payload_dir = payload_dir |
| self._tempdir = tempdir |
| self._payload_name = payload_name |
| self._cmd_kwargs = cmd_kwargs |
| self._device_payload_dir = device_payload_dir |
| if payload_mode not in ('scp', 'parallel'): |
| raise ValueError('The given value %s for payload mode is not valid.' % |
| payload_mode) |
| self._payload_mode = payload_mode |
| self._transfer_stateful_update = transfer_stateful_update |
| self._transfer_rootfs_update = transfer_rootfs_update |
| self._local_payload_props_path = None |
| |
| @property |
| def mode(self): |
| """Mode for copying files.""" |
| return _RSYNC |
| |
| @abc.abstractmethod |
| def CheckPayloads(self): |
| """Verify that all required payloads are in |self.payload_dir|.""" |
| |
| def TransferUpdateUtilsPackage(self): |
| """Transfer update-utils package to work directory of the remote device.""" |
| retry_util.RetryException( |
| cros_build_lib.RunCommandError, |
| _MAX_RETRY, |
| self._TransferUpdateUtilsPackage, |
| delay_sec=_DELAY_SEC_FOR_RETRY) |
| |
| def TransferRootfsUpdate(self): |
| """Transfer files for rootfs update. |
| |
| The corresponding payloads are copied to the remote device for rootfs |
| update. |
| """ |
| retry_util.RetryException( |
| cros_build_lib.RunCommandError, |
| _MAX_RETRY, |
| self._TransferRootfsUpdate, |
| delay_sec=_DELAY_SEC_FOR_RETRY) |
| |
| def TransferStatefulUpdate(self): |
| """Transfer files for stateful update. |
| |
| The stateful update bin and the corresponding payloads are copied to the |
| target remote device for stateful update. |
| """ |
| retry_util.RetryException( |
| cros_build_lib.RunCommandError, |
| _MAX_RETRY, |
| self._TransferStatefulUpdate, |
| delay_sec=_DELAY_SEC_FOR_RETRY) |
| |
| def _EnsureDeviceDirectory(self, directory): |
| """Mkdir the directory no matther whether this directory exists on host. |
| |
| Args: |
| directory: The directory to be made on the device. |
| """ |
| self._device.run(['mkdir', '-p', directory], **self._cmd_kwargs) |
| |
| @abc.abstractmethod |
| def GetPayloadPropsFile(self): |
| """Get the payload properties file path.""" |
| |
| @abc.abstractmethod |
| def GetPayloadProps(self): |
| """Gets properties necessary to fix the payload properties file. |
| |
| Returns: |
| Dict in the format: {'image_version': 12345.0.0, 'size': 123456789}. |
| """ |
| |
| def _GetPayloadFormat(self): |
| """Gets the payload format that should be evaluated. |
| |
| Returns: |
| The payload name as a string. |
| """ |
| return self._payload_name |
| |
| def _GetPayloadPattern(self): |
| """The regex pattern that the payload format must match. |
| |
| Returns: |
| Regular expression. |
| """ |
| return _PAYLOAD_PATTERN |
| |
| |
| class LocalTransfer(Transfer): |
| """Abstracts logic that handles transferring local files to the DUT.""" |
| |
| def __init__(self, *args, **kwargs): |
| """Initialize LocalTransfer to handle transferring files from local to DUT. |
| |
| Args: |
| *args: The list of arguments to be passed. See Base class for a complete |
| list of accepted arguments. |
| **kwargs: Any keyword arguments to be passed. See Base class for a |
| complete list of accepted keyword arguments. |
| """ |
| super(LocalTransfer, self).__init__(*args, **kwargs) |
| |
| def CheckPayloads(self): |
| """Verify that all required payloads are in |self.payload_dir|.""" |
| logging.debug('Checking if payloads have been stored in directory %s...', |
| self._payload_dir) |
| filenames = [] |
| |
| if self._transfer_rootfs_update: |
| filenames += [self._payload_name, |
| GetPayloadPropertiesFileName(self._payload_name)] |
| |
| if self._transfer_stateful_update: |
| filenames += [STATEFUL_FILENAME] |
| |
| for fname in filenames: |
| payload = os.path.join(self._payload_dir, fname) |
| if not os.path.exists(payload): |
| raise ChromiumOSTransferError('Payload %s does not exist!' % payload) |
| |
| def _TransferUpdateUtilsPackage(self): |
| """Transfer update-utils package to work directory of the remote device.""" |
| logging.notice('Copying update script to device...') |
| source_dir = os.path.join(self._tempdir, 'src') |
| osutils.SafeMakedirs(source_dir) |
| nebraska_wrapper.RemoteNebraskaWrapper.GetNebraskaSrcFile(source_dir) |
| |
| # Make sure the device.work_dir exists after any installation and reboot. |
| self._EnsureDeviceDirectory(self._device.work_dir) |
| # Python packages are plain text files so we chose rsync --compress. |
| self._device.CopyToWorkDir(source_dir, mode='rsync', log_output=True, |
| **self._cmd_kwargs) |
| |
| def _TransferRootfsUpdate(self): |
| """Transfer files for rootfs update. |
| |
| Copy the update payload to the remote device for rootfs update. |
| """ |
| self._EnsureDeviceDirectory(self._device_payload_dir) |
| logging.notice('Copying rootfs payload to device...') |
| payload = os.path.join(self._payload_dir, self._payload_name) |
| self._device.CopyToWorkDir(payload, self.PAYLOAD_DIR_NAME, |
| mode=self._payload_mode, |
| log_output=True, **self._cmd_kwargs) |
| payload_properties_path = GetPayloadPropertiesFileName(payload) |
| self._device.CopyToWorkDir(payload_properties_path, self.PAYLOAD_DIR_NAME, |
| mode=self._payload_mode, |
| log_output=True, **self._cmd_kwargs) |
| |
| def _TransferStatefulUpdate(self): |
| """Transfer files for stateful update. |
| |
| The stateful update payloads are copied to the target remote device for |
| stateful update. |
| """ |
| logging.notice('Copying target stateful payload to device...') |
| payload = os.path.join(self._payload_dir, STATEFUL_FILENAME) |
| self._device.CopyToWorkDir(payload, mode=self._payload_mode, |
| log_output=True, **self._cmd_kwargs) |
| |
| def GetPayloadPropsFile(self): |
| """Finds the local payload properties file.""" |
| # Payload properties file is available locally so just catch it next to the |
| # payload file. |
| if self._local_payload_props_path is None: |
| self._local_payload_props_path = os.path.join( |
| self._payload_dir, GetPayloadPropertiesFileName(self._payload_name)) |
| return self._local_payload_props_path |
| |
| def GetPayloadProps(self): |
| """Gets image_version from the payload_name and size of the payload. |
| |
| The payload_dir must be in the format <board>/Rxx-12345.0.0 for a complete |
| match; else a ValueError will be raised. In case the payload filename is |
| update.gz, then image_version cannot be extracted from its name; therefore, |
| image_version is set to a dummy 99999.0.0. |
| |
| Returns: |
| Dict - See parent class's function for full details. |
| """ |
| payload_filepath = os.path.join(self._payload_dir, self._payload_name) |
| values = { |
| 'image_version': '99999.0.0', |
| 'size': os.path.getsize(payload_filepath) |
| } |
| if self._payload_name != ROOTFS_FILENAME: |
| payload_format = self._GetPayloadFormat() |
| payload_pattern = self._GetPayloadPattern() |
| m = re.match(payload_pattern, payload_format) |
| if not m: |
| raise ValueError( |
| 'Regular expression %r did not match the expected payload format ' |
| '%s' % (payload_pattern, payload_format)) |
| values.update(m.groupdict()) |
| return values |
| |
| |
| class LabTransfer(Transfer): |
| """Abstracts logic that transfers files from staging server to the DUT.""" |
| |
| def __init__(self, staging_server, *args, **kwargs): |
| """Initialize LabTransfer to transfer files from staging server to DUT. |
| |
| Args: |
| staging_server: Url of the server that's staging the payload files. |
| *args: The list of arguments to be passed. See Base class for a complete |
| list of accepted arguments. |
| **kwargs: Any keyword arguments to be passed. See Base class for a |
| complete list of accepted keyword arguments. |
| """ |
| self._staging_server = staging_server |
| self._fallback_metrics = metrics.Counter( |
| 'chromeos/autotest/devserver/remote_devserver_call') |
| super(LabTransfer, self).__init__(*args, **kwargs) |
| |
| @property |
| def mode(self): |
| """Mode for copying files.""" |
| return _SCP |
| |
| def _GetPayloadFormat(self): |
| """Gets the payload format that should be evaluated. |
| |
| Returns: |
| The payload dir as a string. |
| """ |
| return self._payload_dir |
| |
| def _GetPayloadPattern(self): |
| """The regex pattern that the payload format must match. |
| |
| Returns: |
| Regular expression. |
| """ |
| return r'.*/(R[0-9]+-)(?P<image_version>.+)' |
| |
| def _UpdateFallbackMetrics(self, ssh_used, success, hostname=None): |
| """Updates metrics that track the fallback devserver calls. |
| |
| Args: |
| ssh_used: Indicates whether the devserver was ssh-ed into first. |
| success: Whether the command completed successfully or not. |
| hostname: Hostname of the devserver on which payloads are staged. If None, |
| then the hostname is derived from the self._staging_server URL. |
| """ |
| if not hostname: |
| hostname = urllib.parse.urlparse(self._staging_server).hostname |
| |
| self._fallback_metrics.increment(fields={'dev_server': hostname, |
| 'ssh_used': ssh_used, |
| 'success': success}) |
| |
| def _RemoteDevserverCall(self, cmd, stdout=False): |
| """Runs a command on a remote devserver by sshing into it. |
| |
| Raises cros_build_lib.RunCommandError() if the command could not be run |
| successfully. |
| |
| Args: |
| cmd: (list) the command to be run. |
| stdout: True if the stdout of the command should be captured. |
| """ |
| ip = urllib.parse.urlparse(self._staging_server).hostname |
| try: |
| success = True |
| return cros_build_lib.run(['ssh', ip] + cmd, log_output=True, |
| stdout=stdout) |
| except cros_build_lib.RunCommandError: |
| success = False |
| logging.error('Remote devserver call failed.') |
| raise |
| finally: |
| self._UpdateFallbackMetrics(ssh_used=True, success=success, hostname=ip) |
| |
| def _CheckPayloads(self, payload_name): |
| """Runs the curl command that checks if payloads have been staged.""" |
| payload_url = self._GetStagedUrl(staged_filename=payload_name, |
| build_id=self._payload_dir) |
| cmd = ['curl', '-I', payload_url, '--fail'] |
| try: |
| self._RemoteDevserverCall(cmd) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Could not verify if %s was staged at %s. Received ' |
| 'exception: %s', payload_name, payload_url, e) |
| # TODO(crbug.com/1059008): The following code block is a fallback. It |
| # should be removed once _RemoteDevserverCall has been proven to be |
| # reliable. |
| try: |
| success = True |
| # TODO(crbug.com/1033187): Remove log_output parameter passed to |
| # retry_util.RunCurl after the bug is fixed. The log_output=True option |
| # has been added to correct what seems to be a timing issue in |
| # retry_util.RunCurl. The error ((23) Failed writing body) is usually |
| # observed when a piped program closes the read pipe before the curl |
| # command has finished writing. log_output forces the read pipe to stay |
| # open, thus avoiding the failure. |
| retry_util.RunCurl(curl_args=cmd[1:], log_output=True) |
| except retry_util.DownloadError as e: |
| success = False |
| raise ChromiumOSTransferError('Payload %s does not exist at %s: %s' % |
| (payload_name, payload_url, e)) |
| finally: |
| self._UpdateFallbackMetrics(ssh_used=False, success=success) |
| |
| def CheckPayloads(self): |
| """Verify that all required payloads are staged on staging server.""" |
| logging.debug('Checking if payloads have been staged on server %s...', |
| self._staging_server) |
| |
| if self._transfer_rootfs_update: |
| self._CheckPayloads(self._payload_name) |
| self._CheckPayloads(GetPayloadPropertiesFileName(self._payload_name)) |
| |
| if self._transfer_stateful_update: |
| self._CheckPayloads(STATEFUL_FILENAME) |
| |
| def _GetStagedUrl(self, staged_filename, build_id=None): |
| """Returns a valid url to check availability of staged files. |
| |
| Args: |
| staged_filename: Name of the staged file. |
| build_id: This is the path at which the needed file can be found. It |
| is usually of the format <board_name>-release/R79-12345.6.0. By default, |
| the path is set to be None. |
| |
| Returns: |
| A URL in the format: |
| http://<ip>:<port>/static/<board>-release/<version>/<staged_filename> |
| """ |
| # Formulate the download URL out of components. |
| url = urllib.parse.urljoin(self._staging_server, 'static/') |
| if build_id: |
| # Add slash at the end of image_name if necessary. |
| if not build_id.endswith('/'): |
| build_id = build_id + '/' |
| url = urllib.parse.urljoin(url, build_id) |
| return urllib.parse.urljoin(url, staged_filename) |
| |
| def _GetCurlCmdForPayloadDownload(self, payload_dir, payload_filename, |
| build_id=None): |
| """Returns a valid curl command to download payloads into device tmp dir. |
| |
| Args: |
| payload_dir: Path to the payload directory on the device. |
| payload_filename: Name of the file by which the downloaded payload should |
| be saved. This is assumed to be the same as the name of the payload. |
| build_id: This is the path at which the needed payload can be found. It |
| is usually of the format <board_name>-release/R79-12345.6.0. By default, |
| the path is set to None. |
| |
| Returns: |
| A fully formed curl command in the format: |
| ['curl', '-o', '<path where payload should be saved>', |
| '<payload download URL>'] |
| """ |
| return ['curl', '-o', os.path.join(payload_dir, payload_filename), |
| self._GetStagedUrl(payload_filename, build_id)] |
| |
| def _TransferUpdateUtilsPackage(self): |
| """Transfer update-utils package to work directory of the remote device. |
| |
| The update-utils package will be transferred to the device from the |
| staging server via curl. |
| """ |
| logging.notice('Copying update script to device...') |
| source_dir = os.path.join(self._device.work_dir, 'src') |
| self._EnsureDeviceDirectory(source_dir) |
| |
| self._device.run(self._GetCurlCmdForPayloadDownload( |
| payload_dir=source_dir, |
| payload_filename=nebraska_wrapper.NEBRASKA_FILENAME)) |
| |
| # Make sure the device.work_dir exists after any installation and reboot. |
| self._EnsureDeviceDirectory(self._device.work_dir) |
| |
| def _TransferStatefulUpdate(self): |
| """Transfer files for stateful update. |
| |
| The stateful update bin and the corresponding payloads are copied to the |
| target remote device for stateful update from the staging server via curl. |
| """ |
| self._EnsureDeviceDirectory(self._device_payload_dir) |
| |
| # TODO(crbug.com/1024639): Another way to make the payloads available is |
| # to make update_engine download it directly from the staging_server. This |
| # will avoid a disk copy but has the potential to be harder to debug if |
| # update engine does not report the error clearly. |
| |
| logging.notice('Copying target stateful payload to device...') |
| self._device.run(self._GetCurlCmdForPayloadDownload( |
| payload_dir=self._device.work_dir, build_id=self._payload_dir, |
| payload_filename=STATEFUL_FILENAME)) |
| |
| def _TransferRootfsUpdate(self): |
| """Transfer files for rootfs update. |
| |
| Copy the update payload to the remote device for rootfs update from the |
| staging server via curl. |
| """ |
| self._EnsureDeviceDirectory(self._device_payload_dir) |
| |
| logging.notice('Copying rootfs payload to device...') |
| |
| # TODO(crbug.com/1024639): Another way to make the payloads available is |
| # to make update_engine download it directly from the staging_server. This |
| # will avoid a disk copy but has the potential to be harder to debug if |
| # update engine does not report the error clearly. |
| |
| self._device.run(self._GetCurlCmdForPayloadDownload( |
| payload_dir=self._device_payload_dir, build_id=self._payload_dir, |
| payload_filename=self._payload_name)) |
| |
| self._device.CopyToWorkDir(src=self._local_payload_props_path, |
| dest=self.PAYLOAD_DIR_NAME, |
| mode=self._payload_mode, |
| log_output=True, **self._cmd_kwargs) |
| |
| def GetPayloadPropsFile(self): |
| """Downloads the PayloadProperties file onto the drone. |
| |
| The payload properties file may be required to be updated in |
| auto_updater.ResolveAppIsMismatchIfAny(). Download the file from where it |
| has been staged on the staging server into the tempdir of the drone, so that |
| the file is available locally for any updates. |
| """ |
| if self._local_payload_props_path is None: |
| payload_props_filename = GetPayloadPropertiesFileName(self._payload_name) |
| payload_props_path = os.path.join(self._tempdir, payload_props_filename) |
| fallback = True |
| |
| # Get command to retrieve contents of the properties file. |
| cmd = ['curl', |
| self._GetStagedUrl(payload_props_filename, self._payload_dir)] |
| try: |
| result = self._RemoteDevserverCall(cmd, stdout=True) |
| json.loads(result.output) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Unable to get payload properties file by running %s due ' |
| 'to exception: %s.', ' '.join(cmd), e) |
| except ValueError: |
| logging.error('Could not create %s as %s not valid json.', |
| payload_props_path, result.output) |
| else: |
| fallback = False |
| osutils.WriteFile(payload_props_path, result.output, 'wb', |
| makedirs=True) |
| |
| if fallback: |
| # TODO(crbug.com/1059008): Fallback in case the try block above fails. |
| # Should be removed once reliable. |
| |
| # Get command to download the properties file. |
| cmd = self._GetCurlCmdForPayloadDownload( |
| payload_dir=self._tempdir, build_id=self._payload_dir, |
| payload_filename=payload_props_filename) |
| |
| # _GetCurlCmdForPayloadDownload removes the 'payloads/' prefix from the |
| # beginning of the payload_props_filename before creating the |
| # command. So here we need to remove that too. |
| prefix = 'payloads/' |
| if payload_props_filename.startswith(prefix): |
| payload_props_path = os.path.join( |
| self._tempdir, |
| payload_props_filename[len(prefix):]) |
| |
| try: |
| success = True |
| retry_util.RunCurl(cmd[1:]) |
| except retry_util.DownloadError as e: |
| success = False |
| raise ChromiumOSTransferError('Unable to download %s: %s' % |
| (payload_props_filename, e)) |
| finally: |
| self._UpdateFallbackMetrics(ssh_used=False, success=success) |
| |
| self._local_payload_props_path = payload_props_path |
| return self._local_payload_props_path |
| |
| def _GetPayloadSize(self): |
| """Returns the size of the payload by running a curl -I command. |
| |
| Returns: |
| Payload size in bytes. |
| """ |
| payload_url = self._GetStagedUrl(staged_filename=self._payload_name, |
| build_id=self._payload_dir) |
| cmd = ['curl', '-I', payload_url, '--fail'] |
| try: |
| proc = self._RemoteDevserverCall(cmd, stdout=True) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Unable to get payload size by running command %s due ' |
| 'to exception: %s.', ' '.join(cmd), e) |
| # TODO(crbug.com/1059008): Fallback in case the try block above fails. |
| # Should be removed once reliable. |
| try: |
| success = True |
| proc = retry_util.RunCurl(cmd[1:], stdout=True) |
| except cros_build_lib.RunCommandError as e: |
| success = False |
| raise ChromiumOSTransferError('Unable to get payload size: %s' % e) |
| finally: |
| self._UpdateFallbackMetrics(ssh_used=False, success=success) |
| |
| pattern = re.compile(r'Content-Length: [0-9]+', re.I) |
| match = pattern.findall(proc.output) |
| if not match: |
| raise ChromiumOSTransferError('Could not get payload size from output: ' |
| '%s ' % proc.output) |
| return int(match[0].split()[1].strip()) |
| |
| def GetPayloadProps(self): |
| """Gets image_version from the payload_dir name and gets payload size. |
| |
| The payload_dir must be in the format <board>/Rxx-12345.0.0 for a complete |
| match; else a ValueError will be raised. |
| |
| Returns: |
| Dict - See parent class's function for full details. |
| """ |
| values = {'size': self._GetPayloadSize()} |
| payload_format = self._GetPayloadFormat() |
| payload_pattern = self._GetPayloadPattern() |
| m = re.match(payload_pattern, payload_format) |
| if not m: |
| raise ValueError('Regular expression %r did not match the expected ' |
| 'payload format %s' % (payload_pattern, payload_format)) |
| values.update(m.groupdict()) |
| return values |
| |
| |
| class LabEndToEndPayloadTransfer(LabTransfer): |
| """Abstracts logic that transfers files from staging server to the DUT. |
| |
| TODO(crbug.com/1061570): AutoUpdate_endToEnd tests stage their payloads in a |
| different location on the devserver in comparison to the provision_AutoUpdate |
| test. Since we are removing the use of the cros_au RPC (see crbug.com/1049708 |
| and go/devserver-deprecation) from the EndToEnd tests, it is necessary to |
| extend LabTransfer class to support this new payload staging location. |
| Ideally, the URL at which the payload is staged should be abstracted from the |
| actual transfer of payloads. |
| """ |
| |
| def _GetPayloadFormat(self): |
| """Gets the payload format that should be evaluated. |
| |
| Returns: |
| The payload name as a string. |
| """ |
| return self._payload_name |
| |
| def _GetPayloadPattern(self): |
| """The regex pattern that the payload format must match. |
| |
| Returns: |
| Regular expression. |
| """ |
| return _PAYLOAD_PATTERN |
| |
| def _GetCurlCmdForPayloadDownload(self, payload_dir, payload_filename, |
| build_id=None): |
| """Returns a valid curl command to download payloads into device tmp dir. |
| |
| Args: |
| payload_dir: Path to the payload directory on the device. |
| payload_filename: Name of the file by which the downloaded payload should |
| be saved. This is assumed to be the same as the name of the payload. |
| If the payload_name must is in this format: |
| payloads/whatever_file_name, the 'payloads/' at the start will be |
| removed while saving the file as the files need to be saved in specific |
| directories for their subsequent installation. Keeping the 'payloads/' |
| at the beginning of the payload_filename, adds a new directory that |
| messes up its installation. |
| build_id: This is the path at which the needed payload can be found. It |
| is usually of the format <board_name>-release/R79-12345.6.0. By default, |
| the path is set to None. |
| |
| Returns: |
| A fully formed curl command in the format: |
| ['curl', '-o', '<path where payload should be saved>', |
| '<payload download URL>'] |
| """ |
| saved_filename = payload_filename |
| if saved_filename.startswith('payloads/'): |
| saved_filename = '/'.join(saved_filename.split('/')[1:]) |
| cmd = ['curl', '-o', os.path.join(payload_dir, saved_filename), |
| self._GetStagedUrl(payload_filename, build_id)] |
| return cmd |