blob: e3fa0c195177158b9ed728fdb3a3dd915f85f937 [file] [log] [blame]
Richard Barnette6c2b70a2017-01-26 13:40:51 -08001# pylint: disable=missing-docstring
Don Garretta06ea082017-01-13 00:04:26 +00002
mblighe8819cd2008-02-15 16:48:40 +00003"""\
4Functions to expose over the RPC interface.
5
6For all modify* and delete* functions that ask for an 'id' parameter to
7identify the object to operate on, the id may be either
8 * the database row ID
9 * the name of the object (label name, hostname, user login, etc.)
10 * a dictionary containing uniquely identifying field (this option should seldom
11 be used)
12
13When specifying foreign key fields (i.e. adding hosts to a label, or adding
14users to an ACL group), the given value may be either the database row ID or the
15name of the object.
16
17All get* functions return lists of dictionaries. Each dictionary represents one
18object and maps field names to values.
19
20Some examples:
21modify_host(2, hostname='myhost') # modify hostname of host with database ID 2
22modify_host('ipaj2', hostname='myhost') # modify hostname of host 'ipaj2'
23modify_test('sleeptest', test_type='Client', params=', seconds=60')
24delete_acl_group(1) # delete by ID
25delete_acl_group('Everyone') # delete by name
26acl_group_add_users('Everyone', ['mbligh', 'showard'])
27get_jobs(owner='showard', status='Queued')
28
mbligh93c80e62009-02-03 17:48:30 +000029See doctests/001_rpc_test.txt for (lots) more examples.
mblighe8819cd2008-02-15 16:48:40 +000030"""
31
32__author__ = 'showard@google.com (Steve Howard)'
33
Michael Tang6dc174e2016-05-31 23:13:42 -070034import ast
showard29f7cd22009-04-29 21:16:24 +000035import datetime
Shuqian Zhao4c0d2902016-01-12 17:03:15 -080036import logging
Allen Licdd00f22017-02-01 18:01:52 -080037import os
Dan Shi4a3deb82016-10-27 21:32:30 -070038import sys
MK Ryu9c5fbbe2015-02-11 15:46:22 -080039
Moises Osorio2dc7a102014-12-02 18:24:02 -080040from django.db.models import Count
Allen Licdd00f22017-02-01 18:01:52 -080041
showardcafd16e2009-05-29 18:37:49 +000042import common
Aviv Keshet14cac442016-11-20 21:44:11 -080043# TODO(akeshet): Replace with monarch stats once we know how to instrument rpc
44# server with ts_mon.
Gabe Black1e1c41b2015-02-04 23:55:15 -080045from autotest_lib.client.common_lib.cros.graphite import autotest_stats
Allen Licdd00f22017-02-01 18:01:52 -080046from autotest_lib.client.common_lib import control_data
47from autotest_lib.client.common_lib import error
48from autotest_lib.client.common_lib import global_config
49from autotest_lib.client.common_lib import priorities
50from autotest_lib.client.common_lib import time_utils
51from autotest_lib.client.common_lib.cros import dev_server
Allen Lia59b1262016-12-14 12:53:51 -080052from autotest_lib.frontend.afe import control_file as control_file_lib
Allen Licdd00f22017-02-01 18:01:52 -080053from autotest_lib.frontend.afe import model_attributes
54from autotest_lib.frontend.afe import model_logic
55from autotest_lib.frontend.afe import models
Allen Lia59b1262016-12-14 12:53:51 -080056from autotest_lib.frontend.afe import rpc_utils
Moises Osorio2dc7a102014-12-02 18:24:02 -080057from autotest_lib.frontend.tko import models as tko_models
Jiaxi Luoaac54572014-06-04 13:57:02 -070058from autotest_lib.frontend.tko import rpc_interface as tko_rpc_interface
J. Richard Barnetteb5164d62015-04-13 12:59:31 -070059from autotest_lib.server import frontend
Simran Basi71206ef2014-08-13 13:51:18 -070060from autotest_lib.server import utils
Dan Shid215dbe2015-06-18 16:14:59 -070061from autotest_lib.server.cros import provision
Allen Licdd00f22017-02-01 18:01:52 -080062from autotest_lib.server.cros.dynamic_suite import constants
63from autotest_lib.server.cros.dynamic_suite import control_file_getter
64from autotest_lib.server.cros.dynamic_suite import suite as SuiteBase
Jiaxi Luo90190c92014-06-18 12:35:57 -070065from autotest_lib.server.cros.dynamic_suite import tools
Allen Licdd00f22017-02-01 18:01:52 -080066from autotest_lib.server.cros.dynamic_suite.suite import Suite
Aviv Keshet7ee95862016-08-30 15:18:27 -070067from autotest_lib.server.lib import status_history
Allen Licdd00f22017-02-01 18:01:52 -080068from autotest_lib.site_utils import host_history
69from autotest_lib.site_utils import job_history
70from autotest_lib.site_utils import server_manager_utils
71from autotest_lib.site_utils import stable_version_utils
mblighe8819cd2008-02-15 16:48:40 +000072
Moises Osorio2dc7a102014-12-02 18:24:02 -080073
Allen Licdd00f22017-02-01 18:01:52 -080074_CONFIG = global_config.global_config
75
76# Relevant CrosDynamicSuiteExceptions are defined in client/common_lib/error.py.
77
mblighe8819cd2008-02-15 16:48:40 +000078# labels
79
mblighe8819cd2008-02-15 16:48:40 +000080def modify_label(id, **data):
MK Ryu8c554cf2015-06-12 11:45:50 -070081 """Modify a label.
82
83 @param id: id or name of a label. More often a label name.
84 @param data: New data for a label.
85 """
86 label_model = models.Label.smart_get(id)
MK Ryu8e2c2d02016-01-06 15:24:38 -080087 label_model.update_object(data)
MK Ryu8c554cf2015-06-12 11:45:50 -070088
89 # Master forwards the RPC to shards
90 if not utils.is_shard():
91 rpc_utils.fanout_rpc(label_model.host_set.all(), 'modify_label', False,
92 id=id, **data)
93
mblighe8819cd2008-02-15 16:48:40 +000094
95def delete_label(id):
MK Ryu8c554cf2015-06-12 11:45:50 -070096 """Delete a label.
97
98 @param id: id or name of a label. More often a label name.
99 """
100 label_model = models.Label.smart_get(id)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800101 # Hosts that have the label to be deleted. Save this info before
102 # the label is deleted to use it later.
103 hosts = []
104 for h in label_model.host_set.all():
105 hosts.append(models.Host.smart_get(h.id))
106 label_model.delete()
MK Ryu8c554cf2015-06-12 11:45:50 -0700107
108 # Master forwards the RPC to shards
109 if not utils.is_shard():
MK Ryu8e2c2d02016-01-06 15:24:38 -0800110 rpc_utils.fanout_rpc(hosts, 'delete_label', False, id=id)
mblighe8819cd2008-02-15 16:48:40 +0000111
Prashanth Balasubramanian744898f2015-01-13 05:04:16 -0800112
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800113def add_label(name, ignore_exception_if_exists=False, **kwargs):
MK Ryucf027c62015-03-04 12:00:50 -0800114 """Adds a new label of a given name.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800115
116 @param name: label name.
117 @param ignore_exception_if_exists: If True and the exception was
118 thrown due to the duplicated label name when adding a label,
119 then suppress the exception. Default is False.
120 @param kwargs: keyword args that store more info about a label
121 other than the name.
122 @return: int/long id of a new label.
123 """
124 # models.Label.add_object() throws model_logic.ValidationError
125 # when it is given a label name that already exists.
126 # However, ValidationError can be thrown with different errors,
127 # and those errors should be thrown up to the call chain.
128 try:
129 label = models.Label.add_object(name=name, **kwargs)
130 except:
131 exc_info = sys.exc_info()
132 if ignore_exception_if_exists:
133 label = rpc_utils.get_label(name)
134 # If the exception is raised not because of duplicated
135 # "name", then raise the original exception.
136 if label is None:
137 raise exc_info[0], exc_info[1], exc_info[2]
138 else:
139 raise exc_info[0], exc_info[1], exc_info[2]
140 return label.id
141
142
143def add_label_to_hosts(id, hosts):
MK Ryucf027c62015-03-04 12:00:50 -0800144 """Adds a label of the given id to the given hosts only in local DB.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800145
146 @param id: id or name of a label. More often a label name.
147 @param hosts: The hostnames of hosts that need the label.
148
149 @raises models.Label.DoesNotExist: If the label with id doesn't exist.
150 """
151 label = models.Label.smart_get(id)
152 host_objs = models.Host.smart_get_bulk(hosts)
153 if label.platform:
154 models.Host.check_no_platform(host_objs)
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700155 # Ensure a host has no more than one board label with it.
156 if label.name.startswith('board:'):
Dan Shib5b8b4f2016-11-02 14:04:02 -0700157 models.Host.check_board_labels_allowed(host_objs, [label.name])
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800158 label.host_set.add(*host_objs)
159
160
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700161def _create_label_everywhere(id, hosts):
162 """
163 Yet another method to create labels.
164
165 ALERT! This method should be run only on master not shards!
166 DO NOT RUN THIS ON A SHARD!!! Deputies will hate you if you do!!!
167
168 This method exists primarily to serve label_add_hosts() and
169 host_add_labels(). Basically it pulls out the label check/add logic
170 from label_add_hosts() into this nice method that not only creates
171 the label but also tells the shards that service the hosts to also
172 create the label.
173
174 @param id: id or name of a label. More often a label name.
175 @param hosts: A list of hostnames or ids. More often hostnames.
176 """
177 try:
178 label = models.Label.smart_get(id)
179 except models.Label.DoesNotExist:
180 # This matches the type checks in smart_get, which is a hack
181 # in and off itself. The aim here is to create any non-existent
182 # label, which we cannot do if the 'id' specified isn't a label name.
183 if isinstance(id, basestring):
184 label = models.Label.smart_get(add_label(id))
185 else:
186 raise ValueError('Label id (%s) does not exist. Please specify '
187 'the argument, id, as a string (label name).'
188 % id)
189
190 # Make sure the label exists on the shard with the same id
191 # as it is on the master.
192 # It is possible that the label is already in a shard because
193 # we are adding a new label only to shards of hosts that the label
194 # is going to be attached.
195 # For example, we add a label L1 to a host in shard S1.
196 # Master and S1 will have L1 but other shards won't.
197 # Later, when we add the same label L1 to hosts in shards S1 and S2,
198 # S1 already has the label but S2 doesn't.
199 # S2 should have the new label without any problem.
200 # We ignore exception in such a case.
201 host_objs = models.Host.smart_get_bulk(hosts)
202 rpc_utils.fanout_rpc(
203 host_objs, 'add_label', include_hostnames=False,
204 name=label.name, ignore_exception_if_exists=True,
205 id=label.id, platform=label.platform)
206
207
MK Ryufbb002c2015-06-08 14:13:16 -0700208@rpc_utils.route_rpc_to_master
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800209def label_add_hosts(id, hosts):
MK Ryucf027c62015-03-04 12:00:50 -0800210 """Adds a label with the given id to the given hosts.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800211
212 This method should be run only on master not shards.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800213 The given label will be created if it doesn't exist, provided the `id`
214 supplied is a label name not an int/long id.
215
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800216 @param id: id or name of a label. More often a label name.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800217 @param hosts: A list of hostnames or ids. More often hostnames.
218
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800219 @raises ValueError: If the id specified is an int/long (label id)
220 while the label does not exist.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800221 """
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700222 # Create the label.
223 _create_label_everywhere(id, hosts)
224
225 # Add it to the master.
MK Ryu8e2c2d02016-01-06 15:24:38 -0800226 add_label_to_hosts(id, hosts)
MK Ryucf027c62015-03-04 12:00:50 -0800227
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700228 # Add it to the shards.
MK Ryucf027c62015-03-04 12:00:50 -0800229 host_objs = models.Host.smart_get_bulk(hosts)
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800230 rpc_utils.fanout_rpc(host_objs, 'add_label_to_hosts', id=id)
showardbbabf502008-06-06 00:02:02 +0000231
232
MK Ryucf027c62015-03-04 12:00:50 -0800233def remove_label_from_hosts(id, hosts):
234 """Removes a label of the given id from the given hosts only in local DB.
235
236 @param id: id or name of a label.
237 @param hosts: The hostnames of hosts that need to remove the label from.
238 """
showardbe3ec042008-11-12 18:16:07 +0000239 host_objs = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000240 models.Label.smart_get(id).host_set.remove(*host_objs)
showardbbabf502008-06-06 00:02:02 +0000241
242
MK Ryufbb002c2015-06-08 14:13:16 -0700243@rpc_utils.route_rpc_to_master
MK Ryucf027c62015-03-04 12:00:50 -0800244def label_remove_hosts(id, hosts):
245 """Removes a label of the given id from the given hosts.
246
247 This method should be run only on master not shards.
248
249 @param id: id or name of a label.
250 @param hosts: A list of hostnames or ids. More often hostnames.
251 """
MK Ryucf027c62015-03-04 12:00:50 -0800252 host_objs = models.Host.smart_get_bulk(hosts)
MK Ryu26f0c932015-05-28 18:14:33 -0700253 remove_label_from_hosts(id, hosts)
254
MK Ryu8e2c2d02016-01-06 15:24:38 -0800255 rpc_utils.fanout_rpc(host_objs, 'remove_label_from_hosts', id=id)
256
MK Ryucf027c62015-03-04 12:00:50 -0800257
Jiaxi Luo31874592014-06-11 10:36:35 -0700258def get_labels(exclude_filters=(), **filter_data):
showardc92da832009-04-07 18:14:34 +0000259 """\
Jiaxi Luo31874592014-06-11 10:36:35 -0700260 @param exclude_filters: A sequence of dictionaries of filters.
261
showardc92da832009-04-07 18:14:34 +0000262 @returns A sequence of nested dictionaries of label information.
263 """
Jiaxi Luo31874592014-06-11 10:36:35 -0700264 labels = models.Label.query_objects(filter_data)
265 for exclude_filter in exclude_filters:
266 labels = labels.exclude(**exclude_filter)
267 return rpc_utils.prepare_rows_as_nested_dicts(labels, ('atomic_group',))
showardc92da832009-04-07 18:14:34 +0000268
269
270# atomic groups
271
showarde9450c92009-06-30 01:58:52 +0000272def add_atomic_group(name, max_number_of_machines=None, description=None):
showardc92da832009-04-07 18:14:34 +0000273 return models.AtomicGroup.add_object(
274 name=name, max_number_of_machines=max_number_of_machines,
275 description=description).id
276
277
278def modify_atomic_group(id, **data):
279 models.AtomicGroup.smart_get(id).update_object(data)
280
281
282def delete_atomic_group(id):
283 models.AtomicGroup.smart_get(id).delete()
284
285
286def atomic_group_add_labels(id, labels):
287 label_objs = models.Label.smart_get_bulk(labels)
288 models.AtomicGroup.smart_get(id).label_set.add(*label_objs)
289
290
291def atomic_group_remove_labels(id, labels):
292 label_objs = models.Label.smart_get_bulk(labels)
293 models.AtomicGroup.smart_get(id).label_set.remove(*label_objs)
294
295
296def get_atomic_groups(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000297 return rpc_utils.prepare_for_serialization(
showardc92da832009-04-07 18:14:34 +0000298 models.AtomicGroup.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000299
300
301# hosts
302
Matthew Sartori68186332015-04-27 17:19:53 -0700303def add_host(hostname, status=None, locked=None, lock_reason='', protection=None):
304 if locked and not lock_reason:
305 raise model_logic.ValidationError(
306 {'locked': 'Please provide a reason for locking when adding host.'})
307
jadmanski0afbb632008-06-06 21:10:57 +0000308 return models.Host.add_object(hostname=hostname, status=status,
Matthew Sartori68186332015-04-27 17:19:53 -0700309 locked=locked, lock_reason=lock_reason,
310 protection=protection).id
mblighe8819cd2008-02-15 16:48:40 +0000311
312
MK Ryu33889612015-09-04 14:32:35 -0700313@rpc_utils.route_rpc_to_master
314def modify_host(id, **kwargs):
Jakob Juelich50e91f72014-10-01 12:43:23 -0700315 """Modify local attributes of a host.
316
317 If this is called on the master, but the host is assigned to a shard, this
MK Ryu33889612015-09-04 14:32:35 -0700318 will call `modify_host_local` RPC to the responsible shard. This means if
319 a host is being locked using this function, this change will also propagate
320 to shards.
321 When this is called on a shard, the shard just routes the RPC to the master
322 and does nothing.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700323
324 @param id: id of the host to modify.
MK Ryu33889612015-09-04 14:32:35 -0700325 @param kwargs: key=value pairs of values to set on the host.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700326 """
MK Ryu33889612015-09-04 14:32:35 -0700327 rpc_utils.check_modify_host(kwargs)
showardce7c0922009-09-11 18:39:24 +0000328 host = models.Host.smart_get(id)
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800329 try:
330 rpc_utils.check_modify_host_locking(host, kwargs)
331 except model_logic.ValidationError as e:
332 if not kwargs.get('force_modify_locking', False):
333 raise
334 logging.exception('The following exception will be ignored and lock '
335 'modification will be enforced. %s', e)
Jakob Juelich50e91f72014-10-01 12:43:23 -0700336
MK Ryud53e1492015-12-15 12:09:03 -0800337 # This is required to make `lock_time` for a host be exactly same
338 # between the master and a shard.
339 if kwargs.get('locked', None) and 'lock_time' not in kwargs:
340 kwargs['lock_time'] = datetime.datetime.now()
MK Ryu8e2c2d02016-01-06 15:24:38 -0800341 host.update_object(kwargs)
MK Ryud53e1492015-12-15 12:09:03 -0800342
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800343 # force_modifying_locking is not an internal field in database, remove.
344 kwargs.pop('force_modify_locking', None)
MK Ryu33889612015-09-04 14:32:35 -0700345 rpc_utils.fanout_rpc([host], 'modify_host_local',
346 include_hostnames=False, id=id, **kwargs)
mblighe8819cd2008-02-15 16:48:40 +0000347
348
MK Ryu33889612015-09-04 14:32:35 -0700349def modify_host_local(id, **kwargs):
350 """Modify host attributes in local DB.
351
352 @param id: Host id.
353 @param kwargs: key=value pairs of values to set on the host.
354 """
355 models.Host.smart_get(id).update_object(kwargs)
356
357
358@rpc_utils.route_rpc_to_master
showard276f9442009-05-20 00:33:16 +0000359def modify_hosts(host_filter_data, update_data):
Jakob Juelich50e91f72014-10-01 12:43:23 -0700360 """Modify local attributes of multiple hosts.
361
362 If this is called on the master, but one of the hosts in that match the
MK Ryu33889612015-09-04 14:32:35 -0700363 filters is assigned to a shard, this will call `modify_hosts_local` RPC
364 to the responsible shard.
365 When this is called on a shard, the shard just routes the RPC to the master
366 and does nothing.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700367
368 The filters are always applied on the master, not on the shards. This means
369 if the states of a host differ on the master and a shard, the state on the
370 master will be used. I.e. this means:
371 A host was synced to Shard 1. On Shard 1 the status of the host was set to
372 'Repair Failed'.
373 - A call to modify_hosts with host_filter_data={'status': 'Ready'} will
374 update the host (both on the shard and on the master), because the state
375 of the host as the master knows it is still 'Ready'.
376 - A call to modify_hosts with host_filter_data={'status': 'Repair failed'
377 will not update the host, because the filter doesn't apply on the master.
378
showardbe0d8692009-08-20 23:42:44 +0000379 @param host_filter_data: Filters out which hosts to modify.
380 @param update_data: A dictionary with the changes to make to the hosts.
showard276f9442009-05-20 00:33:16 +0000381 """
MK Ryu93161712015-12-21 10:41:32 -0800382 update_data = update_data.copy()
showardbe0d8692009-08-20 23:42:44 +0000383 rpc_utils.check_modify_host(update_data)
showard276f9442009-05-20 00:33:16 +0000384 hosts = models.Host.query_objects(host_filter_data)
Jakob Juelich50e91f72014-10-01 12:43:23 -0700385
386 affected_shard_hostnames = set()
387 affected_host_ids = []
388
Alex Miller9658a952013-05-14 16:40:02 -0700389 # Check all hosts before changing data for exception safety.
390 for host in hosts:
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800391 try:
392 rpc_utils.check_modify_host_locking(host, update_data)
393 except model_logic.ValidationError as e:
394 if not update_data.get('force_modify_locking', False):
395 raise
396 logging.exception('The following exception will be ignored and '
397 'lock modification will be enforced. %s', e)
398
Jakob Juelich50e91f72014-10-01 12:43:23 -0700399 if host.shard:
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800400 affected_shard_hostnames.add(host.shard.rpc_hostname())
Jakob Juelich50e91f72014-10-01 12:43:23 -0700401 affected_host_ids.append(host.id)
402
MK Ryud53e1492015-12-15 12:09:03 -0800403 # This is required to make `lock_time` for a host be exactly same
404 # between the master and a shard.
405 if update_data.get('locked', None) and 'lock_time' not in update_data:
406 update_data['lock_time'] = datetime.datetime.now()
MK Ryu8e2c2d02016-01-06 15:24:38 -0800407 for host in hosts:
408 host.update_object(update_data)
MK Ryud53e1492015-12-15 12:09:03 -0800409
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800410 update_data.pop('force_modify_locking', None)
MK Ryu33889612015-09-04 14:32:35 -0700411 # Caution: Changing the filter from the original here. See docstring.
412 rpc_utils.run_rpc_on_multiple_hostnames(
413 'modify_hosts_local', affected_shard_hostnames,
Jakob Juelich50e91f72014-10-01 12:43:23 -0700414 host_filter_data={'id__in': affected_host_ids},
415 update_data=update_data)
416
showard276f9442009-05-20 00:33:16 +0000417
MK Ryu33889612015-09-04 14:32:35 -0700418def modify_hosts_local(host_filter_data, update_data):
419 """Modify attributes of hosts in local DB.
420
421 @param host_filter_data: Filters out which hosts to modify.
422 @param update_data: A dictionary with the changes to make to the hosts.
423 """
424 for host in models.Host.query_objects(host_filter_data):
425 host.update_object(update_data)
426
427
MK Ryufbb002c2015-06-08 14:13:16 -0700428def add_labels_to_host(id, labels):
429 """Adds labels to a given host only in local DB.
showardcafd16e2009-05-29 18:37:49 +0000430
MK Ryufbb002c2015-06-08 14:13:16 -0700431 @param id: id or hostname for a host.
432 @param labels: ids or names for labels.
433 """
434 label_objs = models.Label.smart_get_bulk(labels)
435 models.Host.smart_get(id).labels.add(*label_objs)
436
437
438@rpc_utils.route_rpc_to_master
439def host_add_labels(id, labels):
440 """Adds labels to a given host.
441
442 @param id: id or hostname for a host.
443 @param labels: ids or names for labels.
444
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700445 @raises ValidationError: If adding more than one platform/board label.
MK Ryufbb002c2015-06-08 14:13:16 -0700446 """
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700447 # Create the labels on the master/shards.
448 for label in labels:
449 _create_label_everywhere(label, [id])
450
MK Ryufbb002c2015-06-08 14:13:16 -0700451 label_objs = models.Label.smart_get_bulk(labels)
452 platforms = [label.name for label in label_objs if label.platform]
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700453 boards = [label.name for label in label_objs
454 if label.name.startswith('board:')]
Dan Shib5b8b4f2016-11-02 14:04:02 -0700455 if len(platforms) > 1 or not utils.board_labels_allowed(boards):
showardcafd16e2009-05-29 18:37:49 +0000456 raise model_logic.ValidationError(
Dan Shib5b8b4f2016-11-02 14:04:02 -0700457 {'labels': ('Adding more than one platform label, or a list of '
458 'non-compatible board labels.: %s %s' %
459 (', '.join(platforms), ', '.join(boards)))})
MK Ryufbb002c2015-06-08 14:13:16 -0700460
461 host_obj = models.Host.smart_get(id)
Dan Shi4a3deb82016-10-27 21:32:30 -0700462 if platforms:
MK Ryufbb002c2015-06-08 14:13:16 -0700463 models.Host.check_no_platform([host_obj])
Dan Shi4a3deb82016-10-27 21:32:30 -0700464 if boards:
Dan Shib5b8b4f2016-11-02 14:04:02 -0700465 models.Host.check_board_labels_allowed([host_obj], labels)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800466 add_labels_to_host(id, labels)
MK Ryufbb002c2015-06-08 14:13:16 -0700467
468 rpc_utils.fanout_rpc([host_obj], 'add_labels_to_host', False,
469 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000470
471
MK Ryufbb002c2015-06-08 14:13:16 -0700472def remove_labels_from_host(id, labels):
473 """Removes labels from a given host only in local DB.
474
475 @param id: id or hostname for a host.
476 @param labels: ids or names for labels.
477 """
478 label_objs = models.Label.smart_get_bulk(labels)
479 models.Host.smart_get(id).labels.remove(*label_objs)
480
481
482@rpc_utils.route_rpc_to_master
mblighe8819cd2008-02-15 16:48:40 +0000483def host_remove_labels(id, labels):
MK Ryufbb002c2015-06-08 14:13:16 -0700484 """Removes labels from a given host.
485
486 @param id: id or hostname for a host.
487 @param labels: ids or names for labels.
488 """
MK Ryu8e2c2d02016-01-06 15:24:38 -0800489 remove_labels_from_host(id, labels)
490
MK Ryufbb002c2015-06-08 14:13:16 -0700491 host_obj = models.Host.smart_get(id)
492 rpc_utils.fanout_rpc([host_obj], 'remove_labels_from_host', False,
493 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000494
495
MK Ryuacf35922014-10-03 14:56:49 -0700496def get_host_attribute(attribute, **host_filter_data):
497 """
498 @param attribute: string name of attribute
499 @param host_filter_data: filter data to apply to Hosts to choose hosts to
500 act upon
501 """
502 hosts = rpc_utils.get_host_query((), False, False, True, host_filter_data)
503 hosts = list(hosts)
504 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
505 'attribute_list')
506 host_attr_dicts = []
507 for host_obj in hosts:
508 for attr_obj in host_obj.attribute_list:
509 if attr_obj.attribute == attribute:
510 host_attr_dicts.append(attr_obj.get_object_dict())
511 return rpc_utils.prepare_for_serialization(host_attr_dicts)
512
513
showard0957a842009-05-11 19:25:08 +0000514def set_host_attribute(attribute, value, **host_filter_data):
515 """
MK Ryu26f0c932015-05-28 18:14:33 -0700516 @param attribute: string name of attribute
517 @param value: string, or None to delete an attribute
518 @param host_filter_data: filter data to apply to Hosts to choose hosts to
519 act upon
showard0957a842009-05-11 19:25:08 +0000520 """
521 assert host_filter_data # disallow accidental actions on all hosts
522 hosts = models.Host.query_objects(host_filter_data)
523 models.AclGroup.check_for_acl_violation_hosts(hosts)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800524 for host in hosts:
525 host.set_or_delete_attribute(attribute, value)
showard0957a842009-05-11 19:25:08 +0000526
MK Ryu26f0c932015-05-28 18:14:33 -0700527 # Master forwards this RPC to shards.
528 if not utils.is_shard():
529 rpc_utils.fanout_rpc(hosts, 'set_host_attribute', False,
530 attribute=attribute, value=value, **host_filter_data)
531
showard0957a842009-05-11 19:25:08 +0000532
Jakob Juelich50e91f72014-10-01 12:43:23 -0700533@rpc_utils.forward_single_host_rpc_to_shard
mblighe8819cd2008-02-15 16:48:40 +0000534def delete_host(id):
jadmanski0afbb632008-06-06 21:10:57 +0000535 models.Host.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000536
537
showard87cc38f2009-08-20 23:37:04 +0000538def get_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
Dan Shi37df54d2015-12-14 11:16:28 -0800539 exclude_atomic_group_hosts=False, valid_only=True,
540 include_current_job=False, **filter_data):
541 """Get a list of dictionaries which contains the information of hosts.
542
showard87cc38f2009-08-20 23:37:04 +0000543 @param multiple_labels: match hosts in all of the labels given. Should
544 be a list of label names.
545 @param exclude_only_if_needed_labels: Exclude hosts with at least one
546 "only_if_needed" label applied.
547 @param exclude_atomic_group_hosts: Exclude hosts that have one or more
548 atomic group labels associated with them.
Dan Shi37df54d2015-12-14 11:16:28 -0800549 @param include_current_job: Set to True to include ids of currently running
550 job and special task.
jadmanski0afbb632008-06-06 21:10:57 +0000551 """
showard43a3d262008-11-12 18:17:05 +0000552 hosts = rpc_utils.get_host_query(multiple_labels,
553 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000554 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000555 valid_only, filter_data)
showard0957a842009-05-11 19:25:08 +0000556 hosts = list(hosts)
557 models.Host.objects.populate_relationships(hosts, models.Label,
558 'label_list')
559 models.Host.objects.populate_relationships(hosts, models.AclGroup,
560 'acl_list')
561 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
562 'attribute_list')
showard43a3d262008-11-12 18:17:05 +0000563 host_dicts = []
564 for host_obj in hosts:
565 host_dict = host_obj.get_object_dict()
showard0957a842009-05-11 19:25:08 +0000566 host_dict['labels'] = [label.name for label in host_obj.label_list]
showard909c9142009-07-07 20:54:42 +0000567 host_dict['platform'], host_dict['atomic_group'] = (rpc_utils.
568 find_platform_and_atomic_group(host_obj))
showard0957a842009-05-11 19:25:08 +0000569 host_dict['acls'] = [acl.name for acl in host_obj.acl_list]
570 host_dict['attributes'] = dict((attribute.attribute, attribute.value)
571 for attribute in host_obj.attribute_list)
Dan Shi37df54d2015-12-14 11:16:28 -0800572 if include_current_job:
573 host_dict['current_job'] = None
574 host_dict['current_special_task'] = None
575 entries = models.HostQueueEntry.objects.filter(
576 host_id=host_dict['id'], active=True, complete=False)
577 if entries:
578 host_dict['current_job'] = (
579 entries[0].get_object_dict()['job'])
580 tasks = models.SpecialTask.objects.filter(
581 host_id=host_dict['id'], is_active=True, is_complete=False)
582 if tasks:
583 host_dict['current_special_task'] = (
584 '%d-%s' % (tasks[0].get_object_dict()['id'],
585 tasks[0].get_object_dict()['task'].lower()))
showard43a3d262008-11-12 18:17:05 +0000586 host_dicts.append(host_dict)
587 return rpc_utils.prepare_for_serialization(host_dicts)
mblighe8819cd2008-02-15 16:48:40 +0000588
589
showard87cc38f2009-08-20 23:37:04 +0000590def get_num_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
showard8aa84fc2009-09-16 17:17:55 +0000591 exclude_atomic_group_hosts=False, valid_only=True,
592 **filter_data):
showard87cc38f2009-08-20 23:37:04 +0000593 """
594 Same parameters as get_hosts().
595
596 @returns The number of matching hosts.
597 """
showard43a3d262008-11-12 18:17:05 +0000598 hosts = rpc_utils.get_host_query(multiple_labels,
599 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000600 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000601 valid_only, filter_data)
showard43a3d262008-11-12 18:17:05 +0000602 return hosts.count()
showard1385b162008-03-13 15:59:40 +0000603
mblighe8819cd2008-02-15 16:48:40 +0000604
605# tests
606
mblighe8819cd2008-02-15 16:48:40 +0000607def get_tests(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000608 return rpc_utils.prepare_for_serialization(
609 models.Test.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000610
611
Moises Osorio2dc7a102014-12-02 18:24:02 -0800612def get_tests_status_counts_by_job_name_label(job_name_prefix, label_name):
613 """Gets the counts of all passed and failed tests from the matching jobs.
614
Allen Licdd00f22017-02-01 18:01:52 -0800615 @param job_name_prefix: Name prefix of the jobs to get the summary
616 from, e.g., 'butterfly-release/R40-6457.21.0/bvt-cq/'.
Moises Osorio2dc7a102014-12-02 18:24:02 -0800617 @param label_name: Label that must be set in the jobs, e.g.,
618 'cros-version:butterfly-release/R40-6457.21.0'.
619
620 @returns A summary of the counts of all the passed and failed tests.
621 """
622 job_ids = list(models.Job.objects.filter(
623 name__startswith=job_name_prefix,
624 dependency_labels__name=label_name).values_list(
625 'pk', flat=True))
626 summary = {'passed': 0, 'failed': 0}
627 if not job_ids:
628 return summary
629
630 counts = (tko_models.TestView.objects.filter(
631 afe_job_id__in=job_ids).exclude(
632 test_name='SERVER_JOB').exclude(
633 test_name__startswith='CLIENT_JOB').values(
634 'status').annotate(
635 count=Count('status')))
636 for status in counts:
637 if status['status'] == 'GOOD':
638 summary['passed'] += status['count']
639 else:
640 summary['failed'] += status['count']
641 return summary
642
643
showard2b9a88b2008-06-13 20:55:03 +0000644# profilers
645
646def add_profiler(name, description=None):
647 return models.Profiler.add_object(name=name, description=description).id
648
649
650def modify_profiler(id, **data):
651 models.Profiler.smart_get(id).update_object(data)
652
653
654def delete_profiler(id):
655 models.Profiler.smart_get(id).delete()
656
657
658def get_profilers(**filter_data):
659 return rpc_utils.prepare_for_serialization(
660 models.Profiler.list_objects(filter_data))
661
662
mblighe8819cd2008-02-15 16:48:40 +0000663# users
664
mblighe8819cd2008-02-15 16:48:40 +0000665def get_users(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000666 return rpc_utils.prepare_for_serialization(
667 models.User.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000668
669
670# acl groups
671
672def add_acl_group(name, description=None):
showard04f2cd82008-07-25 20:53:31 +0000673 group = models.AclGroup.add_object(name=name, description=description)
showard64a95952010-01-13 21:27:16 +0000674 group.users.add(models.User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000675 return group.id
mblighe8819cd2008-02-15 16:48:40 +0000676
677
678def modify_acl_group(id, **data):
showard04f2cd82008-07-25 20:53:31 +0000679 group = models.AclGroup.smart_get(id)
680 group.check_for_acl_violation_acl_group()
681 group.update_object(data)
682 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000683
684
685def acl_group_add_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000686 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000687 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000688 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000689 group.users.add(*users)
mblighe8819cd2008-02-15 16:48:40 +0000690
691
692def acl_group_remove_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000693 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000694 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000695 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000696 group.users.remove(*users)
showard04f2cd82008-07-25 20:53:31 +0000697 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000698
699
700def acl_group_add_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000701 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000702 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000703 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000704 group.hosts.add(*hosts)
showard08f981b2008-06-24 21:59:03 +0000705 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000706
707
708def acl_group_remove_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000709 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000710 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000711 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000712 group.hosts.remove(*hosts)
showard08f981b2008-06-24 21:59:03 +0000713 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000714
715
716def delete_acl_group(id):
jadmanski0afbb632008-06-06 21:10:57 +0000717 models.AclGroup.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000718
719
720def get_acl_groups(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000721 acl_groups = models.AclGroup.list_objects(filter_data)
722 for acl_group in acl_groups:
723 acl_group_obj = models.AclGroup.objects.get(id=acl_group['id'])
724 acl_group['users'] = [user.login
725 for user in acl_group_obj.users.all()]
726 acl_group['hosts'] = [host.hostname
727 for host in acl_group_obj.hosts.all()]
728 return rpc_utils.prepare_for_serialization(acl_groups)
mblighe8819cd2008-02-15 16:48:40 +0000729
730
731# jobs
732
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700733def generate_control_file(tests=(), profilers=(),
showard91f85102009-10-12 20:34:52 +0000734 client_control_file='', use_container=False,
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700735 profile_only=None, db_tests=True,
736 test_source_build=None):
jadmanski0afbb632008-06-06 21:10:57 +0000737 """
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700738 Generates a client-side control file to run tests.
mbligh120351e2009-01-24 01:40:45 +0000739
Matthew Sartori10438092015-06-24 14:30:18 -0700740 @param tests List of tests to run. See db_tests for more information.
mbligh120351e2009-01-24 01:40:45 +0000741 @param profilers List of profilers to activate during the job.
742 @param client_control_file The contents of a client-side control file to
743 run at the end of all tests. If this is supplied, all tests must be
744 client side.
745 TODO: in the future we should support server control files directly
746 to wrap with a kernel. That'll require changing the parameter
747 name and adding a boolean to indicate if it is a client or server
748 control file.
749 @param use_container unused argument today. TODO: Enable containers
750 on the host during a client side test.
showard91f85102009-10-12 20:34:52 +0000751 @param profile_only A boolean that indicates what default profile_only
752 mode to use in the control file. Passing None will generate a
753 control file that does not explcitly set the default mode at all.
Matthew Sartori10438092015-06-24 14:30:18 -0700754 @param db_tests: if True, the test object can be found in the database
755 backing the test model. In this case, tests is a tuple
756 of test IDs which are used to retrieve the test objects
757 from the database. If False, tests is a tuple of test
758 dictionaries stored client-side in the AFE.
Michael Tang84a2ecf2016-06-07 15:10:53 -0700759 @param test_source_build: Build to be used to retrieve test code. Default
760 to None.
mbligh120351e2009-01-24 01:40:45 +0000761
762 @returns a dict with the following keys:
763 control_file: str, The control file text.
764 is_server: bool, is the control file a server-side control file?
765 synch_count: How many machines the job uses per autoserv execution.
766 synch_count == 1 means the job is asynchronous.
767 dependencies: A list of the names of labels on which the job depends.
768 """
showardd86debe2009-06-10 17:37:56 +0000769 if not tests and not client_control_file:
showard2bab8f42008-11-12 18:15:22 +0000770 return dict(control_file='', is_server=False, synch_count=1,
showard989f25d2008-10-01 11:38:11 +0000771 dependencies=[])
mblighe8819cd2008-02-15 16:48:40 +0000772
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700773 cf_info, test_objects, profiler_objects = (
774 rpc_utils.prepare_generate_control_file(tests, profilers,
775 db_tests))
Allen Lia59b1262016-12-14 12:53:51 -0800776 cf_info['control_file'] = control_file_lib.generate_control(
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700777 tests=test_objects, profilers=profiler_objects,
778 is_server=cf_info['is_server'],
showard232b7ae2009-11-10 00:46:48 +0000779 client_control_file=client_control_file, profile_only=profile_only,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700780 test_source_build=test_source_build)
showard989f25d2008-10-01 11:38:11 +0000781 return cf_info
mblighe8819cd2008-02-15 16:48:40 +0000782
783
Simran Basib6ec8ae2014-04-23 12:05:08 -0700784def create_job_page_handler(name, priority, control_file, control_type,
Dan Shid215dbe2015-06-18 16:14:59 -0700785 image=None, hostless=False, firmware_rw_build=None,
786 firmware_ro_build=None, test_source_build=None,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700787 is_cloning=False, **kwargs):
Simran Basib6ec8ae2014-04-23 12:05:08 -0700788 """\
789 Create and enqueue a job.
790
791 @param name name of this job
792 @param priority Integer priority of this job. Higher is more important.
793 @param control_file String contents of the control file.
794 @param control_type Type of control file, Client or Server.
Dan Shid215dbe2015-06-18 16:14:59 -0700795 @param image: ChromeOS build to be installed in the dut. Default to None.
796 @param firmware_rw_build: Firmware build to update RW firmware. Default to
797 None, i.e., RW firmware will not be updated.
798 @param firmware_ro_build: Firmware build to update RO firmware. Default to
799 None, i.e., RO firmware will not be updated.
800 @param test_source_build: Build to be used to retrieve test code. Default
801 to None.
Michael Tang6dc174e2016-05-31 23:13:42 -0700802 @param is_cloning: True if creating a cloning job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700803 @param kwargs extra args that will be required by create_suite_job or
804 create_job.
805
806 @returns The created Job id number.
807 """
Michael Tang6dc174e2016-05-31 23:13:42 -0700808 if is_cloning:
809 logging.info('Start to clone a new job')
Shuqian Zhao61f5d312016-08-05 17:15:23 -0700810 # When cloning a job, hosts and meta_hosts should not exist together,
811 # which would cause host-scheduler to schedule two hqe jobs to one host
812 # at the same time, and crash itself. Clear meta_hosts for this case.
813 if kwargs.get('hosts') and kwargs.get('meta_hosts'):
814 kwargs['meta_hosts'] = []
Michael Tang6dc174e2016-05-31 23:13:42 -0700815 else:
816 logging.info('Start to create a new job')
Simran Basib6ec8ae2014-04-23 12:05:08 -0700817 control_file = rpc_utils.encode_ascii(control_file)
Jiaxi Luodd67beb2014-07-18 16:28:31 -0700818 if not control_file:
819 raise model_logic.ValidationError({
820 'control_file' : "Control file cannot be empty"})
Simran Basib6ec8ae2014-04-23 12:05:08 -0700821
822 if image and hostless:
Dan Shid215dbe2015-06-18 16:14:59 -0700823 builds = {}
824 builds[provision.CROS_VERSION_PREFIX] = image
825 if firmware_rw_build:
Dan Shi0723bf52015-06-24 10:52:38 -0700826 builds[provision.FW_RW_VERSION_PREFIX] = firmware_rw_build
Dan Shid215dbe2015-06-18 16:14:59 -0700827 if firmware_ro_build:
828 builds[provision.FW_RO_VERSION_PREFIX] = firmware_ro_build
Allen Licdd00f22017-02-01 18:01:52 -0800829 return create_suite_job(
Simran Basib6ec8ae2014-04-23 12:05:08 -0700830 name=name, control_file=control_file, priority=priority,
Michael Tang6dc174e2016-05-31 23:13:42 -0700831 builds=builds, test_source_build=test_source_build,
832 is_cloning=is_cloning, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700833 return create_job(name, priority, control_file, control_type, image=image,
Allen Liac199b62016-12-14 12:56:02 -0800834 hostless=hostless, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700835
836
MK Ryue301eb72015-06-25 12:51:02 -0700837@rpc_utils.route_rpc_to_master
Allen Li8af9da02016-12-12 17:32:39 -0800838def create_job(
839 name,
840 priority,
841 control_file,
842 control_type,
843 hosts=(),
844 meta_hosts=(),
845 one_time_hosts=(),
846 atomic_group_name=None,
847 synch_count=None,
848 is_template=False,
849 timeout=None,
850 timeout_mins=None,
851 max_runtime_mins=None,
852 run_verify=False,
853 email_list='',
854 dependencies=(),
855 reboot_before=None,
856 reboot_after=None,
857 parse_failed_repair=None,
858 hostless=False,
859 keyvals=None,
860 drone_set=None,
861 image=None,
862 parent_job_id=None,
863 test_retry=0,
864 run_reset=True,
865 require_ssp=None,
866 args=(),
Allen Li8af9da02016-12-12 17:32:39 -0800867 **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000868 """\
869 Create and enqueue a job.
mblighe8819cd2008-02-15 16:48:40 +0000870
showarda1e74b32009-05-12 17:32:04 +0000871 @param name name of this job
Alex Miller7d658cf2013-09-04 16:00:35 -0700872 @param priority Integer priority of this job. Higher is more important.
showarda1e74b32009-05-12 17:32:04 +0000873 @param control_file String contents of the control file.
874 @param control_type Type of control file, Client or Server.
875 @param synch_count How many machines the job uses per autoserv execution.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700876 synch_count == 1 means the job is asynchronous. If an atomic group is
877 given this value is treated as a minimum.
showarda1e74b32009-05-12 17:32:04 +0000878 @param is_template If true then create a template job.
879 @param timeout Hours after this call returns until the job times out.
Simran Basi7e605742013-11-12 13:43:36 -0800880 @param timeout_mins Minutes after this call returns until the job times
Jiaxi Luo90190c92014-06-18 12:35:57 -0700881 out.
Simran Basi34217022012-11-06 13:43:15 -0800882 @param max_runtime_mins Minutes from job starting time until job times out
showarda1e74b32009-05-12 17:32:04 +0000883 @param run_verify Should the host be verified before running the test?
884 @param email_list String containing emails to mail when the job is done
885 @param dependencies List of label names on which this job depends
886 @param reboot_before Never, If dirty, or Always
887 @param reboot_after Never, If all tests passed, or Always
888 @param parse_failed_repair if true, results of failed repairs launched by
Jiaxi Luo90190c92014-06-18 12:35:57 -0700889 this job will be parsed as part of the job.
showarda9545c02009-12-18 22:44:26 +0000890 @param hostless if true, create a hostless job
showardc1a98d12010-01-15 00:22:22 +0000891 @param keyvals dict of keyvals to associate with the job
showarda1e74b32009-05-12 17:32:04 +0000892 @param hosts List of hosts to run job on.
893 @param meta_hosts List where each entry is a label name, and for each entry
Jiaxi Luo90190c92014-06-18 12:35:57 -0700894 one host will be chosen from that label to run the job on.
showarda1e74b32009-05-12 17:32:04 +0000895 @param one_time_hosts List of hosts not in the database to run the job on.
896 @param atomic_group_name The name of an atomic group to schedule the job on.
jamesren76fcf192010-04-21 20:39:50 +0000897 @param drone_set The name of the drone set to run this test on.
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800898 @param image OS image to install before running job.
Aviv Keshet0b9cfc92013-02-05 11:36:02 -0800899 @param parent_job_id id of a job considered to be parent of created job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700900 @param test_retry Number of times to retry test if the test did not
Jiaxi Luo90190c92014-06-18 12:35:57 -0700901 complete successfully. (optional, default: 0)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700902 @param run_reset Should the host be reset before running the test?
Dan Shiec1d47d2015-02-13 11:38:13 -0800903 @param require_ssp Set to True to require server-side packaging to run the
904 test. If it's set to None, drone will still try to run
905 the server side with server-side packaging. If the
906 autotest-server package doesn't exist for the build or
907 image is not set, drone will run the test without server-
908 side packaging. Default is None.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700909 @param args A list of args to be injected into control file.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700910 @param kwargs extra keyword args. NOT USED.
showardc92da832009-04-07 18:14:34 +0000911
912 @returns The created Job id number.
jadmanski0afbb632008-06-06 21:10:57 +0000913 """
Jiaxi Luo90190c92014-06-18 12:35:57 -0700914 if args:
915 control_file = tools.inject_vars({'args': args}, control_file)
Richard Barnette6c2b70a2017-01-26 13:40:51 -0800916 if image:
917 dependencies += (provision.image_version_to_label(image),)
jamesren4a41e012010-07-16 22:33:48 +0000918 return rpc_utils.create_job_common(
Allen Li81996a82016-12-14 13:01:37 -0800919 name=name,
920 priority=priority,
921 control_type=control_type,
922 control_file=control_file,
923 hosts=hosts,
924 meta_hosts=meta_hosts,
925 one_time_hosts=one_time_hosts,
926 atomic_group_name=atomic_group_name,
927 synch_count=synch_count,
928 is_template=is_template,
929 timeout=timeout,
930 timeout_mins=timeout_mins,
931 max_runtime_mins=max_runtime_mins,
932 run_verify=run_verify,
933 email_list=email_list,
934 dependencies=dependencies,
935 reboot_before=reboot_before,
936 reboot_after=reboot_after,
937 parse_failed_repair=parse_failed_repair,
938 hostless=hostless,
939 keyvals=keyvals,
940 drone_set=drone_set,
Allen Li81996a82016-12-14 13:01:37 -0800941 parent_job_id=parent_job_id,
942 test_retry=test_retry,
943 run_reset=run_reset,
944 require_ssp=require_ssp)
mblighe8819cd2008-02-15 16:48:40 +0000945
946
showard9dbdcda2008-10-14 17:34:36 +0000947def abort_host_queue_entries(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000948 """\
showard9dbdcda2008-10-14 17:34:36 +0000949 Abort a set of host queue entries.
Fang Deng63b0e452014-12-19 14:38:15 -0800950
951 @return: A list of dictionaries, each contains information
952 about an aborted HQE.
jadmanski0afbb632008-06-06 21:10:57 +0000953 """
showard9dbdcda2008-10-14 17:34:36 +0000954 query = models.HostQueueEntry.query_objects(filter_data)
beepsfaecbce2013-10-29 11:35:10 -0700955
956 # Dont allow aborts on:
957 # 1. Jobs that have already completed (whether or not they were aborted)
958 # 2. Jobs that we have already been aborted (but may not have completed)
959 query = query.filter(complete=False).filter(aborted=False)
showarddc817512008-11-12 18:16:41 +0000960 models.AclGroup.check_abort_permissions(query)
showard9dbdcda2008-10-14 17:34:36 +0000961 host_queue_entries = list(query.select_related())
showard2bab8f42008-11-12 18:15:22 +0000962 rpc_utils.check_abort_synchronous_jobs(host_queue_entries)
mblighe8819cd2008-02-15 16:48:40 +0000963
Simran Basic1b26762013-06-26 14:23:21 -0700964 models.HostQueueEntry.abort_host_queue_entries(host_queue_entries)
Fang Deng63b0e452014-12-19 14:38:15 -0800965 hqe_info = [{'HostQueueEntry': hqe.id, 'Job': hqe.job_id,
966 'Job name': hqe.job.name} for hqe in host_queue_entries]
967 return hqe_info
showard9d821ab2008-07-11 16:54:29 +0000968
969
beeps8bb1f7d2013-08-05 01:30:09 -0700970def abort_special_tasks(**filter_data):
971 """\
972 Abort the special task, or tasks, specified in the filter.
973 """
974 query = models.SpecialTask.query_objects(filter_data)
975 special_tasks = query.filter(is_active=True)
976 for task in special_tasks:
977 task.abort()
978
979
Simran Basi73dae552013-02-25 14:57:46 -0800980def _call_special_tasks_on_hosts(task, hosts):
981 """\
982 Schedules a set of hosts for a special task.
983
984 @returns A list of hostnames that a special task was created for.
985 """
986 models.AclGroup.check_for_acl_violation_hosts(hosts)
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800987 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800988 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -0800989 raise ValueError('The following hosts are on shards, please '
990 'follow the link to the shards and create jobs '
991 'there instead. %s.' % shard_host_map)
Simran Basi73dae552013-02-25 14:57:46 -0800992 for host in hosts:
993 models.SpecialTask.schedule_special_task(host, task)
994 return list(sorted(host.hostname for host in hosts))
995
996
MK Ryu5aa25042015-07-28 16:08:04 -0700997def _forward_special_tasks_on_hosts(task, rpc, **filter_data):
998 """Forward special tasks to corresponding shards.
mbligh4e545a52009-12-19 05:30:39 +0000999
MK Ryu5aa25042015-07-28 16:08:04 -07001000 For master, when special tasks are fired on hosts that are sharded,
1001 forward the RPC to corresponding shards.
1002
1003 For shard, create special task records in local DB.
1004
1005 @param task: Enum value of frontend.afe.models.SpecialTask.Task
1006 @param rpc: RPC name to forward.
1007 @param filter_data: Filter keywords to be used for DB query.
1008
1009 @return: A list of hostnames that a special task was created for.
showard1ff7b2e2009-05-15 23:17:18 +00001010 """
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001011 hosts = models.Host.query_objects(filter_data)
1012 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts, rpc_hostnames=True)
1013
1014 # Filter out hosts on a shard from those on the master, forward
1015 # rpcs to the shard with an additional hostname__in filter, and
1016 # create a local SpecialTask for each remaining host.
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001017 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001018 hosts = [h for h in hosts if h.shard is None]
1019 for shard, hostnames in shard_host_map.iteritems():
1020
1021 # The main client of this module is the frontend website, and
1022 # it invokes it with an 'id' or an 'id__in' filter. Regardless,
1023 # the 'hostname' filter should narrow down the list of hosts on
1024 # each shard even though we supply all the ids in filter_data.
1025 # This method uses hostname instead of id because it fits better
MK Ryu5aa25042015-07-28 16:08:04 -07001026 # with the overall architecture of redirection functions in
1027 # rpc_utils.
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001028 shard_filter = filter_data.copy()
1029 shard_filter['hostname__in'] = hostnames
1030 rpc_utils.run_rpc_on_multiple_hostnames(
MK Ryu5aa25042015-07-28 16:08:04 -07001031 rpc, [shard], **shard_filter)
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001032
1033 # There is a race condition here if someone assigns a shard to one of these
1034 # hosts before we create the task. The host will stay on the master if:
1035 # 1. The host is not Ready
1036 # 2. The host is Ready but has a task
1037 # But if the host is Ready and doesn't have a task yet, it will get sent
1038 # to the shard as we're creating a task here.
1039
1040 # Given that we only rarely verify Ready hosts it isn't worth putting this
1041 # entire method in a transaction. The worst case scenario is that we have
MK Ryu5aa25042015-07-28 16:08:04 -07001042 # a verify running on a Ready host while the shard is using it, if the
1043 # verify fails no subsequent tasks will be created against the host on the
1044 # master, and verifies are safe enough that this is OK.
1045 return _call_special_tasks_on_hosts(task, hosts)
1046
1047
1048def reverify_hosts(**filter_data):
1049 """\
1050 Schedules a set of hosts for verify.
1051
1052 @returns A list of hostnames that a verify task was created for.
1053 """
1054 return _forward_special_tasks_on_hosts(
1055 models.SpecialTask.Task.VERIFY, 'reverify_hosts', **filter_data)
Simran Basi73dae552013-02-25 14:57:46 -08001056
1057
1058def repair_hosts(**filter_data):
1059 """\
1060 Schedules a set of hosts for repair.
1061
1062 @returns A list of hostnames that a repair task was created for.
1063 """
MK Ryu5aa25042015-07-28 16:08:04 -07001064 return _forward_special_tasks_on_hosts(
1065 models.SpecialTask.Task.REPAIR, 'repair_hosts', **filter_data)
showard1ff7b2e2009-05-15 23:17:18 +00001066
1067
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001068def get_jobs(not_yet_run=False, running=False, finished=False,
1069 suite=False, sub=False, standalone=False, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001070 """\
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001071 Extra status filter args for get_jobs:
jadmanski0afbb632008-06-06 21:10:57 +00001072 -not_yet_run: Include only jobs that have not yet started running.
1073 -running: Include only jobs that have start running but for which not
1074 all hosts have completed.
1075 -finished: Include only jobs for which all hosts have completed (or
1076 aborted).
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001077
1078 Extra type filter args for get_jobs:
1079 -suite: Include only jobs with child jobs.
1080 -sub: Include only jobs with a parent job.
1081 -standalone: Inlcude only jobs with no child or parent jobs.
1082 At most one of these three fields should be specified.
jadmanski0afbb632008-06-06 21:10:57 +00001083 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001084 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1085 running,
1086 finished)
1087 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1088 suite,
1089 sub,
1090 standalone)
showard0957a842009-05-11 19:25:08 +00001091 job_dicts = []
1092 jobs = list(models.Job.query_objects(filter_data))
1093 models.Job.objects.populate_relationships(jobs, models.Label,
1094 'dependencies')
showardc1a98d12010-01-15 00:22:22 +00001095 models.Job.objects.populate_relationships(jobs, models.JobKeyval, 'keyvals')
showard0957a842009-05-11 19:25:08 +00001096 for job in jobs:
1097 job_dict = job.get_object_dict()
1098 job_dict['dependencies'] = ','.join(label.name
1099 for label in job.dependencies)
showardc1a98d12010-01-15 00:22:22 +00001100 job_dict['keyvals'] = dict((keyval.key, keyval.value)
1101 for keyval in job.keyvals)
showard0957a842009-05-11 19:25:08 +00001102 job_dicts.append(job_dict)
1103 return rpc_utils.prepare_for_serialization(job_dicts)
mblighe8819cd2008-02-15 16:48:40 +00001104
1105
1106def get_num_jobs(not_yet_run=False, running=False, finished=False,
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001107 suite=False, sub=False, standalone=False,
jadmanski0afbb632008-06-06 21:10:57 +00001108 **filter_data):
Aviv Keshet17660a52016-04-06 18:56:43 +00001109 """\
1110 See get_jobs() for documentation of extra filter parameters.
jadmanski0afbb632008-06-06 21:10:57 +00001111 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001112 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1113 running,
1114 finished)
1115 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1116 suite,
1117 sub,
1118 standalone)
Aviv Keshet17660a52016-04-06 18:56:43 +00001119 return models.Job.query_count(filter_data)
mblighe8819cd2008-02-15 16:48:40 +00001120
1121
mblighe8819cd2008-02-15 16:48:40 +00001122def get_jobs_summary(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001123 """\
Jiaxi Luoaac54572014-06-04 13:57:02 -07001124 Like get_jobs(), but adds 'status_counts' and 'result_counts' field.
1125
1126 'status_counts' filed is a dictionary mapping status strings to the number
1127 of hosts currently with that status, i.e. {'Queued' : 4, 'Running' : 2}.
1128
1129 'result_counts' field is piped to tko's rpc_interface and has the return
1130 format specified under get_group_counts.
jadmanski0afbb632008-06-06 21:10:57 +00001131 """
1132 jobs = get_jobs(**filter_data)
1133 ids = [job['id'] for job in jobs]
1134 all_status_counts = models.Job.objects.get_status_counts(ids)
1135 for job in jobs:
1136 job['status_counts'] = all_status_counts[job['id']]
Jiaxi Luoaac54572014-06-04 13:57:02 -07001137 job['result_counts'] = tko_rpc_interface.get_status_counts(
1138 ['afe_job_id', 'afe_job_id'],
1139 header_groups=[['afe_job_id'], ['afe_job_id']],
1140 **{'afe_job_id': job['id']})
jadmanski0afbb632008-06-06 21:10:57 +00001141 return rpc_utils.prepare_for_serialization(jobs)
mblighe8819cd2008-02-15 16:48:40 +00001142
1143
showarda965cef2009-05-15 23:17:41 +00001144def get_info_for_clone(id, preserve_metahosts, queue_entry_filter_data=None):
showarda8709c52008-07-03 19:44:54 +00001145 """\
1146 Retrieves all the information needed to clone a job.
1147 """
showarda8709c52008-07-03 19:44:54 +00001148 job = models.Job.objects.get(id=id)
showard29f7cd22009-04-29 21:16:24 +00001149 job_info = rpc_utils.get_job_info(job,
showarda965cef2009-05-15 23:17:41 +00001150 preserve_metahosts,
1151 queue_entry_filter_data)
showard945072f2008-09-03 20:34:59 +00001152
showardd9992fe2008-07-31 02:15:03 +00001153 host_dicts = []
showard29f7cd22009-04-29 21:16:24 +00001154 for host in job_info['hosts']:
1155 host_dict = get_hosts(id=host.id)[0]
1156 other_labels = host_dict['labels']
1157 if host_dict['platform']:
1158 other_labels.remove(host_dict['platform'])
1159 host_dict['other_labels'] = ', '.join(other_labels)
showardd9992fe2008-07-31 02:15:03 +00001160 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001161
showard29f7cd22009-04-29 21:16:24 +00001162 for host in job_info['one_time_hosts']:
1163 host_dict = dict(hostname=host.hostname,
1164 id=host.id,
1165 platform='(one-time host)',
1166 locked_text='')
1167 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001168
showard4d077562009-05-08 18:24:36 +00001169 # convert keys from Label objects to strings (names of labels)
showard29f7cd22009-04-29 21:16:24 +00001170 meta_host_counts = dict((meta_host.name, count) for meta_host, count
showard4d077562009-05-08 18:24:36 +00001171 in job_info['meta_host_counts'].iteritems())
showard29f7cd22009-04-29 21:16:24 +00001172
1173 info = dict(job=job.get_object_dict(),
1174 meta_host_counts=meta_host_counts,
1175 hosts=host_dicts)
1176 info['job']['dependencies'] = job_info['dependencies']
1177 if job_info['atomic_group']:
1178 info['atomic_group_name'] = (job_info['atomic_group']).name
1179 else:
1180 info['atomic_group_name'] = None
jamesren2275ef12010-04-12 18:25:06 +00001181 info['hostless'] = job_info['hostless']
jamesren76fcf192010-04-21 20:39:50 +00001182 info['drone_set'] = job.drone_set and job.drone_set.name
showarda8709c52008-07-03 19:44:54 +00001183
Michael Tang6dc174e2016-05-31 23:13:42 -07001184 image = _get_image_for_job(job, job_info['hostless'])
1185 if image:
1186 info['job']['image'] = image
Eric Lid23bc192011-02-09 14:38:57 -08001187
showarda8709c52008-07-03 19:44:54 +00001188 return rpc_utils.prepare_for_serialization(info)
1189
1190
Michael Tang6dc174e2016-05-31 23:13:42 -07001191def _get_image_for_job(job, hostless):
Richard Barnette0ed60752017-02-02 13:23:30 -08001192 """Gets the image used for a job.
Michael Tang6dc174e2016-05-31 23:13:42 -07001193
Richard Barnette0ed60752017-02-02 13:23:30 -08001194 Gets the image used for an AFE job from the job's keyvals 'build' or
1195 'builds'. If that fails, and the job is a hostless job, tries to
1196 get the image from its control file attributes 'build' or 'builds'.
Michael Tang6dc174e2016-05-31 23:13:42 -07001197
1198 TODO(ntang): Needs to handle FAFT with two builds for ro/rw.
1199
1200 @param job An AFE job object.
Richard Barnette0ed60752017-02-02 13:23:30 -08001201 @param hostless Boolean indicating whether the job is hostless.
Michael Tang6dc174e2016-05-31 23:13:42 -07001202
1203 @returns The image build used for the job.
1204 """
Richard Barnette0ed60752017-02-02 13:23:30 -08001205 keyvals = job.keyval_dict()
1206 image = keyvals.get('build')
1207 if not image:
1208 value = keyvals.get('builds')
1209 builds = None
1210 if isinstance(value, dict):
1211 builds = value
1212 elif isinstance(value, basestring):
1213 builds = ast.literal_eval(value)
1214 if builds:
1215 image = builds.get('cros-version')
1216 if not image and hostless and job.control_file:
1217 try:
1218 control_obj = control_data.parse_control_string(
1219 job.control_file)
1220 if hasattr(control_obj, 'build'):
1221 image = getattr(control_obj, 'build')
1222 if not image and hasattr(control_obj, 'builds'):
1223 builds = getattr(control_obj, 'builds')
Michael Tang6dc174e2016-05-31 23:13:42 -07001224 image = builds.get('cros-version')
Richard Barnette0ed60752017-02-02 13:23:30 -08001225 except:
1226 logging.warning('Failed to parse control file for job: %s',
1227 job.name)
Michael Tang6dc174e2016-05-31 23:13:42 -07001228 return image
1229
showard34dc5fa2008-04-24 20:58:40 +00001230
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001231def get_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001232 """\
showardc92da832009-04-07 18:14:34 +00001233 @returns A sequence of nested dictionaries of host and job information.
jadmanski0afbb632008-06-06 21:10:57 +00001234 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001235 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1236 'started_on__lte',
1237 start_time,
1238 end_time,
1239 **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001240 return rpc_utils.prepare_rows_as_nested_dicts(
1241 models.HostQueueEntry.query_objects(filter_data),
1242 ('host', 'atomic_group', 'job'))
showard34dc5fa2008-04-24 20:58:40 +00001243
1244
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001245def get_num_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001246 """\
1247 Get the number of host queue entries associated with this job.
1248 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001249 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1250 'started_on__lte',
1251 start_time,
1252 end_time,
1253 **filter_data)
jadmanski0afbb632008-06-06 21:10:57 +00001254 return models.HostQueueEntry.query_count(filter_data)
showard34dc5fa2008-04-24 20:58:40 +00001255
1256
showard1e935f12008-07-11 00:11:36 +00001257def get_hqe_percentage_complete(**filter_data):
1258 """
showardc92da832009-04-07 18:14:34 +00001259 Computes the fraction of host queue entries matching the given filter data
showard1e935f12008-07-11 00:11:36 +00001260 that are complete.
1261 """
1262 query = models.HostQueueEntry.query_objects(filter_data)
1263 complete_count = query.filter(complete=True).count()
1264 total_count = query.count()
1265 if total_count == 0:
1266 return 1
1267 return float(complete_count) / total_count
1268
1269
showard1a5a4082009-07-28 20:01:37 +00001270# special tasks
1271
1272def get_special_tasks(**filter_data):
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001273 """Get special task entries from the local database.
1274
1275 Query the special tasks table for tasks matching the given
1276 `filter_data`, and return a list of the results. No attempt is
1277 made to forward the call to shards; the buck will stop here.
1278 The caller is expected to know the target shard for such reasons
1279 as:
1280 * The caller is a service (such as gs_offloader) configured
1281 to operate on behalf of one specific shard, and no other.
1282 * The caller has a host as a parameter, and knows that this is
1283 the shard assigned to that host.
1284
1285 @param filter_data Filter keywords to pass to the underlying
1286 database query.
1287
1288 """
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001289 return rpc_utils.prepare_rows_as_nested_dicts(
1290 models.SpecialTask.query_objects(filter_data),
1291 ('host', 'queue_entry'))
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001292
1293
1294def get_host_special_tasks(host_id, **filter_data):
1295 """Get special task entries for a given host.
1296
1297 Query the special tasks table for tasks that ran on the host
1298 given by `host_id` and matching the given `filter_data`.
1299 Return a list of the results. If the host is assigned to a
1300 shard, forward this call to that shard.
1301
1302 @param host_id Id in the database of the target host.
1303 @param filter_data Filter keywords to pass to the underlying
1304 database query.
1305
1306 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001307 # Retrieve host data even if the host is in an invalid state.
1308 host = models.Host.smart_get(host_id, False)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001309 if not host.shard:
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001310 return get_special_tasks(host_id=host_id, **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001311 else:
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001312 # The return values from AFE methods are post-processed
1313 # objects that aren't JSON-serializable. So, we have to
1314 # call AFE.run() to get the raw, serializable output from
1315 # the shard.
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001316 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1317 return shard_afe.run('get_special_tasks',
1318 host_id=host_id, **filter_data)
showard1a5a4082009-07-28 20:01:37 +00001319
1320
MK Ryu0c1a37d2015-04-30 12:00:55 -07001321def get_num_special_tasks(**kwargs):
1322 """Get the number of special task entries from the local database.
1323
1324 Query the special tasks table for tasks matching the given 'kwargs',
1325 and return the number of the results. No attempt is made to forward
1326 the call to shards; the buck will stop here.
1327
1328 @param kwargs Filter keywords to pass to the underlying database query.
1329
1330 """
1331 return models.SpecialTask.query_count(kwargs)
1332
1333
1334def get_host_num_special_tasks(host, **kwargs):
1335 """Get special task entries for a given host.
1336
1337 Query the special tasks table for tasks that ran on the host
1338 given by 'host' and matching the given 'kwargs'.
1339 Return a list of the results. If the host is assigned to a
1340 shard, forward this call to that shard.
1341
1342 @param host id or name of a host. More often a hostname.
1343 @param kwargs Filter keywords to pass to the underlying database query.
1344
1345 """
1346 # Retrieve host data even if the host is in an invalid state.
1347 host_model = models.Host.smart_get(host, False)
1348 if not host_model.shard:
1349 return get_num_special_tasks(host=host, **kwargs)
1350 else:
1351 shard_afe = frontend.AFE(server=host_model.shard.rpc_hostname())
1352 return shard_afe.run('get_num_special_tasks', host=host, **kwargs)
1353
1354
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001355def get_status_task(host_id, end_time):
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001356 """Get the "status task" for a host from the local shard.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001357
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001358 Returns a single special task representing the given host's
1359 "status task". The status task is a completed special task that
1360 identifies whether the corresponding host was working or broken
1361 when it completed. A successful task indicates a working host;
1362 a failed task indicates broken.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001363
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001364 This call will not be forward to a shard; the receiving server
1365 must be the shard that owns the host.
1366
1367 @param host_id Id in the database of the target host.
1368 @param end_time Time reference for the host's status.
1369
1370 @return A single task; its status (successful or not)
1371 corresponds to the status of the host (working or
1372 broken) at the given time. If no task is found, return
1373 `None`.
1374
1375 """
1376 tasklist = rpc_utils.prepare_rows_as_nested_dicts(
1377 status_history.get_status_task(host_id, end_time),
1378 ('host', 'queue_entry'))
1379 return tasklist[0] if tasklist else None
1380
1381
1382def get_host_status_task(host_id, end_time):
1383 """Get the "status task" for a host from its owning shard.
1384
1385 Finds the given host's owning shard, and forwards to it a call
1386 to `get_status_task()` (see above).
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001387
1388 @param host_id Id in the database of the target host.
1389 @param end_time Time reference for the host's status.
1390
1391 @return A single task; its status (successful or not)
1392 corresponds to the status of the host (working or
1393 broken) at the given time. If no task is found, return
1394 `None`.
1395
1396 """
1397 host = models.Host.smart_get(host_id)
1398 if not host.shard:
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001399 return get_status_task(host_id, end_time)
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001400 else:
1401 # The return values from AFE methods are post-processed
1402 # objects that aren't JSON-serializable. So, we have to
1403 # call AFE.run() to get the raw, serializable output from
1404 # the shard.
1405 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1406 return shard_afe.run('get_status_task',
1407 host_id=host_id, end_time=end_time)
1408
1409
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001410def get_host_diagnosis_interval(host_id, end_time, success):
1411 """Find a "diagnosis interval" for a given host.
1412
1413 A "diagnosis interval" identifies a start and end time where
1414 the host went from "working" to "broken", or vice versa. The
1415 interval's starting time is the starting time of the last status
1416 task with the old status; the end time is the finish time of the
1417 first status task with the new status.
1418
1419 This routine finds the most recent diagnosis interval for the
1420 given host prior to `end_time`, with a starting status matching
1421 `success`. If `success` is true, the interval will start with a
1422 successful status task; if false the interval will start with a
1423 failed status task.
1424
1425 @param host_id Id in the database of the target host.
1426 @param end_time Time reference for the diagnosis interval.
1427 @param success Whether the diagnosis interval should start
1428 with a successful or failed status task.
1429
1430 @return A list of two strings. The first is the timestamp for
1431 the beginning of the interval; the second is the
1432 timestamp for the end. If the host has never changed
1433 state, the list is empty.
1434
1435 """
1436 host = models.Host.smart_get(host_id)
J. Richard Barnette78f281a2015-06-29 13:24:51 -07001437 if not host.shard or utils.is_shard():
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001438 return status_history.get_diagnosis_interval(
1439 host_id, end_time, success)
1440 else:
1441 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1442 return shard_afe.get_host_diagnosis_interval(
1443 host_id, end_time, success)
1444
1445
showardc0ac3a72009-07-08 21:14:45 +00001446# support for host detail view
1447
MK Ryu0c1a37d2015-04-30 12:00:55 -07001448def get_host_queue_entries_and_special_tasks(host, query_start=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001449 query_limit=None, start_time=None,
1450 end_time=None):
showardc0ac3a72009-07-08 21:14:45 +00001451 """
1452 @returns an interleaved list of HostQueueEntries and SpecialTasks,
1453 in approximate run order. each dict contains keys for type, host,
1454 job, status, started_on, execution_path, and ID.
1455 """
1456 total_limit = None
1457 if query_limit is not None:
1458 total_limit = query_start + query_limit
MK Ryu0c1a37d2015-04-30 12:00:55 -07001459 filter_data_common = {'host': host,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001460 'query_limit': total_limit,
1461 'sort_by': ['-id']}
showardc0ac3a72009-07-08 21:14:45 +00001462
MK Ryu0c1a37d2015-04-30 12:00:55 -07001463 filter_data_special_tasks = rpc_utils.inject_times_to_filter(
1464 'time_started__gte', 'time_started__lte', start_time, end_time,
1465 **filter_data_common)
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001466
MK Ryu0c1a37d2015-04-30 12:00:55 -07001467 queue_entries = get_host_queue_entries(
1468 start_time, end_time, **filter_data_common)
1469 special_tasks = get_host_special_tasks(host, **filter_data_special_tasks)
showardc0ac3a72009-07-08 21:14:45 +00001470
1471 interleaved_entries = rpc_utils.interleave_entries(queue_entries,
1472 special_tasks)
1473 if query_start is not None:
1474 interleaved_entries = interleaved_entries[query_start:]
1475 if query_limit is not None:
1476 interleaved_entries = interleaved_entries[:query_limit]
MK Ryu0c1a37d2015-04-30 12:00:55 -07001477 return rpc_utils.prepare_host_queue_entries_and_special_tasks(
1478 interleaved_entries, queue_entries)
showardc0ac3a72009-07-08 21:14:45 +00001479
1480
MK Ryu0c1a37d2015-04-30 12:00:55 -07001481def get_num_host_queue_entries_and_special_tasks(host, start_time=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001482 end_time=None):
MK Ryu0c1a37d2015-04-30 12:00:55 -07001483 filter_data_common = {'host': host}
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001484
1485 filter_data_queue_entries, filter_data_special_tasks = (
1486 rpc_utils.inject_times_to_hqe_special_tasks_filters(
1487 filter_data_common, start_time, end_time))
1488
1489 return (models.HostQueueEntry.query_count(filter_data_queue_entries)
MK Ryu0c1a37d2015-04-30 12:00:55 -07001490 + get_host_num_special_tasks(**filter_data_special_tasks))
showardc0ac3a72009-07-08 21:14:45 +00001491
1492
mblighe8819cd2008-02-15 16:48:40 +00001493# other
1494
showarde0b63622008-08-04 20:58:47 +00001495def echo(data=""):
1496 """\
1497 Returns a passed in string. For doing a basic test to see if RPC calls
1498 can successfully be made.
1499 """
1500 return data
1501
1502
showardb7a52fd2009-04-27 20:10:56 +00001503def get_motd():
1504 """\
1505 Returns the message of the day as a string.
1506 """
1507 return rpc_utils.get_motd()
1508
1509
mblighe8819cd2008-02-15 16:48:40 +00001510def get_static_data():
jadmanski0afbb632008-06-06 21:10:57 +00001511 """\
1512 Returns a dictionary containing a bunch of data that shouldn't change
1513 often and is otherwise inaccessible. This includes:
showardc92da832009-04-07 18:14:34 +00001514
1515 priorities: List of job priority choices.
1516 default_priority: Default priority value for new jobs.
1517 users: Sorted list of all users.
Jiaxi Luo31874592014-06-11 10:36:35 -07001518 labels: Sorted list of labels not start with 'cros-version' and
1519 'fw-version'.
showardc92da832009-04-07 18:14:34 +00001520 atomic_groups: Sorted list of all atomic groups.
1521 tests: Sorted list of all tests.
1522 profilers: Sorted list of all profilers.
1523 current_user: Logged-in username.
1524 host_statuses: Sorted list of possible Host statuses.
1525 job_statuses: Sorted list of possible HostQueueEntry statuses.
Simran Basi7e605742013-11-12 13:43:36 -08001526 job_timeout_default: The default job timeout length in minutes.
showarda1e74b32009-05-12 17:32:04 +00001527 parse_failed_repair_default: Default value for the parse_failed_repair job
Jiaxi Luo31874592014-06-11 10:36:35 -07001528 option.
showardc92da832009-04-07 18:14:34 +00001529 reboot_before_options: A list of valid RebootBefore string enums.
1530 reboot_after_options: A list of valid RebootAfter string enums.
1531 motd: Server's message of the day.
1532 status_dictionary: A mapping from one word job status names to a more
1533 informative description.
jadmanski0afbb632008-06-06 21:10:57 +00001534 """
showard21baa452008-10-21 00:08:39 +00001535
jamesren76fcf192010-04-21 20:39:50 +00001536 default_drone_set_name = models.DroneSet.default_drone_set_name()
1537 drone_sets = ([default_drone_set_name] +
1538 sorted(drone_set.name for drone_set in
1539 models.DroneSet.objects.exclude(
1540 name=default_drone_set_name)))
showard21baa452008-10-21 00:08:39 +00001541
jadmanski0afbb632008-06-06 21:10:57 +00001542 result = {}
Alex Miller7d658cf2013-09-04 16:00:35 -07001543 result['priorities'] = priorities.Priority.choices()
Alex Miller7d658cf2013-09-04 16:00:35 -07001544 result['default_priority'] = 'Default'
1545 result['max_schedulable_priority'] = priorities.Priority.DEFAULT
jadmanski0afbb632008-06-06 21:10:57 +00001546 result['users'] = get_users(sort_by=['login'])
Jiaxi Luo31874592014-06-11 10:36:35 -07001547
1548 label_exclude_filters = [{'name__startswith': 'cros-version'},
Dan Shi65351d62015-08-03 12:03:23 -07001549 {'name__startswith': 'fw-version'},
1550 {'name__startswith': 'fwrw-version'},
Dan Shi27516972016-03-16 14:03:41 -07001551 {'name__startswith': 'fwro-version'},
1552 {'name__startswith': 'ab-version'},
1553 {'name__startswith': 'testbed-version'}]
Jiaxi Luo31874592014-06-11 10:36:35 -07001554 result['labels'] = get_labels(
1555 label_exclude_filters,
1556 sort_by=['-platform', 'name'])
1557
showardc92da832009-04-07 18:14:34 +00001558 result['atomic_groups'] = get_atomic_groups(sort_by=['name'])
jadmanski0afbb632008-06-06 21:10:57 +00001559 result['tests'] = get_tests(sort_by=['name'])
showard2b9a88b2008-06-13 20:55:03 +00001560 result['profilers'] = get_profilers(sort_by=['name'])
showard0fc38302008-10-23 00:44:07 +00001561 result['current_user'] = rpc_utils.prepare_for_serialization(
showard64a95952010-01-13 21:27:16 +00001562 models.User.current_user().get_object_dict())
showard2b9a88b2008-06-13 20:55:03 +00001563 result['host_statuses'] = sorted(models.Host.Status.names)
mbligh5a198b92008-12-11 19:33:29 +00001564 result['job_statuses'] = sorted(models.HostQueueEntry.Status.names)
Simran Basi7e605742013-11-12 13:43:36 -08001565 result['job_timeout_mins_default'] = models.Job.DEFAULT_TIMEOUT_MINS
Simran Basi34217022012-11-06 13:43:15 -08001566 result['job_max_runtime_mins_default'] = (
1567 models.Job.DEFAULT_MAX_RUNTIME_MINS)
showarda1e74b32009-05-12 17:32:04 +00001568 result['parse_failed_repair_default'] = bool(
1569 models.Job.DEFAULT_PARSE_FAILED_REPAIR)
jamesrendd855242010-03-02 22:23:44 +00001570 result['reboot_before_options'] = model_attributes.RebootBefore.names
1571 result['reboot_after_options'] = model_attributes.RebootAfter.names
showard8fbae652009-01-20 23:23:10 +00001572 result['motd'] = rpc_utils.get_motd()
jamesren76fcf192010-04-21 20:39:50 +00001573 result['drone_sets_enabled'] = models.DroneSet.drone_sets_enabled()
1574 result['drone_sets'] = drone_sets
showard8ac29b42008-07-17 17:01:55 +00001575
showardd3dc1992009-04-22 21:01:40 +00001576 result['status_dictionary'] = {"Aborted": "Aborted",
showard8ac29b42008-07-17 17:01:55 +00001577 "Verifying": "Verifying Host",
Alex Millerdfff2fd2013-05-28 13:05:06 -07001578 "Provisioning": "Provisioning Host",
showard8ac29b42008-07-17 17:01:55 +00001579 "Pending": "Waiting on other hosts",
1580 "Running": "Running autoserv",
1581 "Completed": "Autoserv completed",
1582 "Failed": "Failed to complete",
showardd823b362008-07-24 16:35:46 +00001583 "Queued": "Queued",
showard5deb6772008-11-04 21:54:33 +00001584 "Starting": "Next in host's queue",
1585 "Stopped": "Other host(s) failed verify",
showardd3dc1992009-04-22 21:01:40 +00001586 "Parsing": "Awaiting parse of final results",
showard29f7cd22009-04-29 21:16:24 +00001587 "Gathering": "Gathering log files",
mbligh4608b002010-01-05 18:22:35 +00001588 "Waiting": "Waiting for scheduler action",
Dan Shi07e09af2013-04-12 09:31:29 -07001589 "Archiving": "Archiving results",
1590 "Resetting": "Resetting hosts"}
Jiaxi Luo421608e2014-07-07 14:38:00 -07001591
1592 result['wmatrix_url'] = rpc_utils.get_wmatrix_url()
Simran Basi71206ef2014-08-13 13:51:18 -07001593 result['is_moblab'] = bool(utils.is_moblab())
Jiaxi Luo421608e2014-07-07 14:38:00 -07001594
jadmanski0afbb632008-06-06 21:10:57 +00001595 return result
showard29f7cd22009-04-29 21:16:24 +00001596
1597
1598def get_server_time():
1599 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
Kevin Cheng19521982016-09-22 12:27:23 -07001600
1601
1602def get_hosts_by_attribute(attribute, value):
1603 """
1604 Get the list of valid hosts that share the same host attribute value.
1605
1606 @param attribute: String of the host attribute to check.
1607 @param value: String of the value that is shared between hosts.
1608
1609 @returns List of hostnames that all have the same host attribute and
1610 value.
1611 """
1612 hosts = models.HostAttribute.query_objects({'attribute': attribute,
1613 'value': value})
1614 return [row.host.hostname for row in hosts if row.host.invalid == 0]
Allen Licdd00f22017-02-01 18:01:52 -08001615
1616
1617def canonicalize_suite_name(suite_name):
1618 """Canonicalize the suite's name.
1619
1620 @param suite_name: the name of the suite.
1621 """
1622 # Do not change this naming convention without updating
1623 # site_utils.parse_job_name.
1624 return 'test_suites/control.%s' % suite_name
1625
1626
1627def formatted_now():
1628 """Format the current datetime."""
1629 return datetime.datetime.now().strftime(time_utils.TIME_FMT)
1630
1631
1632def _get_control_file_by_build(build, ds, suite_name):
1633 """Return control file contents for |suite_name|.
1634
1635 Query the dev server at |ds| for the control file |suite_name|, included
1636 in |build| for |board|.
1637
1638 @param build: unique name by which to refer to the image from now on.
1639 @param ds: a dev_server.DevServer instance to fetch control file with.
1640 @param suite_name: canonicalized suite name, e.g. test_suites/control.bvt.
1641 @raises ControlFileNotFound if a unique suite control file doesn't exist.
1642 @raises NoControlFileList if we can't list the control files at all.
1643 @raises ControlFileEmpty if the control file exists on the server, but
1644 can't be read.
1645
1646 @return the contents of the desired control file.
1647 """
1648 getter = control_file_getter.DevServerGetter.create(build, ds)
1649 devserver_name = ds.hostname
1650 timer = autotest_stats.Timer('control_files.parse.%s.%s' %
1651 (devserver_name.replace('.', '_'),
1652 suite_name.rsplit('.')[-1]))
1653 # Get the control file for the suite.
1654 try:
1655 with timer:
1656 control_file_in = getter.get_control_file_contents_by_name(
1657 suite_name)
1658 except error.CrosDynamicSuiteException as e:
1659 raise type(e)('Failed to get control file for %s '
1660 '(devserver: %s) (error: %s)' %
1661 (build, devserver_name, e))
1662 if not control_file_in:
1663 raise error.ControlFileEmpty(
1664 "Fetching %s returned no data. (devserver: %s)" %
1665 (suite_name, devserver_name))
1666 # Force control files to only contain ascii characters.
1667 try:
1668 control_file_in.encode('ascii')
1669 except UnicodeDecodeError as e:
1670 raise error.ControlFileMalformed(str(e))
1671
1672 return control_file_in
1673
1674
1675def _get_control_file_by_suite(suite_name):
1676 """Get control file contents by suite name.
1677
1678 @param suite_name: Suite name as string.
1679 @returns: Control file contents as string.
1680 """
1681 getter = control_file_getter.FileSystemGetter(
Dan Shi6cd838f2017-02-02 15:30:18 -08001682 [_CONFIG.get_config_value('SCHEDULER',
1683 'drone_installation_directory')])
Allen Licdd00f22017-02-01 18:01:52 -08001684 return getter.get_control_file_contents_by_name(suite_name)
1685
1686
1687def _stage_build_artifacts(build, hostname=None):
1688 """
1689 Ensure components of |build| necessary for installing images are staged.
1690
1691 @param build image we want to stage.
1692 @param hostname hostname of a dut may run test on. This is to help to locate
1693 a devserver closer to duts if needed. Default is None.
1694
1695 @raises StageControlFileFailure: if the dev server throws 500 while staging
1696 suite control files.
1697
1698 @return: dev_server.ImageServer instance to use with this build.
1699 @return: timings dictionary containing staging start/end times.
1700 """
1701 timings = {}
1702 # Ensure components of |build| necessary for installing images are staged
1703 # on the dev server. However set synchronous to False to allow other
1704 # components to be downloaded in the background.
1705 ds = dev_server.resolve(build, hostname=hostname)
1706 ds_name = ds.hostname
1707 timings[constants.DOWNLOAD_STARTED_TIME] = formatted_now()
1708 timer = autotest_stats.Timer('control_files.stage.%s' % (
1709 ds_name.replace('.', '_')))
1710 try:
1711 with timer:
1712 ds.stage_artifacts(image=build, artifacts=['test_suites'])
1713 except dev_server.DevServerException as e:
1714 raise error.StageControlFileFailure(
1715 "Failed to stage %s on %s: %s" % (build, ds_name, e))
1716 timings[constants.PAYLOAD_FINISHED_TIME] = formatted_now()
1717 return (ds, timings)
1718
1719
1720@rpc_utils.route_rpc_to_master
1721def create_suite_job(
1722 name='',
1723 board='',
1724 pool='',
1725 control_file='',
1726 check_hosts=True,
1727 num=None,
1728 file_bugs=False,
1729 timeout=24,
1730 timeout_mins=None,
1731 priority=priorities.Priority.DEFAULT,
1732 suite_args=None,
1733 wait_for_results=True,
1734 job_retry=False,
1735 max_retries=None,
1736 max_runtime_mins=None,
1737 suite_min_duts=0,
1738 offload_failures_only=False,
1739 builds=None,
1740 test_source_build=None,
1741 run_prod_code=False,
1742 delay_minutes=0,
1743 is_cloning=False,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001744 job_keyvals=None,
Allen Licdd00f22017-02-01 18:01:52 -08001745 **kwargs
1746):
1747 """
1748 Create a job to run a test suite on the given device with the given image.
1749
1750 When the timeout specified in the control file is reached, the
1751 job is guaranteed to have completed and results will be available.
1752
1753 @param name: The test name if control_file is supplied, otherwise the name
1754 of the test suite to run, e.g. 'bvt'.
1755 @param board: the kind of device to run the tests on.
1756 @param builds: the builds to install e.g.
1757 {'cros-version:': 'x86-alex-release/R18-1655.0.0',
1758 'fwrw-version:': 'x86-alex-firmware/R36-5771.50.0',
1759 'fwro-version:': 'x86-alex-firmware/R36-5771.49.0'}
1760 If builds is given a value, it overrides argument build.
1761 @param test_source_build: Build that contains the server-side test code.
1762 @param pool: Specify the pool of machines to use for scheduling
1763 purposes.
1764 @param control_file: the control file of the job.
1765 @param check_hosts: require appropriate live hosts to exist in the lab.
1766 @param num: Specify the number of machines to schedule across (integer).
1767 Leave unspecified or use None to use default sharding factor.
1768 @param file_bugs: File a bug on each test failure in this suite.
1769 @param timeout: The max lifetime of this suite, in hours.
1770 @param timeout_mins: The max lifetime of this suite, in minutes. Takes
1771 priority over timeout.
1772 @param priority: Integer denoting priority. Higher is more important.
1773 @param suite_args: Optional arguments which will be parsed by the suite
1774 control file. Used by control.test_that_wrapper to
1775 determine which tests to run.
1776 @param wait_for_results: Set to False to run the suite job without waiting
1777 for test jobs to finish. Default is True.
1778 @param job_retry: Set to True to enable job-level retry. Default is False.
1779 @param max_retries: Integer, maximum job retries allowed at suite level.
1780 None for no max.
1781 @param max_runtime_mins: Maximum amount of time a job can be running in
1782 minutes.
1783 @param suite_min_duts: Integer. Scheduler will prioritize getting the
1784 minimum number of machines for the suite when it is
1785 competing with another suite that has a higher
1786 priority but already got minimum machines it needs.
1787 @param offload_failures_only: Only enable gs_offloading for failed jobs.
1788 @param run_prod_code: If True, the suite will run the test code that
1789 lives in prod aka the test code currently on the
1790 lab servers. If False, the control files and test
1791 code for this suite run will be retrieved from the
1792 build artifacts.
1793 @param delay_minutes: Delay the creation of test jobs for a given number of
1794 minutes.
1795 @param is_cloning: True if creating a cloning job.
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001796 @param job_keyvals: A dict of job keyvals to be inject to control file.
Allen Licdd00f22017-02-01 18:01:52 -08001797 @param kwargs: extra keyword args. NOT USED.
1798
1799 @raises ControlFileNotFound: if a unique suite control file doesn't exist.
1800 @raises NoControlFileList: if we can't list the control files at all.
1801 @raises StageControlFileFailure: If the dev server throws 500 while
1802 staging test_suites.
1803 @raises ControlFileEmpty: if the control file exists on the server, but
1804 can't be read.
1805
1806 @return: the job ID of the suite; -1 on error.
1807 """
1808 if type(num) is not int and num is not None:
1809 raise error.SuiteArgumentException('Ill specified num argument %r. '
1810 'Must be an integer or None.' % num)
1811 if num == 0:
1812 logging.warning("Can't run on 0 hosts; using default.")
1813 num = None
1814
1815 if builds is None:
1816 builds = {}
1817
1818 # Default test source build to CrOS build if it's not specified and
1819 # run_prod_code is set to False.
1820 if not run_prod_code:
1821 test_source_build = Suite.get_test_source_build(
1822 builds, test_source_build=test_source_build)
1823
1824 sample_dut = rpc_utils.get_sample_dut(board, pool)
1825
1826 suite_name = canonicalize_suite_name(name)
1827 if run_prod_code:
1828 ds = dev_server.resolve(test_source_build, hostname=sample_dut)
1829 keyvals = {}
1830 else:
1831 (ds, keyvals) = _stage_build_artifacts(
1832 test_source_build, hostname=sample_dut)
1833 keyvals[constants.SUITE_MIN_DUTS_KEY] = suite_min_duts
1834
1835 # Do not change this naming convention without updating
1836 # site_utils.parse_job_name.
1837 if run_prod_code:
1838 # If run_prod_code is True, test_source_build is not set, use the
1839 # first build in the builds list for the sutie job name.
1840 name = '%s-%s' % (builds.values()[0], suite_name)
1841 else:
1842 name = '%s-%s' % (test_source_build, suite_name)
1843
1844 timeout_mins = timeout_mins or timeout * 60
1845 max_runtime_mins = max_runtime_mins or timeout * 60
1846
1847 if not board:
1848 board = utils.ParseBuildName(builds[provision.CROS_VERSION_PREFIX])[0]
1849
1850 if run_prod_code:
1851 control_file = _get_control_file_by_suite(suite_name)
1852
1853 if not control_file:
1854 # No control file was supplied so look it up from the build artifacts.
1855 control_file = _get_control_file_by_build(
1856 test_source_build, ds, suite_name)
1857
1858 # Prepend builds and board to the control file.
1859 if is_cloning:
1860 control_file = tools.remove_injection(control_file)
1861
1862 inject_dict = {
1863 'board': board,
1864 # `build` is needed for suites like AU to stage image inside suite
1865 # control file.
1866 'build': test_source_build,
1867 'builds': builds,
1868 'check_hosts': check_hosts,
1869 'pool': pool,
1870 'num': num,
1871 'file_bugs': file_bugs,
1872 'timeout': timeout,
1873 'timeout_mins': timeout_mins,
1874 'devserver_url': ds.url(),
1875 'priority': priority,
1876 'suite_args' : suite_args,
1877 'wait_for_results': wait_for_results,
1878 'job_retry': job_retry,
1879 'max_retries': max_retries,
1880 'max_runtime_mins': max_runtime_mins,
1881 'offload_failures_only': offload_failures_only,
1882 'test_source_build': test_source_build,
1883 'run_prod_code': run_prod_code,
1884 'delay_minutes': delay_minutes,
Shuqian Zhaoda1118d2017-02-13 16:22:58 -08001885 'job_keyvals': job_keyvals
Allen Licdd00f22017-02-01 18:01:52 -08001886 }
1887 control_file = tools.inject_vars(inject_dict, control_file)
1888
1889 return rpc_utils.create_job_common(name,
1890 priority=priority,
1891 timeout_mins=timeout_mins,
1892 max_runtime_mins=max_runtime_mins,
1893 control_type='Server',
1894 control_file=control_file,
1895 hostless=True,
1896 keyvals=keyvals)
1897
1898
1899def get_job_history(**filter_data):
1900 """Get history of the job, including the special tasks executed for the job
1901
1902 @param filter_data: filter for the call, should at least include
1903 {'job_id': [job id]}
1904 @returns: JSON string of the job's history, including the information such
1905 as the hosts run the job and the special tasks executed before
1906 and after the job.
1907 """
1908 job_id = filter_data['job_id']
1909 job_info = job_history.get_job_info(job_id)
1910 return rpc_utils.prepare_for_serialization(job_info.get_history())
1911
1912
1913def get_host_history(start_time, end_time, hosts=None, board=None, pool=None):
1914 """Get history of a list of host.
1915
1916 The return is a JSON string of host history for each host, for example,
1917 {'172.22.33.51': [{'status': 'Resetting'
1918 'start_time': '2014-08-07 10:02:16',
1919 'end_time': '2014-08-07 10:03:16',
1920 'log_url': 'http://autotest/reset-546546/debug',
1921 'dbg_str': 'Task: Special Task 19441991 (host ...)'},
1922 {'status': 'Running'
1923 'start_time': '2014-08-07 10:03:18',
1924 'end_time': '2014-08-07 10:13:00',
1925 'log_url': 'http://autotest/reset-546546/debug',
1926 'dbg_str': 'HQE: 15305005, for job: 14995562'}
1927 ]
1928 }
1929 @param start_time: start time to search for history, can be string value or
1930 epoch time.
1931 @param end_time: end time to search for history, can be string value or
1932 epoch time.
1933 @param hosts: A list of hosts to search for history. Default is None.
1934 @param board: board type of hosts. Default is None.
1935 @param pool: pool type of hosts. Default is None.
1936 @returns: JSON string of the host history.
1937 """
1938 return rpc_utils.prepare_for_serialization(
1939 host_history.get_history_details(
1940 start_time=start_time, end_time=end_time,
1941 hosts=hosts, board=board, pool=pool,
1942 process_pool_size=4))
1943
1944
1945def shard_heartbeat(shard_hostname, jobs=(), hqes=(), known_job_ids=(),
1946 known_host_ids=(), known_host_statuses=()):
1947 """Receive updates for job statuses from shards and assign hosts and jobs.
1948
1949 @param shard_hostname: Hostname of the calling shard
1950 @param jobs: Jobs in serialized form that should be updated with newer
1951 status from a shard.
1952 @param hqes: Hostqueueentries in serialized form that should be updated with
1953 newer status from a shard. Note that for every hostqueueentry
1954 the corresponding job must be in jobs.
1955 @param known_job_ids: List of ids of jobs the shard already has.
1956 @param known_host_ids: List of ids of hosts the shard already has.
1957 @param known_host_statuses: List of statuses of hosts the shard already has.
1958
1959 @returns: Serialized representations of hosts, jobs, suite job keyvals
1960 and their dependencies to be inserted into a shard's database.
1961 """
1962 # The following alternatives to sending host and job ids in every heartbeat
1963 # have been considered:
1964 # 1. Sending the highest known job and host ids. This would work for jobs:
1965 # Newer jobs always have larger ids. Also, if a job is not assigned to a
1966 # particular shard during a heartbeat, it never will be assigned to this
1967 # shard later.
1968 # This is not true for hosts though: A host that is leased won't be sent
1969 # to the shard now, but might be sent in a future heartbeat. This means
1970 # sometimes hosts should be transfered that have a lower id than the
1971 # maximum host id the shard knows.
1972 # 2. Send the number of jobs/hosts the shard knows to the master in each
1973 # heartbeat. Compare these to the number of records that already have
1974 # the shard_id set to this shard. In the normal case, they should match.
1975 # In case they don't, resend all entities of that type.
1976 # This would work well for hosts, because there aren't that many.
1977 # Resending all jobs is quite a big overhead though.
1978 # Also, this approach might run into edge cases when entities are
1979 # ever deleted.
1980 # 3. Mixtures of the above: Use 1 for jobs and 2 for hosts.
1981 # Using two different approaches isn't consistent and might cause
1982 # confusion. Also the issues with the case of deletions might still
1983 # occur.
1984 #
1985 # The overhead of sending all job and host ids in every heartbeat is low:
1986 # At peaks one board has about 1200 created but unfinished jobs.
1987 # See the numbers here: http://goo.gl/gQCGWH
1988 # Assuming that job id's have 6 digits and that json serialization takes a
1989 # comma and a space as overhead, the traffic per id sent is about 8 bytes.
1990 # If 5000 ids need to be sent, this means 40 kilobytes of traffic.
1991 # A NOT IN query with 5000 ids took about 30ms in tests made.
1992 # These numbers seem low enough to outweigh the disadvantages of the
1993 # solutions described above.
1994 timer = autotest_stats.Timer('shard_heartbeat')
1995 with timer:
1996 shard_obj = rpc_utils.retrieve_shard(shard_hostname=shard_hostname)
1997 rpc_utils.persist_records_sent_from_shard(shard_obj, jobs, hqes)
1998 assert len(known_host_ids) == len(known_host_statuses)
1999 for i in range(len(known_host_ids)):
2000 host_model = models.Host.objects.get(pk=known_host_ids[i])
2001 if host_model.status != known_host_statuses[i]:
2002 host_model.status = known_host_statuses[i]
2003 host_model.save()
2004
2005 hosts, jobs, suite_keyvals = rpc_utils.find_records_for_shard(
2006 shard_obj, known_job_ids=known_job_ids,
2007 known_host_ids=known_host_ids)
2008 return {
2009 'hosts': [host.serialize() for host in hosts],
2010 'jobs': [job.serialize() for job in jobs],
2011 'suite_keyvals': [kv.serialize() for kv in suite_keyvals],
2012 }
2013
2014
2015def get_shards(**filter_data):
2016 """Return a list of all shards.
2017
2018 @returns A sequence of nested dictionaries of shard information.
2019 """
2020 shards = models.Shard.query_objects(filter_data)
2021 serialized_shards = rpc_utils.prepare_rows_as_nested_dicts(shards, ())
2022 for serialized, shard in zip(serialized_shards, shards):
2023 serialized['labels'] = [label.name for label in shard.labels.all()]
2024
2025 return serialized_shards
2026
2027
2028def _assign_board_to_shard_precheck(labels):
2029 """Verify whether board labels are valid to be added to a given shard.
2030
2031 First check whether board label is in correct format. Second, check whether
2032 the board label exist. Third, check whether the board has already been
2033 assigned to shard.
2034
2035 @param labels: Board labels separated by comma.
2036
2037 @raises error.RPCException: If label provided doesn't start with `board:`
2038 or board has been added to shard already.
2039 @raises models.Label.DoesNotExist: If the label specified doesn't exist.
2040
2041 @returns: A list of label models that ready to be added to shard.
2042 """
2043 labels = labels.split(',')
2044 label_models = []
2045 for label in labels:
2046 # Check whether the board label is in correct format.
2047 if not label.startswith('board:'):
2048 raise error.RPCException('Sharding only supports `board:.*` label.')
2049 # Check whether the board label exist. If not, exception will be thrown
2050 # by smart_get function.
2051 label = models.Label.smart_get(label)
2052 # Check whether the board has been sharded already
2053 try:
2054 shard = models.Shard.objects.get(labels=label)
2055 raise error.RPCException(
2056 '%s is already on shard %s' % (label, shard.hostname))
2057 except models.Shard.DoesNotExist:
2058 # board is not on any shard, so it's valid.
2059 label_models.append(label)
2060 return label_models
2061
2062
2063def add_shard(hostname, labels):
2064 """Add a shard and start running jobs on it.
2065
2066 @param hostname: The hostname of the shard to be added; needs to be unique.
2067 @param labels: Board labels separated by comma. Jobs of one of the labels
2068 will be assigned to the shard.
2069
2070 @raises error.RPCException: If label provided doesn't start with `board:` or
2071 board has been added to shard already.
2072 @raises model_logic.ValidationError: If a shard with the given hostname
2073 already exist.
2074 @raises models.Label.DoesNotExist: If the label specified doesn't exist.
2075
2076 @returns: The id of the added shard.
2077 """
2078 labels = _assign_board_to_shard_precheck(labels)
2079 shard = models.Shard.add_object(hostname=hostname)
2080 for label in labels:
2081 shard.labels.add(label)
2082 return shard.id
2083
2084
2085def add_board_to_shard(hostname, labels):
2086 """Add boards to a given shard
2087
2088 @param hostname: The hostname of the shard to be changed.
2089 @param labels: Board labels separated by comma.
2090
2091 @raises error.RPCException: If label provided doesn't start with `board:` or
2092 board has been added to shard already.
2093 @raises models.Label.DoesNotExist: If the label specified doesn't exist.
2094
2095 @returns: The id of the changed shard.
2096 """
2097 labels = _assign_board_to_shard_precheck(labels)
2098 shard = models.Shard.objects.get(hostname=hostname)
2099 for label in labels:
2100 shard.labels.add(label)
2101 return shard.id
2102
2103
2104def delete_shard(hostname):
2105 """Delete a shard and reclaim all resources from it.
2106
2107 This claims back all assigned hosts from the shard. To ensure all DUTs are
2108 in a sane state, a Reboot task with highest priority is scheduled for them.
2109 This reboots the DUTs and then all left tasks continue to run in drone of
2110 the master.
2111
2112 The procedure for deleting a shard:
2113 * Lock all unlocked hosts on that shard.
2114 * Remove shard information .
2115 * Assign a reboot task with highest priority to these hosts.
2116 * Unlock these hosts, then, the reboot tasks run in front of all other
2117 tasks.
2118
2119 The status of jobs that haven't been reported to be finished yet, will be
2120 lost. The master scheduler will pick up the jobs and execute them.
2121
2122 @param hostname: Hostname of the shard to delete.
2123 """
2124 shard = rpc_utils.retrieve_shard(shard_hostname=hostname)
2125 hostnames_to_lock = [h.hostname for h in
2126 models.Host.objects.filter(shard=shard, locked=False)]
2127
2128 # TODO(beeps): Power off shard
2129 # For ChromeOS hosts, a reboot test with the highest priority is added to
2130 # the DUT. After a reboot it should be ganranteed that no processes from
2131 # prior tests that were run by a shard are still running on.
2132
2133 # Lock all unlocked hosts.
2134 dicts = {'locked': True, 'lock_time': datetime.datetime.now()}
2135 models.Host.objects.filter(hostname__in=hostnames_to_lock).update(**dicts)
2136
2137 # Remove shard information.
2138 models.Host.objects.filter(shard=shard).update(shard=None)
2139 models.Job.objects.filter(shard=shard).update(shard=None)
2140 shard.labels.clear()
2141 shard.delete()
2142
2143 # Assign a reboot task with highest priority: Super.
2144 t = models.Test.objects.get(name='platform_BootPerfServer:shard')
2145 c = utils.read_file(os.path.join(common.autotest_dir, t.path))
2146 if hostnames_to_lock:
2147 rpc_utils.create_job_common(
2148 'reboot_dut_for_shard_deletion',
2149 priority=priorities.Priority.SUPER,
2150 control_type='Server',
2151 control_file=c, hosts=hostnames_to_lock)
2152
2153 # Unlock these shard-related hosts.
2154 dicts = {'locked': False, 'lock_time': None}
2155 models.Host.objects.filter(hostname__in=hostnames_to_lock).update(**dicts)
2156
2157
2158def get_servers(hostname=None, role=None, status=None):
2159 """Get a list of servers with matching role and status.
2160
2161 @param hostname: FQDN of the server.
2162 @param role: Name of the server role, e.g., drone, scheduler. Default to
2163 None to match any role.
2164 @param status: Status of the server, e.g., primary, backup, repair_required.
2165 Default to None to match any server status.
2166
2167 @raises error.RPCException: If server database is not used.
2168 @return: A list of server names for servers with matching role and status.
2169 """
2170 if not server_manager_utils.use_server_db():
2171 raise error.RPCException('Server database is not enabled. Please try '
2172 'retrieve servers from global config.')
2173 servers = server_manager_utils.get_servers(hostname=hostname, role=role,
2174 status=status)
2175 return [s.get_details() for s in servers]
2176
2177
2178@rpc_utils.route_rpc_to_master
2179def get_stable_version(board=stable_version_utils.DEFAULT, android=False):
2180 """Get stable version for the given board.
2181
2182 @param board: Name of the board.
2183 @param android: If True, the given board is an Android-based device. If
2184 False, assume its a Chrome OS-based device.
2185
2186 @return: Stable version of the given board. Return global configure value
2187 of CROS.stable_cros_version if stable_versinos table does not have
2188 entry of board DEFAULT.
2189 """
2190 return stable_version_utils.get(board=board, android=android)
2191
2192
2193@rpc_utils.route_rpc_to_master
2194def get_all_stable_versions():
2195 """Get stable versions for all boards.
2196
2197 @return: A dictionary of board:version.
2198 """
2199 return stable_version_utils.get_all()
2200
2201
2202@rpc_utils.route_rpc_to_master
2203def set_stable_version(version, board=stable_version_utils.DEFAULT):
2204 """Modify stable version for the given board.
2205
2206 @param version: The new value of stable version for given board.
2207 @param board: Name of the board, default to value `DEFAULT`.
2208 """
2209 stable_version_utils.set(version=version, board=board)
2210
2211
2212@rpc_utils.route_rpc_to_master
2213def delete_stable_version(board):
2214 """Modify stable version for the given board.
2215
2216 Delete a stable version entry in afe_stable_versions table for a given
2217 board, so default stable version will be used.
2218
2219 @param board: Name of the board.
2220 """
2221 stable_version_utils.delete(board=board)
2222
2223
2224def get_tests_by_build(build, ignore_invalid_tests=True):
2225 """Get the tests that are available for the specified build.
2226
2227 @param build: unique name by which to refer to the image.
2228 @param ignore_invalid_tests: flag on if unparsable tests are ignored.
2229
2230 @return: A sorted list of all tests that are in the build specified.
2231 """
2232 # Collect the control files specified in this build
2233 cfile_getter = control_file_lib._initialize_control_file_getter(build)
2234 if SuiteBase.ENABLE_CONTROLS_IN_BATCH:
2235 control_file_info_list = cfile_getter.get_suite_info()
2236 control_file_list = control_file_info_list.keys()
2237 else:
2238 control_file_list = cfile_getter.get_control_file_list()
2239
2240 test_objects = []
2241 _id = 0
2242 for control_file_path in control_file_list:
2243 # Read and parse the control file
2244 if SuiteBase.ENABLE_CONTROLS_IN_BATCH:
2245 control_file = control_file_info_list[control_file_path]
2246 else:
2247 control_file = cfile_getter.get_control_file_contents(
2248 control_file_path)
2249 try:
2250 control_obj = control_data.parse_control_string(control_file)
2251 except:
2252 logging.info('Failed to parse control file: %s', control_file_path)
2253 if not ignore_invalid_tests:
2254 raise
2255
2256 # Extract the values needed for the AFE from the control_obj.
2257 # The keys list represents attributes in the control_obj that
2258 # are required by the AFE
2259 keys = ['author', 'doc', 'name', 'time', 'test_type', 'experimental',
2260 'test_category', 'test_class', 'dependencies', 'run_verify',
2261 'sync_count', 'job_retries', 'retries', 'path']
2262
2263 test_object = {}
2264 for key in keys:
2265 test_object[key] = getattr(control_obj, key) if hasattr(
2266 control_obj, key) else ''
2267
2268 # Unfortunately, the AFE expects different key-names for certain
2269 # values, these must be corrected to avoid the risk of tests
2270 # being omitted by the AFE.
2271 # The 'id' is an additional value used in the AFE.
2272 # The control_data parsing does not reference 'run_reset', but it
2273 # is also used in the AFE and defaults to True.
2274 test_object['id'] = _id
2275 test_object['run_reset'] = True
2276 test_object['description'] = test_object.get('doc', '')
2277 test_object['test_time'] = test_object.get('time', 0)
2278 test_object['test_retry'] = test_object.get('retries', 0)
2279
2280 # Fix the test name to be consistent with the current presentation
2281 # of test names in the AFE.
2282 testpath, subname = os.path.split(control_file_path)
2283 testname = os.path.basename(testpath)
2284 subname = subname.split('.')[1:]
2285 if subname:
2286 testname = '%s:%s' % (testname, ':'.join(subname))
2287
2288 test_object['name'] = testname
2289
2290 # Correct the test path as parse_control_string sets an empty string.
2291 test_object['path'] = control_file_path
2292
2293 _id += 1
2294 test_objects.append(test_object)
2295
2296 test_objects = sorted(test_objects, key=lambda x: x.get('name'))
2297 return rpc_utils.prepare_for_serialization(test_objects)