| # Copyright 2015 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. |
| |
| """Stage a custom image on a Moblab device or in Google Storage.""" |
| |
| from __future__ import print_function |
| |
| import httplib |
| import os |
| import re |
| import shutil |
| import urllib2 |
| |
| from chromite.cbuildbot import commands, constants |
| from chromite.cli import command |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import dev_server_wrapper |
| from chromite.lib import gs |
| from chromite.lib import remote_access |
| from chromite.lib import osutils |
| |
| |
| DEVSERVER_STATIC_DIR = cros_build_lib.FromChrootPath( |
| os.path.join(constants.CHROOT_SOURCE_ROOT, 'devserver', 'static')) |
| MOBLAB_STATIC_DIR = '/mnt/moblab/static' |
| MOBLAB_TMP_DIR = os.path.join(MOBLAB_STATIC_DIR, 'tmp') |
| BOARD_BUILD_DIR = 'usr/local/build' |
| DEVSERVER_STAGE_URL = ('http://%(moblab)s:8080/stage?local_path=%(staged_dir)s' |
| '&artifacts=full_payload,stateful,test_suites,' |
| 'control_files,autotest_packages,' |
| 'autotest_server_package') |
| CUSTOM_BUILD_NAME = '%(board)s-custom/%(build)s' |
| |
| |
| class CustomImageStagingException(Exception): |
| """Thrown when there is an error staging an custom image.""" |
| |
| |
| def GSURLRegexHelper(gsurl): |
| """Helper to do regex matching on a Google Storage URL |
| |
| Args: |
| gsurl: Google Storage URL to match. |
| |
| Returns: |
| Regex Match Object with groups(board, type, & build_name) or None if there |
| was no match. |
| """ |
| return re.match(r'gs://.*/(trybot-)?(?P<board>[\w-]+)-(?P<type>\w+)/' |
| r'(?P<build_name>R\d+-[\d.ab-]+)', gsurl) |
| |
| |
| @command.CommandDecorator('stage') |
| class StageCommand(command.CliCommand): |
| """Remotely stages an image onto a MobLab device or into Google Storage. |
| |
| The image to be staged may be a local custom image built in the chroot or an |
| official image in Google Storage. The test binaries will always come from the |
| local build root regardless of the image source. |
| |
| This script generates/copies the update payloads and test binaries required. |
| It then stages them on the Moblab's devserver or copies them into the |
| specified Google Storage Bucket. |
| |
| The image name to then use for testing is outputted at the end of this |
| script. |
| """ |
| |
| EPILOG = """ |
| To stage a local image path onto a moblab device: |
| cros stage /path/to/board/build/chromiumos-test-image.bin <moblab> |
| |
| To stage an official image with custom test binaries onto a moblab device: |
| cros stage <gs_image_dir> <moblab> |
| |
| To stage a local image path into a Google Storage Bucket: |
| cros stage /path/to/board/build/chromiumos-test-image.bin <gs_base_path> |
| --boto_file=<boto_file_path> |
| |
| NOTES: |
| * The autotest bits used to test this image will be the latest in your |
| build sysroot! I.E. if you emerge new autotest changes after producing the |
| image you wish to stage, there is a chance that the changes will not match. |
| * The custom image will only stay on the Moblab device for 24 hours at which |
| point it will be wiped. |
| """ |
| |
| @classmethod |
| def AddParser(cls, parser): |
| """Add parser arguments.""" |
| super(StageCommand, cls).AddParser(parser) |
| parser.add_argument( |
| 'image', nargs='?', default='latest', help='Path to image we want to ' |
| 'stage. If a local path, it should be in the format of ' |
| '/.../.../board/build/<image>.bin . If a Google Storage path it should' |
| 'be in the format of ' |
| 'gs://<bucket-name>/<board>-<builder type>/<build name>') |
| parser.add_argument( |
| 'remote', help='MobLab device that has password-less SSH set up via ' |
| 'the chroot already. Or Google Storage Bucket in the form of ' |
| 'gs://<bucket-name>/') |
| parser.add_argument( |
| '--boto_file', dest='boto_file', default=None, |
| help='Path to boto file to use when uploading to Google Storage. If ' |
| 'none the default chroot boto file is used.') |
| |
| def __init__(self, options): |
| """Initializes cros stage.""" |
| super(StageCommand, self).__init__(options) |
| self.board = None |
| # Determine if we are staging a local custom image or an official image. |
| if self.options.image.startswith('gs://'): |
| self._remote_image = True |
| self.staged_image_name = self._GenerateImageNameFromGSUrl( |
| self.options.image) |
| else: |
| self._remote_image = False |
| self.staged_image_name = self._GenerateImageNameFromLocalPath( |
| self.options.image) |
| self.stage_directory = os.path.join(MOBLAB_TMP_DIR, self.staged_image_name) |
| |
| # Determine if the staging destination is a Moblab or Google Storage. |
| if self.options.remote.startswith('gs://'): |
| self._remote_is_moblab = False |
| else: |
| self._remote_is_moblab = True |
| |
| def _GenerateImageNameFromLocalPath(self, image): |
| """Generate the name as which |image| will be staged onto Moblab. |
| |
| Args: |
| image: Path to image we want to stage. It should be in the format of |
| /.../.../board/build/<image>.bin |
| |
| Returns: |
| Name the image will be staged as. |
| |
| Raises: |
| CustomImageStagingException: If the image name supplied is not valid. |
| """ |
| realpath = osutils.ExpandPath(image) |
| if not realpath.endswith('.bin'): |
| raise CustomImageStagingException( |
| 'Image path: %s does not end in .bin !' % realpath) |
| build_name = os.path.basename(os.path.dirname(realpath)) |
| # Custom builds are name with the suffix of '-a1' but the build itself |
| # is missing this suffix in its filesystem. Therefore lets rename the build |
| # name to match the name inside the build. |
| if build_name.endswith('-a1'): |
| build_name = build_name[:-len('-a1')] |
| |
| self.board = os.path.basename(os.path.dirname(os.path.dirname(realpath))) |
| return CUSTOM_BUILD_NAME % dict(board=self.board, build=build_name) |
| |
| def _GenerateImageNameFromGSUrl(self, image): |
| """Generate the name as which |image| will be staged onto Moblab. |
| |
| Args: |
| image: GS Url to the image we want to stage. It should be in the format |
| gs://<bucket-name>/<board>-<builder type>/<build name> |
| |
| Returns: |
| Name the image will be staged as. |
| |
| Raises: |
| CustomImageStagingException: If the image name supplied is not valid. |
| """ |
| match = GSURLRegexHelper(image) |
| if not match: |
| raise CustomImageStagingException( |
| 'Image URL: %s is improperly defined!' % image) |
| self.board = match.group('board') |
| return CUSTOM_BUILD_NAME % dict(board=self.board, |
| build=match.group('build_name')) |
| |
| def _DownloadPayloads(self, tempdir): |
| """Download from GS the update payloads we require. |
| |
| Args: |
| tempdir: Temporary Directory to store the downloaded payloads. |
| """ |
| gs_context = gs.GSContext(boto_file=self.options.boto_file) |
| gs_context.Copy(os.path.join(self.options.image, 'stateful.tgz'), tempdir) |
| gs_context.Copy(os.path.join(self.options.image, '*_full*'), tempdir) |
| |
| def _GeneratePayloads(self, tempdir): |
| """Generate the update payloads we require. |
| |
| Args: |
| tempdir: Temporary Directory to store the generated payloads. |
| """ |
| dev_server_wrapper.GetUpdatePayloadsFromLocalPath( |
| self.options.image, tempdir, static_dir=DEVSERVER_STATIC_DIR) |
| rootfs_payload = os.path.join(tempdir, dev_server_wrapper.ROOTFS_FILENAME) |
| # Devservers will look for a file named *_full_*. |
| shutil.move(rootfs_payload, os.path.join(tempdir, 'update_full_dev.bin')) |
| |
| def _GenerateTestBits(self, tempdir): |
| """Generate and transfer to the Moblab the test bits we require. |
| |
| Args: |
| tempdir: Temporary Directory to store the generated test artifacts. |
| """ |
| build_root = cros_build_lib.GetSysroot(board=self.board) |
| cwd = os.path.join(build_root, BOARD_BUILD_DIR) |
| tarball_funcs = [commands.BuildAutotestControlFilesTarball, |
| commands.BuildAutotestPackagesTarball, |
| commands.BuildAutotestTestSuitesTarball, |
| commands.BuildAutotestServerPackageTarball] |
| for tarball_func in tarball_funcs: |
| tarball_func(build_root, cwd, tempdir) |
| |
| def _StageOnMoblab(self, tempdir): |
| """Stage the generated payloads and test bits on a moblab device. |
| |
| Args: |
| tempdir: Temporary Directory that contains the generated payloads and |
| test bits. |
| """ |
| with remote_access.ChromiumOSDeviceHandler(self.options.remote) as device: |
| device.RunCommand(['mkdir', '-p', self.stage_directory]) |
| for f in os.listdir(tempdir): |
| device.CopyToDevice(os.path.join(tempdir, f), self.stage_directory) |
| device.RunCommand(['chown', '-R', 'moblab:moblab', |
| MOBLAB_TMP_DIR]) |
| # Delete this image from the Devserver in case it was previously staged. |
| device.RunCommand(['rm', '-rf', os.path.join(MOBLAB_STATIC_DIR, |
| self.staged_image_name)]) |
| try: |
| stage_url = DEVSERVER_STAGE_URL % dict(moblab=self.options.remote, |
| staged_dir=self.stage_directory) |
| res = urllib2.urlopen(stage_url).read() |
| except (urllib2.HTTPError, httplib.HTTPException, urllib2.URLError) as e: |
| logging.error('Unable to stage artifacts on moblab. Error: %s', e) |
| else: |
| if res == 'Success': |
| logging.info('\n\nStaging Completed!') |
| logging.info('Image is staged on Moblab as %s', |
| self.staged_image_name) |
| else: |
| logging.info('Staging failed. Error Message: %s', res) |
| finally: |
| device.RunCommand(['rm', '-rf', self.stage_directory]) |
| |
| def _StageOnGS(self, tempdir): |
| """Stage the generated payloads and test bits into a Google Storage bucket. |
| |
| Args: |
| tempdir: Temporary Directory that contains the generated payloads and |
| test bits. |
| """ |
| gs_context = gs.GSContext(boto_file=self.options.boto_file) |
| for f in os.listdir(tempdir): |
| gs_context.CopyInto(os.path.join(tempdir, f), os.path.join( |
| self.options.remote, self.staged_image_name)) |
| logging.info('\n\nStaging Completed!') |
| logging.info('Image is staged in Google Storage as %s', |
| self.staged_image_name) |
| |
| def Run(self): |
| """Perform the cros stage command.""" |
| logging.info('Attempting to stage: %s as Image: %s at Location: %s', |
| self.options.image, self.staged_image_name, |
| self.options.remote) |
| osutils.SafeMakedirsNonRoot(DEVSERVER_STATIC_DIR) |
| |
| with osutils.TempDir() as tempdir: |
| if self._remote_image: |
| self._DownloadPayloads(tempdir) |
| else: |
| self._GeneratePayloads(tempdir) |
| self._GenerateTestBits(tempdir) |
| if self._remote_is_moblab: |
| self._StageOnMoblab(tempdir) |
| else: |
| self._StageOnGS(tempdir) |