Home | History | Annotate | Download | only in lucifer
      1 # Copyright 2017 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Extra functions for frontend.afe.models.Job objects.
      6 
      7 Most of these exist in tightly coupled forms in legacy Autotest code
      8 (e.g., part of methods with completely unrelated names on Task objects
      9 under multiple layers of abstract classes).  These are defined here to
     10 sanely reuse without having to commit to a long refactor of legacy code
     11 that is getting deleted soon.
     12 
     13 It's not really a good idea to define these on the Job class either;
     14 they are specialized and the Job class already suffers from method
     15 bloat.
     16 """
     17 
     18 from __future__ import absolute_import
     19 from __future__ import division
     20 from __future__ import print_function
     21 
     22 import os
     23 import time
     24 import urllib
     25 
     26 from lucifer import autotest
     27 from lucifer import results
     28 
     29 
     30 def is_hostless(job):
     31     """Return True if the job is hostless.
     32 
     33     @param job: frontend.afe.models.Job instance
     34     """
     35     return not hostnames(job)
     36 
     37 
     38 def hostnames(job):
     39     """Return a list of hostnames for a job.
     40 
     41     @param job: frontend.afe.models.Job instance
     42     """
     43     hqes = job.hostqueueentry_set.all().prefetch_related('host')
     44     return [hqe.host.hostname for hqe in hqes if hqe.host is not None]
     45 
     46 
     47 def is_aborted(job):
     48     """Return if the job is aborted.
     49 
     50     (This means the job is marked for abortion; the job can still be
     51     running.)
     52 
     53     @param job: frontend.afe.models.Job instance
     54     """
     55     return job.hostqueueentry_set.filter(aborted=True).exists()
     56 
     57 
     58 def is_server_job(job):
     59     """Return whether the job is a server job.
     60 
     61     @param job: frontend.afe.models.Job instance
     62     """
     63     return not is_client_job(job)
     64 
     65 
     66 def is_client_job(job):
     67     """Return whether the job is a client job.
     68 
     69     If the job is not a client job, it is a server job.
     70 
     71     (In theory a job can be neither.  I have no idea what you should do
     72     in that case.)
     73 
     74     @param job: frontend.afe.models.Job instance
     75     """
     76     CONTROL_TYPE = autotest.load('client.common_lib.control_data').CONTROL_TYPE
     77     return CONTROL_TYPE.get_value(job.control_type) == CONTROL_TYPE.CLIENT
     78 
     79 
     80 def needs_ssp(job):
     81     """Return whether the job needs SSP.
     82 
     83     This also looks up the config for jobs that do not have a value
     84     specified.
     85 
     86     @param job: frontend.afe.models.Job instance
     87     """
     88     return (_ssp_enabled()
     89             and is_server_job(job)
     90             # None is the same as True.
     91             and job.require_ssp != False)
     92 
     93 
     94 def _ssp_enabled():
     95     """Return whether SSP is enabled in the config."""
     96     global_config = autotest.load('client.common_lib.global_config')
     97     return global_config.global_config.get_config_value(
     98             'AUTOSERV', 'enable_ssp_container', type=bool,
     99             default=True)
    100 
    101 
    102 def control_file_path(workdir):
    103     """Path to control file for a job.
    104 
    105     This makes more sense in the old Autotest drone world.  The
    106     scheduler has to copy the control file to the drone.  It goes to a
    107     temporary path `drone_tmp/attach.N`.
    108 
    109     The drone is then able to run `autoserv <args> drone_tmp/attach.N`.
    110 
    111     But in the Lucifer world, we are already running on the drone, so we
    112     don't need to rendezvous with a temporary directory through
    113     drone_manager first.
    114 
    115     So pick an arbitrary filename to plop into the workdir.  autoserv
    116     will later copy this back to the standard control/control.srv.
    117     """
    118     return os.path.join(workdir, 'lucifer', 'control_attach')
    119 
    120 
    121 def prepare_control_file(job, workdir):
    122     """Prepare control file for a job."""
    123     with open(control_file_path(workdir), 'w') as f:
    124         f.write(job.control_file)
    125 
    126 
    127 def prepare_keyvals_files(job, workdir):
    128     """Prepare Autotest keyvals files for a job."""
    129     keyvals = job.keyval_dict()
    130     keyvals['job_queued'] = \
    131             int(time.mktime(job.created_on.timetuple()))
    132     results.write_keyvals(workdir, keyvals)
    133     if is_hostless(job):
    134         return
    135     for hqe in job.hostqueueentry_set.all().prefetch_related('host'):
    136         results.write_host_keyvals(
    137                 workdir, hqe.host.hostname, _host_keyvals(hqe.host))
    138 
    139 
    140 def write_aborted_keyvals_and_status(job, workdir):
    141     """Write the keyvals and status for an aborted job."""
    142     aborted_by = 'autotest_system'
    143     aborted_on = int(time.time())
    144     for hqe in job.hostqueueentry_set.all():
    145         if not hasattr(hqe, 'abortedhostqueueentry'):
    146             continue
    147         ahqe = hqe.abortedhostqueueentry
    148         aborted_by = ahqe.aborted_by
    149         aborted_on = int(time.mktime(ahqe.aborted_on.timetuple()))
    150         break
    151     results.write_keyvals(workdir, {
    152             'aborted_by': aborted_by,
    153             'aborted_on': aborted_on,
    154     })
    155     results.write_status_comment(
    156             workdir, 'Job aborted by %s on %s' % (aborted_by, aborted_on))
    157 
    158 
    159 def _host_keyvals(host):
    160     """Return keyvals dict for a host.
    161 
    162     @param host: frontend.afe.models.Host instance
    163     """
    164     labels = list(_host_labels(host))
    165     platform = None
    166     for label in labels:
    167         if label.platform:
    168             platform = label.name
    169     return {
    170             'platform': platform,
    171             'labels': ','.join(urllib.quote(label.name) for label in labels),
    172     }
    173 
    174 
    175 def _host_labels(host):
    176     """Return an iterable of labels for a host.
    177 
    178     @param host: frontend.afe.models.Host instance
    179     """
    180     if autotest.load('scheduler.scheduler_models').RESPECT_STATIC_LABELS:
    181         return _host_labels_with_static(host)
    182     else:
    183         return host.labels.all()
    184 
    185 
    186 def _host_labels_with_static(host):
    187     """Return a generator of labels for a host, respecting static labels.
    188 
    189     @param host: frontend.afe.models.Host instance
    190     """
    191     models = autotest.load('frontend.afe.models')
    192     replaced_label_ids = frozenset(models.ReplacedLabel.objects.all()
    193                                    .values_list('label_id', flat=True))
    194     shadowed_labels = set()
    195     for label in host.labels.all():
    196         if label.id in replaced_label_ids:
    197             shadowed_labels.add(label.name)
    198         else:
    199             yield label
    200     for label in host.static_labels.all():
    201         if label.name in shadowed_labels:
    202             yield label
    203