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