Home | History | Annotate | Download | only in site_utils
      1 #!/usr/bin/python
      2 # Copyright 2015 The Chromium OS Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import httplib
      7 import logging
      8 import os
      9 import sys
     10 import urllib2
     11 
     12 import common
     13 try:
     14     # Ensure the chromite site-package is installed.
     15     from chromite.lib import *
     16 except ImportError:
     17     import subprocess
     18     build_externals_path = os.path.join(
     19             os.path.dirname(os.path.dirname(os.path.realpath(__file__))),
     20             'utils', 'build_externals.py')
     21     subprocess.check_call([build_externals_path, 'chromiterepo'])
     22     # Restart the script so python now finds the autotest site-packages.
     23     sys.exit(os.execv(__file__, sys.argv))
     24 from autotest_lib.server.hosts import moblab_host
     25 from autotest_lib.site_utils import brillo_common
     26 
     27 
     28 _DEFAULT_STAGE_PATH_TEMPLATE = 'aue2e/%(use)s'
     29 _DEVSERVER_STAGE_URL_TEMPLATE = ('http://%(moblab)s:%(port)s/stage?'
     30                                  'local_path=%(stage_dir)s&'
     31                                  'files=%(stage_files)s')
     32 _DEVSERVER_PAYLOAD_URI_TEMPLATE = ('http://%(moblab)s:%(port)s/static/'
     33                                    '%(stage_path)s')
     34 _STAGED_PAYLOAD_FILENAME = 'update.gz'
     35 _SPEC_GEN_LABEL = 'gen'
     36 _TEST_JOB_NAME = 'brillo_update_test'
     37 _TEST_NAME = 'autoupdate_EndToEndTest'
     38 _DEFAULT_DEVSERVER_PORT = '8080'
     39 
     40 # Snippet of code that runs on the Moblab and returns the type of a payload
     41 # file. Result is either 'delta' or 'full', acordingly.
     42 _GET_PAYLOAD_TYPE = """
     43 import update_payload
     44 p = update_payload(open('%(payload_file)s'))
     45 p.Init()
     46 print 'delta' if p.IsDelta() else 'full'
     47 """
     48 
     49 
     50 class PayloadStagingError(brillo_common.BrilloTestError):
     51     """A failure that occurred while staging an update payload."""
     52 
     53 
     54 class PayloadGenerationError(brillo_common.BrilloTestError):
     55     """A failure that occurred while generating an update payload."""
     56 
     57 
     58 def setup_parser(parser):
     59     """Add parser options.
     60 
     61     @param parser: argparse.ArgumentParser of the script.
     62     """
     63     parser.add_argument('-t', '--target_payload', metavar='SPEC', required=True,
     64                         help='Stage a target payload. This can either be a '
     65                              'path to a local payload file, or take the form '
     66                              '"%s:DST_IMAGE[:SRC_IMAGE]", in which case a '
     67                              'new payload will get generated from SRC_IMAGE '
     68                              '(if given) and DST_IMAGE and staged on the '
     69                              'server. This is a mandatory input.' %
     70                              _SPEC_GEN_LABEL)
     71     parser.add_argument('-s', '--source_payload', metavar='SPEC',
     72                         help='Stage a source payload. This is an optional '
     73                              'input. See --target_payload for possible values '
     74                              'for SPEC.')
     75 
     76     brillo_common.setup_test_action_parser(parser)
     77 
     78 
     79 def get_stage_rel_path(stage_file):
     80     """Returns the relative stage path for remote file.
     81 
     82     The relative stage path consists of the last three path components: the
     83     file name and the two directory levels that contain it.
     84 
     85     @param stage_file: Path to the file that is being staged.
     86 
     87     @return A stage relative path.
     88     """
     89     components = []
     90     for i in range(3):
     91         stage_file, component = os.path.split(stage_file)
     92         components.insert(0, component)
     93     return os.path.join(*components)
     94 
     95 
     96 def stage_remote_payload(moblab, devserver_port, tmp_stage_file):
     97     """Stages a remote payload on the Moblab's devserver.
     98 
     99     @param moblab: MoblabHost representing the MobLab being used for testing.
    100     @param devserver_port: Externally accessible port to the Moblab devserver.
    101     @param tmp_stage_file: Path to the remote payload file to stage.
    102 
    103     @return URI to use for downloading the staged payload.
    104 
    105     @raise PayloadStagingError: If we failed to stage the payload.
    106     """
    107     # Remove the artifact if previously staged.
    108     stage_rel_path = get_stage_rel_path(tmp_stage_file)
    109     target_stage_file = os.path.join(moblab_host.MOBLAB_IMAGE_STORAGE,
    110                                      stage_rel_path)
    111     moblab.run('rm -f %s && chown moblab:moblab %s' %
    112                (target_stage_file, tmp_stage_file))
    113     tmp_stage_dir, stage_file = os.path.split(tmp_stage_file)
    114     devserver_host = moblab.web_address.split(':')[0]
    115     try:
    116         stage_url = _DEVSERVER_STAGE_URL_TEMPLATE % {
    117                 'moblab': devserver_host,
    118                 'port': devserver_port,
    119                 'stage_dir': tmp_stage_dir,
    120                 'stage_files': stage_file}
    121         res = urllib2.urlopen(stage_url).read()
    122     except (urllib2.HTTPError, httplib.HTTPException, urllib2.URLError) as e:
    123         raise PayloadStagingError('Unable to stage payload on moblab: %s' % e)
    124     else:
    125         if res != 'Success':
    126             raise PayloadStagingError('Staging failed: %s' % res)
    127 
    128     logging.debug('Payload is staged on Moblab as %s', stage_rel_path)
    129     return _DEVSERVER_PAYLOAD_URI_TEMPLATE % {
    130             'moblab': devserver_host,
    131             'port': _DEFAULT_DEVSERVER_PORT,
    132             'stage_path': os.path.dirname(stage_rel_path)}
    133 
    134 
    135 def stage_local_payload(moblab, devserver_port, tmp_stage_dir, payload):
    136     """Stages a local payload on the MobLab's devserver.
    137 
    138     @param moblab: MoblabHost representing the MobLab being used for testing.
    139     @param devserver_port: Externally accessible port to the Moblab devserver.
    140     @param tmp_stage_dir: Path of temporary staging directory on the Moblab.
    141     @param payload: Path to the local payload file to stage.
    142 
    143     @return Tuple consisting a payload download URI and the payload type
    144             ('delta' or 'full').
    145 
    146     @raise PayloadStagingError: If we failed to stage the payload.
    147     """
    148     if not os.path.isfile(payload):
    149         raise PayloadStagingError('Payload file %s does not exist.' % payload)
    150 
    151     # Copy the payload file over to the temporary stage directory.
    152     tmp_stage_file = os.path.join(tmp_stage_dir, _STAGED_PAYLOAD_FILENAME)
    153     moblab.send_file(payload, tmp_stage_file)
    154 
    155     # Find the payload type.
    156     get_payload_type = _GET_PAYLOAD_TYPE % {'payload_file': tmp_stage_file}
    157     payload_type = moblab.run('python', stdin=get_payload_type).stdout.strip()
    158 
    159     # Stage the copied payload.
    160     payload_uri = stage_remote_payload(moblab, devserver_port, tmp_stage_file)
    161 
    162     return payload_uri, payload_type
    163 
    164 
    165 def generate_payload(moblab, devserver_port, tmp_stage_dir, payload_spec):
    166     """Generates and stages a payload from local image(s).
    167 
    168     @param moblab: MoblabHost representing the MobLab being used for testing.
    169     @param devserver_port: Externally accessible port to the Moblab devserver.
    170     @param tmp_stage_dir: Path of temporary staging directory on the Moblab.
    171     @param payload_spec: A string of the form "DST_IMAGE[:SRC_IMAGE]", where
    172                          DST_IMAGE is a target image and SRC_IMAGE an optional
    173                          source image.
    174 
    175     @return Tuple consisting a payload download URI and the payload type
    176             ('delta' or 'full').
    177 
    178     @raise PayloadGenerationError: If we failed to generate the payload.
    179     @raise PayloadStagingError: If we failed to stage the payload.
    180     """
    181     parts = payload_spec.split(':', 1)
    182     dst_image = parts[0]
    183     src_image = parts[1] if len(parts) == 2 else None
    184 
    185     if not os.path.isfile(dst_image):
    186         raise PayloadGenerationError('Target image file %s does not exist.' %
    187                                      dst_image)
    188     if src_image and not os.path.isfile(src_image):
    189         raise PayloadGenerationError('Source image file %s does not exist.' %
    190                                      src_image)
    191 
    192     tmp_images_dir = moblab.make_tmp_dir()
    193     try:
    194         # Copy the images to a temporary location.
    195         remote_dst_image = os.path.join(tmp_images_dir,
    196                                         os.path.basename(dst_image))
    197         moblab.send_file(dst_image, remote_dst_image)
    198         remote_src_image = None
    199         if src_image:
    200             remote_src_image = os.path.join(tmp_images_dir,
    201                                             os.path.basename(src_image))
    202             moblab.send_file(src_image, remote_src_image)
    203 
    204         # Generate the payload into a temporary staging directory.
    205         tmp_stage_file = os.path.join(tmp_stage_dir, _STAGED_PAYLOAD_FILENAME)
    206         gen_cmd = ['brillo_update_payload', 'generate',
    207                    '--payload', tmp_stage_file,
    208                    '--target_image', remote_dst_image]
    209         if remote_src_image:
    210             payload_type = 'delta'
    211             gen_cmd += ['--source_image', remote_src_image]
    212         else:
    213             payload_type = 'full'
    214 
    215         moblab.run(' '.join(gen_cmd), stdout_tee=None, stderr_tee=None)
    216     finally:
    217         moblab.run('rm -rf %s' % tmp_images_dir)
    218 
    219     # Stage the generated payload.
    220     payload_uri = stage_remote_payload(moblab, devserver_port, tmp_stage_file)
    221 
    222     return payload_uri, payload_type
    223 
    224 
    225 def stage_payload(moblab, devserver_port, tmp_dir, use, payload_spec):
    226     """Stages the payload based on a given specification.
    227 
    228     @param moblab: MoblabHost representing the MobLab being used for testing.
    229     @param devserver_port: Externally accessible port to the Moblab devserver.
    230     @param tmp_dir: Path of temporary static subdirectory.
    231     @param use: String defining the use for the payload, either 'source' or
    232                 'target'.
    233     @param payload_spec: Either a string of the form
    234                          "PAYLOAD:DST_IMAGE[:SRC_IMAGE]" describing how to
    235                          generate a new payload from a target and (optionally)
    236                          source image; or path to a local payload file.
    237 
    238     @return Tuple consisting a payload download URI and the payload type
    239             ('delta' or 'full').
    240 
    241     @raise PayloadGenerationError: If we failed to generate the payload.
    242     @raise PayloadStagingError: If we failed to stage the payload.
    243     """
    244     tmp_stage_dir = os.path.join(
    245             tmp_dir, _DEFAULT_STAGE_PATH_TEMPLATE % {'use': use})
    246     moblab.run('mkdir -p %s && chown -R moblab:moblab %s' %
    247                (tmp_stage_dir, tmp_stage_dir))
    248 
    249     spec_gen_prefix = _SPEC_GEN_LABEL + ':'
    250     if payload_spec.startswith(spec_gen_prefix):
    251         return generate_payload(moblab, devserver_port, tmp_stage_dir,
    252                                 payload_spec[len(spec_gen_prefix):])
    253     else:
    254         return stage_local_payload(moblab, devserver_port, tmp_stage_dir,
    255                                    payload_spec)
    256 
    257 
    258 def main(args):
    259     """The main function."""
    260     args = brillo_common.parse_args(
    261             'Set up Moblab for running Brillo AU end-to-end test, then launch '
    262             'the test (unless otherwise requested).',
    263             setup_parser=setup_parser)
    264 
    265     moblab, devserver_port = brillo_common.get_moblab_and_devserver_port(
    266             args.moblab_host)
    267     tmp_dir = moblab.make_tmp_dir(base=moblab_host.MOBLAB_IMAGE_STORAGE)
    268     moblab.run('chown -R moblab:moblab %s' % tmp_dir)
    269     test_args = {'name': _TEST_JOB_NAME}
    270     try:
    271         if args.source_payload:
    272             payload_uri, _ = stage_payload(moblab, devserver_port, tmp_dir,
    273                                            'source', args.source_payload)
    274             test_args['source_payload_uri'] = payload_uri
    275             logging.info('Source payload was staged')
    276 
    277         payload_uri, payload_type = stage_payload(
    278                 moblab, devserver_port, tmp_dir, 'target', args.target_payload)
    279         test_args['target_payload_uri'] = payload_uri
    280         test_args['update_type'] = payload_type
    281         logging.info('Target payload was staged')
    282     finally:
    283         moblab.run('rm -rf %s' % tmp_dir)
    284 
    285     brillo_common.do_test_action(args, moblab, _TEST_NAME, test_args)
    286 
    287 
    288 if __name__ == '__main__':
    289     try:
    290         main(sys.argv)
    291         sys.exit(0)
    292     except brillo_common.BrilloTestError as e:
    293         logging.error('Error: %s', e)
    294 
    295     sys.exit(1)
    296