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