Home | History | Annotate | Download | only in afe
      1 """\
      2 Logic for control file generation.
      3 """
      4 
      5 __author__ = 'showard (at] google.com (Steve Howard)'
      6 
      7 import re, os
      8 
      9 import common
     10 from autotest_lib.frontend.afe import model_logic
     11 import frontend.settings
     12 
     13 AUTOTEST_DIR = os.path.abspath(os.path.join(
     14     os.path.dirname(frontend.settings.__file__), '..'))
     15 
     16 EMPTY_TEMPLATE = 'def step_init():\n'
     17 
     18 CLIENT_KERNEL_TEMPLATE = """\
     19 kernel_list = %(client_kernel_list)s
     20 
     21 def step_init():
     22     for kernel_info in kernel_list:
     23         job.next_step(boot_kernel, kernel_info)
     24         job.next_step(step_test, kernel_info['version'])
     25     if len(kernel_list) > 1:
     26         job.use_sequence_number = True  # include run numbers in directory names
     27 
     28 
     29 def boot_kernel(kernel_info):
     30     # remove kernels (and associated data) not referenced by the bootloader
     31     for host in job.hosts:
     32         host.cleanup_kernels()
     33 
     34     testkernel = job.kernel(kernel_info['version'])
     35     if kernel_info['config_file']:
     36         testkernel.config(kernel_info['config_file'])
     37     testkernel.build()
     38     testkernel.install()
     39 
     40     cmdline = ' '.join((kernel_info.get('cmdline', ''), '%(kernel_args)s'))
     41     testkernel.boot(args=cmdline)
     42 
     43 
     44 def step_test(kernel_version):
     45     global kernel
     46     kernel = kernel_version  # Set the global in case anyone is using it.
     47     if len(kernel_list) > 1:
     48         # this is local to a machine, safe to assume there's only one host
     49         host, = job.hosts
     50         job.automatic_test_tag = host.get_kernel_ver()
     51 """
     52 
     53 SERVER_KERNEL_TEMPLATE = """\
     54 kernel_list = %%(server_kernel_list)s
     55 kernel_install_control = \"""
     56 %s    pass
     57 \"""
     58 
     59 from autotest_lib.client.common_lib import error
     60 
     61 at = autotest.Autotest()
     62 
     63 %%(upload_config_func)s
     64 def install_kernel(machine, kernel_info):
     65     host = hosts.create_host(machine)
     66     at.install(host=host)
     67     %%(call_upload_config)s
     68     at.run(kernel_install_control %%%%
     69            {'client_kernel_list': repr([kernel_info])}, host=host)
     70 
     71 
     72 num_machines_required = len(machines)
     73 if len(machines) > 4:
     74     # Allow a large multi-host tests to proceed despite a couple of hosts
     75     # failing to properly install the desired kernel (exclude those hosts).
     76     # TODO(gps): Figure out how to get and use SYNC_COUNT here.  It is defined
     77     # within some control files and will end up inside of stepN functions below.
     78     num_machines_required = len(machines) - 2
     79 
     80 
     81 def step_init():
     82     # a host object we use solely for the purpose of finding out the booted
     83     # kernel version, we use machines[0] since we already check that the same
     84     # kernel has been booted on all machines
     85     if len(kernel_list) > 1:
     86         kernel_host = hosts.create_host(machines[0])
     87 
     88     for kernel_info in kernel_list:
     89         func = lambda machine: install_kernel(machine, kernel_info)
     90         good_machines = job.parallel_on_machines(func, machines)
     91         if len(good_machines) < num_machines_required:
     92             raise error.TestError(
     93                     "kernel installed on only %%%%d of %%%%d machines."
     94                     %%%% (len(good_machines), num_machines_required))
     95 
     96         # Replace the machines list that step_test() will use with the
     97         # ones that successfully installed the kernel.
     98         machines[:] = good_machines
     99 
    100         # have server_job.run_test() automatically add the kernel version as
    101         # a suffix to the test name otherwise we cannot run the same test on
    102         # different kernel versions
    103         if len(kernel_list) > 1:
    104             job.automatic_test_tag = kernel_host.get_kernel_ver()
    105         step_test()
    106 
    107 
    108 def step_test():
    109 """ % CLIENT_KERNEL_TEMPLATE
    110 
    111 CLIENT_STEP_TEMPLATE = "    job.next_step('step%d')\n"
    112 SERVER_STEP_TEMPLATE = '    step%d()\n'
    113 
    114 UPLOAD_CONFIG_FUNC = """
    115 def upload_kernel_config(host, kernel_info):
    116     \"""
    117     If the kernel_info['config_file'] is a URL it will be downloaded
    118     locally and then uploaded to the client and a copy of the original
    119     dictionary with the new path to the config file will be returned.
    120     If the config file is not a URL the function returns the original
    121     dictionary.
    122     \"""
    123     import os
    124     from autotest_lib.client.common_lib import autotemp, utils
    125 
    126     config_orig = kernel_info.get('config_file')
    127 
    128     # if the file is not an URL then we assume it's a local client path
    129     if not config_orig or not utils.is_url(config_orig):
    130         return kernel_info
    131 
    132     # download it locally (on the server) and send it to the client
    133     config_tmp = autotemp.tempfile('kernel_config_upload', dir=job.tmpdir)
    134     try:
    135         utils.urlretrieve(config_orig, config_tmp.name)
    136         config_new = os.path.join(host.get_autodir(), 'tmp',
    137                                   os.path.basename(config_orig))
    138         host.send_file(config_tmp.name, config_new)
    139     finally:
    140         config_tmp.clean()
    141 
    142     return dict(kernel_info, config_file=config_new)
    143 
    144 """
    145 
    146 CALL_UPLOAD_CONFIG = 'kernel_info = upload_kernel_config(host, kernel_info)'
    147 
    148 
    149 def kernel_config_file(kernel, platform):
    150     if (not kernel.endswith('.rpm') and platform and
    151         platform.kernel_config):
    152         return platform.kernel_config
    153     return None
    154 
    155 
    156 def read_control_file(test):
    157     control_file = open(os.path.join(AUTOTEST_DIR, test.path))
    158     control_contents = control_file.read()
    159     control_file.close()
    160     return control_contents
    161 
    162 
    163 def get_kernel_stanza(kernel_list, platform=None, kernel_args='',
    164                       is_server=False, upload_kernel_config=False):
    165 
    166     template_args = {'kernel_args' : kernel_args}
    167 
    168     # add 'config_file' keys to the kernel_info dictionaries
    169     new_kernel_list = []
    170     for kernel_info in kernel_list:
    171         if kernel_info.get('config_file'):
    172             # already got a config file from the user
    173             new_kernel_info = kernel_info
    174         else:
    175             config_file = kernel_config_file(kernel_info['version'], platform)
    176             new_kernel_info = dict(kernel_info, config_file=config_file)
    177 
    178         new_kernel_list.append(new_kernel_info)
    179 
    180     if is_server:
    181         template = SERVER_KERNEL_TEMPLATE
    182         # leave client_kernel_list as a placeholder
    183         template_args['client_kernel_list'] = '%(client_kernel_list)s'
    184         template_args['server_kernel_list'] = repr(new_kernel_list)
    185 
    186         if upload_kernel_config:
    187             template_args['call_upload_config'] = CALL_UPLOAD_CONFIG
    188             template_args['upload_config_func'] = UPLOAD_CONFIG_FUNC
    189         else:
    190             template_args['call_upload_config'] = ''
    191             template_args['upload_config_func'] = ''
    192     else:
    193         template = CLIENT_KERNEL_TEMPLATE
    194         template_args['client_kernel_list'] = repr(new_kernel_list)
    195 
    196     return template % template_args
    197 
    198 
    199 def add_boilerplate_to_nested_steps(lines):
    200     # Look for a line that begins with 'def step_init():' while
    201     # being flexible on spacing.  If it's found, this will be
    202     # a nested set of steps, so add magic to make it work.
    203     # See client/bin/job.py's step_engine for more info.
    204     if re.search(r'^(.*\n)*def\s+step_init\s*\(\s*\)\s*:', lines):
    205         lines += '\nreturn locals() '
    206         lines += '# Boilerplate magic for nested sets of steps'
    207     return lines
    208 
    209 
    210 def format_step(item, lines):
    211     lines = indent_text(lines, '    ')
    212     lines = 'def step%d():\n%s' % (item, lines)
    213     return lines
    214 
    215 
    216 def get_tests_stanza(tests, is_server, prepend=None, append=None,
    217                      client_control_file=''):
    218     """ Constructs the control file test step code from a list of tests.
    219 
    220     @param tests A sequence of test control files to run.
    221     @param is_server bool, Is this a server side test?
    222     @param prepend A list of steps to prepend to each client test.
    223         Defaults to [].
    224     @param append A list of steps to append to each client test.
    225         Defaults to [].
    226     @param client_control_file If specified, use this text as the body of a
    227         final client control file to run after tests.  is_server must be False.
    228 
    229     @returns The control file test code to be run.
    230     """
    231     assert not (client_control_file and is_server)
    232     if not prepend:
    233         prepend = []
    234     if not append:
    235         append = []
    236     raw_control_files = [read_control_file(test) for test in tests]
    237     return _get_tests_stanza(raw_control_files, is_server, prepend, append,
    238                              client_control_file=client_control_file)
    239 
    240 
    241 def _get_tests_stanza(raw_control_files, is_server, prepend, append,
    242                       client_control_file=''):
    243     """
    244     Implements the common parts of get_test_stanza.
    245 
    246     A site_control_file that wants to implement its own get_tests_stanza
    247     likely wants to call this in the end.
    248 
    249     @param raw_control_files A list of raw control file data to be combined
    250         into a single control file.
    251     @param is_server bool, Is this a server side test?
    252     @param prepend A list of steps to prepend to each client test.
    253     @param append A list of steps to append to each client test.
    254     @param client_control_file If specified, use this text as the body of a
    255         final client control file to append to raw_control_files after fixups.
    256 
    257     @returns The combined mega control file.
    258     """
    259     if client_control_file:
    260         # 'return locals()' is always appended incase the user forgot, it
    261         # is necessary to allow for nested step engine execution to work.
    262         raw_control_files.append(client_control_file + '\nreturn locals()')
    263     raw_steps = prepend + [add_boilerplate_to_nested_steps(step)
    264                            for step in raw_control_files] + append
    265     steps = [format_step(index, step)
    266              for index, step in enumerate(raw_steps)]
    267     if is_server:
    268         step_template = SERVER_STEP_TEMPLATE
    269         footer = '\n\nstep_init()\n'
    270     else:
    271         step_template = CLIENT_STEP_TEMPLATE
    272         footer = ''
    273 
    274     header = ''.join(step_template % i for i in xrange(len(steps)))
    275     return header + '\n' + '\n\n'.join(steps) + footer
    276 
    277 
    278 def indent_text(text, indent):
    279     """Indent given lines of python code avoiding indenting multiline
    280     quoted content (only for triple " and ' quoting for now)."""
    281     regex = re.compile('(\\\\*)("""|\'\'\')')
    282 
    283     res = []
    284     in_quote = None
    285     for line in text.splitlines():
    286         # if not within a multinline quote indent the line contents
    287         if in_quote:
    288             res.append(line)
    289         else:
    290             res.append(indent + line)
    291 
    292         while line:
    293             match = regex.search(line)
    294             if match:
    295                 # for an even number of backslashes before the triple quote
    296                 if len(match.group(1)) % 2 == 0:
    297                     if not in_quote:
    298                         in_quote = match.group(2)[0]
    299                     elif in_quote == match.group(2)[0]:
    300                         # if we found a matching end triple quote
    301                         in_quote = None
    302                 line = line[match.end():]
    303             else:
    304                 break
    305 
    306     return '\n'.join(res)
    307 
    308 
    309 def _get_profiler_commands(profilers, is_server, profile_only):
    310     prepend, append = [], []
    311     if profile_only is not None:
    312         prepend.append("job.default_profile_only = %r" % profile_only)
    313     for profiler in profilers:
    314         prepend.append("job.profilers.add('%s')" % profiler.name)
    315         append.append("job.profilers.delete('%s')" % profiler.name)
    316     return prepend, append
    317 
    318 
    319 def _sanity_check_generate_control(is_server, client_control_file, kernels,
    320                                    upload_kernel_config):
    321     """
    322     Sanity check some of the parameters to generate_control().
    323 
    324     This exists as its own function so that site_control_file may call it as
    325     well from its own generate_control().
    326 
    327     @raises ValidationError if any of the parameters do not make sense.
    328     """
    329     if is_server and client_control_file:
    330         raise model_logic.ValidationError(
    331                 {'tests' : 'You cannot run server tests at the same time '
    332                  'as directly supplying a client-side control file.'})
    333 
    334     if kernels:
    335         # make sure that kernel is a list of dictionarions with at least
    336         # the 'version' key in them
    337         kernel_error = model_logic.ValidationError(
    338                 {'kernel': 'The kernel parameter must be a sequence of '
    339                  'dictionaries containing at least the "version" key '
    340                  '(got: %r)' % kernels})
    341         try:
    342             iter(kernels)
    343         except TypeError:
    344             raise kernel_error
    345         for kernel_info in kernels:
    346             if (not isinstance(kernel_info, dict) or
    347                     'version' not in kernel_info):
    348                 raise kernel_error
    349 
    350         if upload_kernel_config and not is_server:
    351             raise model_logic.ValidationError(
    352                     {'upload_kernel_config': 'Cannot use upload_kernel_config '
    353                                              'with client side tests'})
    354 
    355 
    356 def generate_control(tests, kernels=None, platform=None, is_server=False,
    357                      profilers=(), client_control_file='', profile_only=None,
    358                      upload_kernel_config=False):
    359     """
    360     Generate a control file for a sequence of tests.
    361 
    362     @param tests A sequence of test control files to run.
    363     @param kernels A sequence of kernel info dictionaries configuring which
    364             kernels to boot for this job and other options for them
    365     @param platform A platform object with a kernel_config attribute.
    366     @param is_server bool, Is this a server control file rather than a client?
    367     @param profilers A list of profiler objects to enable during the tests.
    368     @param client_control_file Contents of a client control file to run as the
    369             last test after everything in tests.  Requires is_server=False.
    370     @param profile_only bool, should this control file run all tests in
    371             profile_only mode by default
    372     @param upload_kernel_config: if enabled it will generate server control
    373             file code that uploads the kernel config file to the client and
    374             tells the client of the new (local) path when compiling the kernel;
    375             the tests must be server side tests
    376 
    377     @returns The control file text as a string.
    378     """
    379     _sanity_check_generate_control(is_server=is_server, kernels=kernels,
    380                                    client_control_file=client_control_file,
    381                                    upload_kernel_config=upload_kernel_config)
    382 
    383     control_file_text = ''
    384     if kernels:
    385         control_file_text = get_kernel_stanza(
    386                 kernels, platform, is_server=is_server,
    387                 upload_kernel_config=upload_kernel_config)
    388     else:
    389         control_file_text = EMPTY_TEMPLATE
    390 
    391     prepend, append = _get_profiler_commands(profilers, is_server, profile_only)
    392 
    393     control_file_text += get_tests_stanza(tests, is_server, prepend, append,
    394                                           client_control_file)
    395     return control_file_text
    396