xref: /aosp_15_r20/external/autotest/server/cros/dynamic_suite/tools.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li
7*9c5db199SXin Lifrom __future__ import absolute_import
8*9c5db199SXin Lifrom __future__ import division
9*9c5db199SXin Lifrom __future__ import print_function
10*9c5db199SXin Li
11*9c5db199SXin Liimport random
12*9c5db199SXin Liimport re
13*9c5db199SXin Liimport six
14*9c5db199SXin Li
15*9c5db199SXin Liimport common
16*9c5db199SXin Li
17*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
18*9c5db199SXin Li
19*9c5db199SXin Li_CONFIG = global_config.global_config
20*9c5db199SXin Li
21*9c5db199SXin Li# comments injected into the control file.
22*9c5db199SXin Li_INJECT_BEGIN = '# INJECT_BEGIN - DO NOT DELETE THIS LINE'
23*9c5db199SXin Li_INJECT_END = '# INJECT_END - DO NOT DELETE LINE'
24*9c5db199SXin Li
25*9c5db199SXin Li
26*9c5db199SXin Li# The regex for an injected line in the control file with the format:
27*9c5db199SXin Li# varable_name=varable_value
28*9c5db199SXin Li_INJECT_VAR_RE = re.compile('^[_A-Za-z]\w*=.+$')
29*9c5db199SXin Li
30*9c5db199SXin Li
31*9c5db199SXin Lidef image_url_pattern():
32*9c5db199SXin Li    """Returns image_url_pattern from global_config."""
33*9c5db199SXin Li    return _CONFIG.get_config_value('CROS', 'image_url_pattern', type=str)
34*9c5db199SXin Li
35*9c5db199SXin Li
36*9c5db199SXin Lidef firmware_url_pattern():
37*9c5db199SXin Li    """Returns firmware_url_pattern from global_config."""
38*9c5db199SXin Li    return _CONFIG.get_config_value('CROS', 'firmware_url_pattern', type=str)
39*9c5db199SXin Li
40*9c5db199SXin Li
41*9c5db199SXin Lidef factory_image_url_pattern():
42*9c5db199SXin Li    """Returns path to factory image after it's been staged."""
43*9c5db199SXin Li    return _CONFIG.get_config_value('CROS', 'factory_image_url_pattern',
44*9c5db199SXin Li                                    type=str)
45*9c5db199SXin Li
46*9c5db199SXin Li
47*9c5db199SXin Lidef sharding_factor():
48*9c5db199SXin Li    """Returns sharding_factor from global_config."""
49*9c5db199SXin Li    return _CONFIG.get_config_value('CROS', 'sharding_factor', type=int)
50*9c5db199SXin Li
51*9c5db199SXin Li
52*9c5db199SXin Lidef infrastructure_user():
53*9c5db199SXin Li    """Returns infrastructure_user from global_config."""
54*9c5db199SXin Li    return _CONFIG.get_config_value('CROS', 'infrastructure_user', type=str)
55*9c5db199SXin Li
56*9c5db199SXin Li
57*9c5db199SXin Lidef package_url_pattern(is_launch_control_build=False):
58*9c5db199SXin Li    """Returns package_url_pattern from global_config.
59*9c5db199SXin Li
60*9c5db199SXin Li    @param is_launch_control_build: True if the package url is for Launch
61*9c5db199SXin Li            Control build. Default is False.
62*9c5db199SXin Li    """
63*9c5db199SXin Li    if is_launch_control_build:
64*9c5db199SXin Li        return _CONFIG.get_config_value('ANDROID', 'package_url_pattern',
65*9c5db199SXin Li                                        type=str)
66*9c5db199SXin Li    else:
67*9c5db199SXin Li        return _CONFIG.get_config_value('CROS', 'package_url_pattern', type=str)
68*9c5db199SXin Li
69*9c5db199SXin Li
70*9c5db199SXin Lidef try_job_timeout_mins():
71*9c5db199SXin Li    """Returns try_job_timeout_mins from global_config."""
72*9c5db199SXin Li    return _CONFIG.get_config_value('SCHEDULER', 'try_job_timeout_mins',
73*9c5db199SXin Li                                    type=int, default=4*60)
74*9c5db199SXin Li
75*9c5db199SXin Li
76*9c5db199SXin Lidef get_package_url(devserver_url, build):
77*9c5db199SXin Li    """Returns the package url from the |devserver_url| and |build|.
78*9c5db199SXin Li
79*9c5db199SXin Li    @param devserver_url: a string specifying the host to contact e.g.
80*9c5db199SXin Li        http://my_host:9090.
81*9c5db199SXin Li    @param build: the build/image string to use e.g. mario-release/R19-123.0.1.
82*9c5db199SXin Li    @return the url where you can find the packages for the build.
83*9c5db199SXin Li    """
84*9c5db199SXin Li    return package_url_pattern() % (devserver_url, build)
85*9c5db199SXin Li
86*9c5db199SXin Li
87*9c5db199SXin Lidef get_devserver_build_from_package_url(package_url,
88*9c5db199SXin Li                                         is_launch_control_build=False):
89*9c5db199SXin Li    """The inverse method of get_package_url.
90*9c5db199SXin Li
91*9c5db199SXin Li    @param package_url: a string specifying the package url.
92*9c5db199SXin Li    @param is_launch_control_build: True if the package url is for Launch
93*9c5db199SXin Li                Control build. Default is False.
94*9c5db199SXin Li
95*9c5db199SXin Li    @return tuple containing the devserver_url, build.
96*9c5db199SXin Li    """
97*9c5db199SXin Li    pattern = package_url_pattern(is_launch_control_build)
98*9c5db199SXin Li    re_pattern = pattern.replace('%s', '(\S+)')
99*9c5db199SXin Li
100*9c5db199SXin Li    devserver_build_tuple = re.search(re_pattern, package_url).groups()
101*9c5db199SXin Li
102*9c5db199SXin Li    # TODO(beeps): This is a temporary hack around the fact that all
103*9c5db199SXin Li    # job_repo_urls in the database currently contain 'archive'. Remove
104*9c5db199SXin Li    # when all hosts have been reimaged at least once. Ref: crbug.com/214373.
105*9c5db199SXin Li    return (devserver_build_tuple[0],
106*9c5db199SXin Li            devserver_build_tuple[1].replace('archive/', ''))
107*9c5db199SXin Li
108*9c5db199SXin Li
109*9c5db199SXin Lidef get_build_from_image(image):
110*9c5db199SXin Li    """Get the build name from the image string.
111*9c5db199SXin Li
112*9c5db199SXin Li    @param image: A string of image, can be the build name or a url to the
113*9c5db199SXin Li                  build, e.g.,
114*9c5db199SXin Li                  http://devserver/update/alex-release/R27-3837.0.0
115*9c5db199SXin Li
116*9c5db199SXin Li    @return: Name of the build. Return None if fail to parse build name.
117*9c5db199SXin Li    """
118*9c5db199SXin Li    if not image.startswith('http://'):
119*9c5db199SXin Li        return image
120*9c5db199SXin Li    else:
121*9c5db199SXin Li        match = re.match('.*/([^/]+/R\d+-[^/]+)', image)
122*9c5db199SXin Li        if match:
123*9c5db199SXin Li            return match.group(1)
124*9c5db199SXin Li
125*9c5db199SXin Li
126*9c5db199SXin Lidef get_random_best_host(afe, host_list, require_usable_hosts=True):
127*9c5db199SXin Li    """
128*9c5db199SXin Li    Randomly choose the 'best' host from host_list, using fresh status.
129*9c5db199SXin Li
130*9c5db199SXin Li    Hit the AFE to get latest status for the listed hosts.  Then apply
131*9c5db199SXin Li    the following heuristic to pick the 'best' set:
132*9c5db199SXin Li
133*9c5db199SXin Li    Remove unusable hosts (not tools.is_usable()), then
134*9c5db199SXin Li    'Ready' > 'Running, Cleaning, Verifying, etc'
135*9c5db199SXin Li
136*9c5db199SXin Li    If any 'Ready' hosts exist, return a random choice.  If not, randomly
137*9c5db199SXin Li    choose from the next tier.  If there are none of those either, None.
138*9c5db199SXin Li
139*9c5db199SXin Li    @param afe: autotest front end that holds the hosts being managed.
140*9c5db199SXin Li    @param host_list: an iterable of Host objects, per server/frontend.py
141*9c5db199SXin Li    @param require_usable_hosts: only return hosts currently in a usable
142*9c5db199SXin Li                                 state.
143*9c5db199SXin Li    @return a Host object, or None if no appropriate host is found.
144*9c5db199SXin Li    """
145*9c5db199SXin Li    if not host_list:
146*9c5db199SXin Li        return None
147*9c5db199SXin Li    hostnames = [host.hostname for host in host_list]
148*9c5db199SXin Li    updated_hosts = afe.get_hosts(hostnames=hostnames)
149*9c5db199SXin Li    usable_hosts = [host for host in updated_hosts if is_usable(host)]
150*9c5db199SXin Li    ready_hosts = [host for host in usable_hosts if host.status == 'Ready']
151*9c5db199SXin Li    unusable_hosts = [h for h in updated_hosts if not is_usable(h)]
152*9c5db199SXin Li    if ready_hosts:
153*9c5db199SXin Li        return random.choice(ready_hosts)
154*9c5db199SXin Li    if usable_hosts:
155*9c5db199SXin Li        return random.choice(usable_hosts)
156*9c5db199SXin Li    if not require_usable_hosts and unusable_hosts:
157*9c5db199SXin Li        return random.choice(unusable_hosts)
158*9c5db199SXin Li    return None
159*9c5db199SXin Li
160*9c5db199SXin Li
161*9c5db199SXin Lidef remove_legacy_injection(control_file_in):
162*9c5db199SXin Li    """
163*9c5db199SXin Li    Removes the legacy injection part from a control file.
164*9c5db199SXin Li
165*9c5db199SXin Li    @param control_file_in: the contents of a control file to munge.
166*9c5db199SXin Li
167*9c5db199SXin Li    @return The modified control file string.
168*9c5db199SXin Li    """
169*9c5db199SXin Li    if not control_file_in:
170*9c5db199SXin Li        return control_file_in
171*9c5db199SXin Li
172*9c5db199SXin Li    new_lines = []
173*9c5db199SXin Li    lines = control_file_in.strip().splitlines()
174*9c5db199SXin Li    remove_done = False
175*9c5db199SXin Li    for line in lines:
176*9c5db199SXin Li        if remove_done:
177*9c5db199SXin Li            new_lines.append(line)
178*9c5db199SXin Li        else:
179*9c5db199SXin Li            if not _INJECT_VAR_RE.match(line):
180*9c5db199SXin Li                remove_done = True
181*9c5db199SXin Li                new_lines.append(line)
182*9c5db199SXin Li    return '\n'.join(new_lines)
183*9c5db199SXin Li
184*9c5db199SXin Li
185*9c5db199SXin Lidef remove_injection(control_file_in):
186*9c5db199SXin Li    """
187*9c5db199SXin Li    Removes the injection part from a control file.
188*9c5db199SXin Li
189*9c5db199SXin Li    @param control_file_in: the contents of a control file to munge.
190*9c5db199SXin Li
191*9c5db199SXin Li    @return The modified control file string.
192*9c5db199SXin Li    """
193*9c5db199SXin Li    if not control_file_in:
194*9c5db199SXin Li        return control_file_in
195*9c5db199SXin Li
196*9c5db199SXin Li    start = control_file_in.find(_INJECT_BEGIN)
197*9c5db199SXin Li    if start >=0:
198*9c5db199SXin Li        end = control_file_in.find(_INJECT_END, start)
199*9c5db199SXin Li    if start < 0 or end < 0:
200*9c5db199SXin Li        return remove_legacy_injection(control_file_in)
201*9c5db199SXin Li
202*9c5db199SXin Li    end += len(_INJECT_END)
203*9c5db199SXin Li    ch = control_file_in[end]
204*9c5db199SXin Li    total_length = len(control_file_in)
205*9c5db199SXin Li    while end <= total_length and (
206*9c5db199SXin Li            ch == '\n' or ch == ' ' or ch == '\t'):
207*9c5db199SXin Li        end += 1
208*9c5db199SXin Li        if end < total_length:
209*9c5db199SXin Li            ch = control_file_in[end]
210*9c5db199SXin Li    return control_file_in[:start] + control_file_in[end:]
211*9c5db199SXin Li
212*9c5db199SXin Li
213*9c5db199SXin Lidef inject_vars(vars, control_file_in):
214*9c5db199SXin Li    """
215*9c5db199SXin Li    Inject the contents of |vars| into |control_file_in|.
216*9c5db199SXin Li
217*9c5db199SXin Li    @param vars: a dict to shoehorn into the provided control file string.
218*9c5db199SXin Li    @param control_file_in: the contents of a control file to munge.
219*9c5db199SXin Li    @return the modified control file string.
220*9c5db199SXin Li    """
221*9c5db199SXin Li    control_file = ''
222*9c5db199SXin Li    control_file += _INJECT_BEGIN + '\n'
223*9c5db199SXin Li    for key, value in six.iteritems(vars):
224*9c5db199SXin Li        # None gets injected as 'None' without this check; same for digits.
225*9c5db199SXin Li        if isinstance(value, str):
226*9c5db199SXin Li            control_file += "%s=%s\n" % (key, repr(value))
227*9c5db199SXin Li        else:
228*9c5db199SXin Li            control_file += "%s=%r\n" % (key, value)
229*9c5db199SXin Li
230*9c5db199SXin Li    args_dict_str = "%s=%s\n" % ('args_dict', repr(vars))
231*9c5db199SXin Li    return control_file + args_dict_str + _INJECT_END + '\n' + control_file_in
232*9c5db199SXin Li
233*9c5db199SXin Li
234*9c5db199SXin Lidef is_usable(host):
235*9c5db199SXin Li    """
236*9c5db199SXin Li    Given a host, determine if the host is usable right now.
237*9c5db199SXin Li
238*9c5db199SXin Li    @param host: Host instance (as in server/frontend.py)
239*9c5db199SXin Li    @return True if host is alive and not incorrectly locked.  Else, False.
240*9c5db199SXin Li    """
241*9c5db199SXin Li    return alive(host) and not incorrectly_locked(host)
242*9c5db199SXin Li
243*9c5db199SXin Li
244*9c5db199SXin Lidef alive(host):
245*9c5db199SXin Li    """
246*9c5db199SXin Li    Given a host, determine if the host is alive.
247*9c5db199SXin Li
248*9c5db199SXin Li    @param host: Host instance (as in server/frontend.py)
249*9c5db199SXin Li    @return True if host is not under, or in need of, repair.  Else, False.
250*9c5db199SXin Li    """
251*9c5db199SXin Li    return host.status not in ['Repair Failed', 'Repairing']
252*9c5db199SXin Li
253*9c5db199SXin Li
254*9c5db199SXin Lidef incorrectly_locked(host):
255*9c5db199SXin Li    """
256*9c5db199SXin Li    Given a host, determine if the host is locked by some user.
257*9c5db199SXin Li
258*9c5db199SXin Li    If the host is unlocked, or locked by the test infrastructure,
259*9c5db199SXin Li    this will return False.  There is only one system user defined as part
260*9c5db199SXin Li    of the test infrastructure and is listed in global_config.ini under the
261*9c5db199SXin Li    [CROS] section in the 'infrastructure_user' field.
262*9c5db199SXin Li
263*9c5db199SXin Li    @param host: Host instance (as in server/frontend.py)
264*9c5db199SXin Li    @return False if the host is not locked, or locked by the infra.
265*9c5db199SXin Li            True if the host is locked by the infra user.
266*9c5db199SXin Li    """
267*9c5db199SXin Li    return (host.locked and host.locked_by != infrastructure_user())
268*9c5db199SXin Li
269*9c5db199SXin Li
270*9c5db199SXin Lidef _testname_to_keyval_key(testname):
271*9c5db199SXin Li    """Make a test name acceptable as a keyval key.
272*9c5db199SXin Li
273*9c5db199SXin Li    @param  testname Test name that must be converted.
274*9c5db199SXin Li    @return          A string with selected bad characters replaced
275*9c5db199SXin Li                     with allowable characters.
276*9c5db199SXin Li    """
277*9c5db199SXin Li    # Characters for keys in autotest keyvals are restricted; in
278*9c5db199SXin Li    # particular, '/' isn't allowed.  Alas, in the case of an
279*9c5db199SXin Li    # aborted job, the test name will be a path that includes '/'
280*9c5db199SXin Li    # characters.  We want to file bugs for aborted jobs, so we
281*9c5db199SXin Li    # apply a transform here to avoid trouble.
282*9c5db199SXin Li    return testname.replace('/', '_')
283*9c5db199SXin Li
284*9c5db199SXin Li
285*9c5db199SXin Li_BUG_ID_KEYVAL = '-Bug_Id'
286*9c5db199SXin Li_BUG_COUNT_KEYVAL = '-Bug_Count'
287*9c5db199SXin Li
288*9c5db199SXin Li
289*9c5db199SXin Lidef create_bug_keyvals(job_id, testname, bug_info):
290*9c5db199SXin Li    """Create keyvals to record a bug filed against a test failure.
291*9c5db199SXin Li
292*9c5db199SXin Li    @param testname  Name of the test for which to record a bug.
293*9c5db199SXin Li    @param bug_info  Pair with the id of the bug and the count of
294*9c5db199SXin Li                     the number of times the bug has been seen.
295*9c5db199SXin Li    @param job_id    The afe job id of job which the test is associated to.
296*9c5db199SXin Li                     job_id will be a part of the key.
297*9c5db199SXin Li    @return          Keyvals to be recorded for the given test.
298*9c5db199SXin Li    """
299*9c5db199SXin Li    testname = _testname_to_keyval_key(testname)
300*9c5db199SXin Li    keyval_base = '%s_%s' % (job_id, testname) if job_id else testname
301*9c5db199SXin Li    return {
302*9c5db199SXin Li        keyval_base + _BUG_ID_KEYVAL: bug_info[0],
303*9c5db199SXin Li        keyval_base + _BUG_COUNT_KEYVAL: bug_info[1]
304*9c5db199SXin Li    }
305*9c5db199SXin Li
306*9c5db199SXin Li
307*9c5db199SXin Lidef get_test_failure_bug_info(keyvals, job_id, testname):
308*9c5db199SXin Li    """Extract information about a bug filed against a test failure.
309*9c5db199SXin Li
310*9c5db199SXin Li    This method tries to extract bug_id and bug_count from the keyvals
311*9c5db199SXin Li    of a suite. If for some reason it cannot retrieve the bug_id it will
312*9c5db199SXin Li    return (None, None) and there will be no link to the bug filed. We will
313*9c5db199SXin Li    instead link directly to the logs of the failed test.
314*9c5db199SXin Li
315*9c5db199SXin Li    If it cannot retrieve the bug_count, it will return (int(bug_id), None)
316*9c5db199SXin Li    and this will result in a link to the bug filed, with an inline message
317*9c5db199SXin Li    saying we weren't able to determine how many times the bug occured.
318*9c5db199SXin Li
319*9c5db199SXin Li    If it retrieved both the bug_id and bug_count, we return a tuple of 2
320*9c5db199SXin Li    integers and link to the bug filed, as well as mention how many times
321*9c5db199SXin Li    the bug has occured in the buildbot stages.
322*9c5db199SXin Li
323*9c5db199SXin Li    @param keyvals  Keyvals associated with a suite job.
324*9c5db199SXin Li    @param job_id   The afe job id of the job that runs the test.
325*9c5db199SXin Li    @param testname Name of a test from the suite.
326*9c5db199SXin Li    @return         None if there is no bug info, or a pair with the
327*9c5db199SXin Li                    id of the bug, and the count of the number of
328*9c5db199SXin Li                    times the bug has been seen.
329*9c5db199SXin Li    """
330*9c5db199SXin Li    testname = _testname_to_keyval_key(testname)
331*9c5db199SXin Li    keyval_base = '%s_%s' % (job_id, testname) if job_id else testname
332*9c5db199SXin Li    bug_id = keyvals.get(keyval_base + _BUG_ID_KEYVAL)
333*9c5db199SXin Li    if not bug_id:
334*9c5db199SXin Li        return None, None
335*9c5db199SXin Li    bug_id = int(bug_id)
336*9c5db199SXin Li    bug_count = keyvals.get(keyval_base + _BUG_COUNT_KEYVAL)
337*9c5db199SXin Li    bug_count = int(bug_count) if bug_count else None
338*9c5db199SXin Li    return bug_id, bug_count
339*9c5db199SXin Li
340*9c5db199SXin Li
341*9c5db199SXin Lidef create_job_name(build, suite, test_name):
342*9c5db199SXin Li    """Create the name of a test job based on given build, suite, and test_name.
343*9c5db199SXin Li
344*9c5db199SXin Li    @param build: name of the build, e.g., lumpy-release/R31-1234.0.0.
345*9c5db199SXin Li    @param suite: name of the suite, e.g., bvt.
346*9c5db199SXin Li    @param test_name: name of the test, e.g., stub_ServerToClientPass.
347*9c5db199SXin Li    @return: the test job's name, e.g.,
348*9c5db199SXin Li             lumpy-release/R31-1234.0.0/bvt/stub_ServerToClientPass.
349*9c5db199SXin Li    """
350*9c5db199SXin Li    return '/'.join([build, suite, test_name])
351*9c5db199SXin Li
352*9c5db199SXin Li
353*9c5db199SXin Lidef get_test_name(build, suite, test_job_name):
354*9c5db199SXin Li    """Get the test name from test job name.
355*9c5db199SXin Li
356*9c5db199SXin Li    Name of test job may contain information like build and suite. This method
357*9c5db199SXin Li    strips these information and return only the test name.
358*9c5db199SXin Li
359*9c5db199SXin Li    @param build: name of the build, e.g., lumpy-release/R31-1234.0.0.
360*9c5db199SXin Li    @param suite: name of the suite, e.g., bvt.
361*9c5db199SXin Li    @param test_job_name: name of the test job, e.g.,
362*9c5db199SXin Li                          lumpy-release/R31-1234.0.0/bvt/stub_ServerToClientPass.
363*9c5db199SXin Li    @return: the test name, e.g., stub_ServerToClientPass.
364*9c5db199SXin Li    """
365*9c5db199SXin Li    # Do not change this naming convention without updating
366*9c5db199SXin Li    # site_utils.parse_job_name.
367*9c5db199SXin Li    return test_job_name.replace('%s/%s/' % (build, suite), '')
368