blob: 6c9c131dd7322f5e1732d21f63c365211ffc2efb [file] [log] [blame]
Shuqian Zhao54a5b672016-05-11 22:12:17 +00001# pylint: disable-msg=C0111
Aviv Keshet0b9cfc92013-02-05 11:36:02 -08002
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
MK Ryu9c5fbbe2015-02-11 15:46:22 -080035import sys
showard29f7cd22009-04-29 21:16:24 +000036import datetime
Shuqian Zhao4c0d2902016-01-12 17:03:15 -080037import logging
MK Ryu9c5fbbe2015-02-11 15:46:22 -080038
Moises Osorio2dc7a102014-12-02 18:24:02 -080039from django.db.models import Count
showardcafd16e2009-05-29 18:37:49 +000040import common
Michael Tang6dc174e2016-05-31 23:13:42 -070041from autotest_lib.client.common_lib import control_data
Simran Basib6ec8ae2014-04-23 12:05:08 -070042from autotest_lib.client.common_lib import priorities
Simran Basi6157e8e2015-12-07 18:22:34 -080043from autotest_lib.client.common_lib.cros import dev_server
Gabe Black1e1c41b2015-02-04 23:55:15 -080044from autotest_lib.client.common_lib.cros.graphite import autotest_stats
showard6d7b2ff2009-06-10 00:16:47 +000045from autotest_lib.frontend.afe import control_file, rpc_utils
J. Richard Barnetteb5164d62015-04-13 12:59:31 -070046from autotest_lib.frontend.afe import models, model_logic, model_attributes
Simran Basib6ec8ae2014-04-23 12:05:08 -070047from autotest_lib.frontend.afe import site_rpc_interface
Moises Osorio2dc7a102014-12-02 18:24:02 -080048from autotest_lib.frontend.tko import models as tko_models
Jiaxi Luoaac54572014-06-04 13:57:02 -070049from autotest_lib.frontend.tko import rpc_interface as tko_rpc_interface
J. Richard Barnetteb5164d62015-04-13 12:59:31 -070050from autotest_lib.server import frontend
Simran Basi71206ef2014-08-13 13:51:18 -070051from autotest_lib.server import utils
Dan Shid215dbe2015-06-18 16:14:59 -070052from autotest_lib.server.cros import provision
Jiaxi Luo90190c92014-06-18 12:35:57 -070053from autotest_lib.server.cros.dynamic_suite import tools
Aviv Keshet7ee95862016-08-30 15:18:27 -070054from autotest_lib.server.lib import status_history
mblighe8819cd2008-02-15 16:48:40 +000055
Moises Osorio2dc7a102014-12-02 18:24:02 -080056
Gabe Black1e1c41b2015-02-04 23:55:15 -080057_timer = autotest_stats.Timer('rpc_interface')
Moises Osorio2dc7a102014-12-02 18:24:02 -080058
Eric Lid23bc192011-02-09 14:38:57 -080059def get_parameterized_autoupdate_image_url(job):
60 """Get the parameterized autoupdate image url from a parameterized job."""
61 known_test_obj = models.Test.smart_get('autoupdate_ParameterizedJob')
62 image_parameter = known_test_obj.testparameter_set.get(test=known_test_obj,
beeps8bb1f7d2013-08-05 01:30:09 -070063 name='image')
Eric Lid23bc192011-02-09 14:38:57 -080064 para_set = job.parameterized_job.parameterizedjobparameter_set
65 job_test_para = para_set.get(test_parameter=image_parameter)
66 return job_test_para.parameter_value
67
68
mblighe8819cd2008-02-15 16:48:40 +000069# labels
70
mblighe8819cd2008-02-15 16:48:40 +000071def modify_label(id, **data):
MK Ryu8c554cf2015-06-12 11:45:50 -070072 """Modify a label.
73
74 @param id: id or name of a label. More often a label name.
75 @param data: New data for a label.
76 """
77 label_model = models.Label.smart_get(id)
MK Ryu8e2c2d02016-01-06 15:24:38 -080078 label_model.update_object(data)
MK Ryu8c554cf2015-06-12 11:45:50 -070079
80 # Master forwards the RPC to shards
81 if not utils.is_shard():
82 rpc_utils.fanout_rpc(label_model.host_set.all(), 'modify_label', False,
83 id=id, **data)
84
mblighe8819cd2008-02-15 16:48:40 +000085
86def delete_label(id):
MK Ryu8c554cf2015-06-12 11:45:50 -070087 """Delete a label.
88
89 @param id: id or name of a label. More often a label name.
90 """
91 label_model = models.Label.smart_get(id)
MK Ryu8e2c2d02016-01-06 15:24:38 -080092 # Hosts that have the label to be deleted. Save this info before
93 # the label is deleted to use it later.
94 hosts = []
95 for h in label_model.host_set.all():
96 hosts.append(models.Host.smart_get(h.id))
97 label_model.delete()
MK Ryu8c554cf2015-06-12 11:45:50 -070098
99 # Master forwards the RPC to shards
100 if not utils.is_shard():
MK Ryu8e2c2d02016-01-06 15:24:38 -0800101 rpc_utils.fanout_rpc(hosts, 'delete_label', False, id=id)
mblighe8819cd2008-02-15 16:48:40 +0000102
Prashanth Balasubramanian744898f2015-01-13 05:04:16 -0800103
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800104def add_label(name, ignore_exception_if_exists=False, **kwargs):
MK Ryucf027c62015-03-04 12:00:50 -0800105 """Adds a new label of a given name.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800106
107 @param name: label name.
108 @param ignore_exception_if_exists: If True and the exception was
109 thrown due to the duplicated label name when adding a label,
110 then suppress the exception. Default is False.
111 @param kwargs: keyword args that store more info about a label
112 other than the name.
113 @return: int/long id of a new label.
114 """
115 # models.Label.add_object() throws model_logic.ValidationError
116 # when it is given a label name that already exists.
117 # However, ValidationError can be thrown with different errors,
118 # and those errors should be thrown up to the call chain.
119 try:
120 label = models.Label.add_object(name=name, **kwargs)
121 except:
122 exc_info = sys.exc_info()
123 if ignore_exception_if_exists:
124 label = rpc_utils.get_label(name)
125 # If the exception is raised not because of duplicated
126 # "name", then raise the original exception.
127 if label is None:
128 raise exc_info[0], exc_info[1], exc_info[2]
129 else:
130 raise exc_info[0], exc_info[1], exc_info[2]
131 return label.id
132
133
134def add_label_to_hosts(id, hosts):
MK Ryucf027c62015-03-04 12:00:50 -0800135 """Adds a label of the given id to the given hosts only in local DB.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800136
137 @param id: id or name of a label. More often a label name.
138 @param hosts: The hostnames of hosts that need the label.
139
140 @raises models.Label.DoesNotExist: If the label with id doesn't exist.
141 """
142 label = models.Label.smart_get(id)
143 host_objs = models.Host.smart_get_bulk(hosts)
144 if label.platform:
145 models.Host.check_no_platform(host_objs)
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700146 # Ensure a host has no more than one board label with it.
147 if label.name.startswith('board:'):
148 models.Host.check_no_board(host_objs)
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800149 label.host_set.add(*host_objs)
150
151
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700152def _create_label_everywhere(id, hosts):
153 """
154 Yet another method to create labels.
155
156 ALERT! This method should be run only on master not shards!
157 DO NOT RUN THIS ON A SHARD!!! Deputies will hate you if you do!!!
158
159 This method exists primarily to serve label_add_hosts() and
160 host_add_labels(). Basically it pulls out the label check/add logic
161 from label_add_hosts() into this nice method that not only creates
162 the label but also tells the shards that service the hosts to also
163 create the label.
164
165 @param id: id or name of a label. More often a label name.
166 @param hosts: A list of hostnames or ids. More often hostnames.
167 """
168 try:
169 label = models.Label.smart_get(id)
170 except models.Label.DoesNotExist:
171 # This matches the type checks in smart_get, which is a hack
172 # in and off itself. The aim here is to create any non-existent
173 # label, which we cannot do if the 'id' specified isn't a label name.
174 if isinstance(id, basestring):
175 label = models.Label.smart_get(add_label(id))
176 else:
177 raise ValueError('Label id (%s) does not exist. Please specify '
178 'the argument, id, as a string (label name).'
179 % id)
180
181 # Make sure the label exists on the shard with the same id
182 # as it is on the master.
183 # It is possible that the label is already in a shard because
184 # we are adding a new label only to shards of hosts that the label
185 # is going to be attached.
186 # For example, we add a label L1 to a host in shard S1.
187 # Master and S1 will have L1 but other shards won't.
188 # Later, when we add the same label L1 to hosts in shards S1 and S2,
189 # S1 already has the label but S2 doesn't.
190 # S2 should have the new label without any problem.
191 # We ignore exception in such a case.
192 host_objs = models.Host.smart_get_bulk(hosts)
193 rpc_utils.fanout_rpc(
194 host_objs, 'add_label', include_hostnames=False,
195 name=label.name, ignore_exception_if_exists=True,
196 id=label.id, platform=label.platform)
197
198
MK Ryufbb002c2015-06-08 14:13:16 -0700199@rpc_utils.route_rpc_to_master
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800200def label_add_hosts(id, hosts):
MK Ryucf027c62015-03-04 12:00:50 -0800201 """Adds a label with the given id to the given hosts.
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800202
203 This method should be run only on master not shards.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800204 The given label will be created if it doesn't exist, provided the `id`
205 supplied is a label name not an int/long id.
206
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800207 @param id: id or name of a label. More often a label name.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800208 @param hosts: A list of hostnames or ids. More often hostnames.
209
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800210 @raises ValueError: If the id specified is an int/long (label id)
211 while the label does not exist.
Prashanth Balasubramanian5949b4a2014-11-23 12:58:30 -0800212 """
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700213 # Create the label.
214 _create_label_everywhere(id, hosts)
215
216 # Add it to the master.
MK Ryu8e2c2d02016-01-06 15:24:38 -0800217 add_label_to_hosts(id, hosts)
MK Ryucf027c62015-03-04 12:00:50 -0800218
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700219 # Add it to the shards.
MK Ryucf027c62015-03-04 12:00:50 -0800220 host_objs = models.Host.smart_get_bulk(hosts)
MK Ryu9c5fbbe2015-02-11 15:46:22 -0800221 rpc_utils.fanout_rpc(host_objs, 'add_label_to_hosts', id=id)
showardbbabf502008-06-06 00:02:02 +0000222
223
MK Ryucf027c62015-03-04 12:00:50 -0800224def remove_label_from_hosts(id, hosts):
225 """Removes a label of the given id from the given hosts only in local DB.
226
227 @param id: id or name of a label.
228 @param hosts: The hostnames of hosts that need to remove the label from.
229 """
showardbe3ec042008-11-12 18:16:07 +0000230 host_objs = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000231 models.Label.smart_get(id).host_set.remove(*host_objs)
showardbbabf502008-06-06 00:02:02 +0000232
233
MK Ryufbb002c2015-06-08 14:13:16 -0700234@rpc_utils.route_rpc_to_master
MK Ryucf027c62015-03-04 12:00:50 -0800235def label_remove_hosts(id, hosts):
236 """Removes a label of the given id from the given hosts.
237
238 This method should be run only on master not shards.
239
240 @param id: id or name of a label.
241 @param hosts: A list of hostnames or ids. More often hostnames.
242 """
MK Ryucf027c62015-03-04 12:00:50 -0800243 host_objs = models.Host.smart_get_bulk(hosts)
MK Ryu26f0c932015-05-28 18:14:33 -0700244 remove_label_from_hosts(id, hosts)
245
MK Ryu8e2c2d02016-01-06 15:24:38 -0800246 rpc_utils.fanout_rpc(host_objs, 'remove_label_from_hosts', id=id)
247
MK Ryucf027c62015-03-04 12:00:50 -0800248
Jiaxi Luo31874592014-06-11 10:36:35 -0700249def get_labels(exclude_filters=(), **filter_data):
showardc92da832009-04-07 18:14:34 +0000250 """\
Jiaxi Luo31874592014-06-11 10:36:35 -0700251 @param exclude_filters: A sequence of dictionaries of filters.
252
showardc92da832009-04-07 18:14:34 +0000253 @returns A sequence of nested dictionaries of label information.
254 """
Jiaxi Luo31874592014-06-11 10:36:35 -0700255 labels = models.Label.query_objects(filter_data)
256 for exclude_filter in exclude_filters:
257 labels = labels.exclude(**exclude_filter)
258 return rpc_utils.prepare_rows_as_nested_dicts(labels, ('atomic_group',))
showardc92da832009-04-07 18:14:34 +0000259
260
261# atomic groups
262
showarde9450c92009-06-30 01:58:52 +0000263def add_atomic_group(name, max_number_of_machines=None, description=None):
showardc92da832009-04-07 18:14:34 +0000264 return models.AtomicGroup.add_object(
265 name=name, max_number_of_machines=max_number_of_machines,
266 description=description).id
267
268
269def modify_atomic_group(id, **data):
270 models.AtomicGroup.smart_get(id).update_object(data)
271
272
273def delete_atomic_group(id):
274 models.AtomicGroup.smart_get(id).delete()
275
276
277def atomic_group_add_labels(id, labels):
278 label_objs = models.Label.smart_get_bulk(labels)
279 models.AtomicGroup.smart_get(id).label_set.add(*label_objs)
280
281
282def atomic_group_remove_labels(id, labels):
283 label_objs = models.Label.smart_get_bulk(labels)
284 models.AtomicGroup.smart_get(id).label_set.remove(*label_objs)
285
286
287def get_atomic_groups(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000288 return rpc_utils.prepare_for_serialization(
showardc92da832009-04-07 18:14:34 +0000289 models.AtomicGroup.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000290
291
292# hosts
293
Matthew Sartori68186332015-04-27 17:19:53 -0700294def add_host(hostname, status=None, locked=None, lock_reason='', protection=None):
295 if locked and not lock_reason:
296 raise model_logic.ValidationError(
297 {'locked': 'Please provide a reason for locking when adding host.'})
298
jadmanski0afbb632008-06-06 21:10:57 +0000299 return models.Host.add_object(hostname=hostname, status=status,
Matthew Sartori68186332015-04-27 17:19:53 -0700300 locked=locked, lock_reason=lock_reason,
301 protection=protection).id
mblighe8819cd2008-02-15 16:48:40 +0000302
303
MK Ryu33889612015-09-04 14:32:35 -0700304@rpc_utils.route_rpc_to_master
305def modify_host(id, **kwargs):
Jakob Juelich50e91f72014-10-01 12:43:23 -0700306 """Modify local attributes of a host.
307
308 If this is called on the master, but the host is assigned to a shard, this
MK Ryu33889612015-09-04 14:32:35 -0700309 will call `modify_host_local` RPC to the responsible shard. This means if
310 a host is being locked using this function, this change will also propagate
311 to shards.
312 When this is called on a shard, the shard just routes the RPC to the master
313 and does nothing.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700314
315 @param id: id of the host to modify.
MK Ryu33889612015-09-04 14:32:35 -0700316 @param kwargs: key=value pairs of values to set on the host.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700317 """
MK Ryu33889612015-09-04 14:32:35 -0700318 rpc_utils.check_modify_host(kwargs)
showardce7c0922009-09-11 18:39:24 +0000319 host = models.Host.smart_get(id)
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800320 try:
321 rpc_utils.check_modify_host_locking(host, kwargs)
322 except model_logic.ValidationError as e:
323 if not kwargs.get('force_modify_locking', False):
324 raise
325 logging.exception('The following exception will be ignored and lock '
326 'modification will be enforced. %s', e)
Jakob Juelich50e91f72014-10-01 12:43:23 -0700327
MK Ryud53e1492015-12-15 12:09:03 -0800328 # This is required to make `lock_time` for a host be exactly same
329 # between the master and a shard.
330 if kwargs.get('locked', None) and 'lock_time' not in kwargs:
331 kwargs['lock_time'] = datetime.datetime.now()
MK Ryu8e2c2d02016-01-06 15:24:38 -0800332 host.update_object(kwargs)
MK Ryud53e1492015-12-15 12:09:03 -0800333
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800334 # force_modifying_locking is not an internal field in database, remove.
335 kwargs.pop('force_modify_locking', None)
MK Ryu33889612015-09-04 14:32:35 -0700336 rpc_utils.fanout_rpc([host], 'modify_host_local',
337 include_hostnames=False, id=id, **kwargs)
mblighe8819cd2008-02-15 16:48:40 +0000338
339
MK Ryu33889612015-09-04 14:32:35 -0700340def modify_host_local(id, **kwargs):
341 """Modify host attributes in local DB.
342
343 @param id: Host id.
344 @param kwargs: key=value pairs of values to set on the host.
345 """
346 models.Host.smart_get(id).update_object(kwargs)
347
348
349@rpc_utils.route_rpc_to_master
showard276f9442009-05-20 00:33:16 +0000350def modify_hosts(host_filter_data, update_data):
Jakob Juelich50e91f72014-10-01 12:43:23 -0700351 """Modify local attributes of multiple hosts.
352
353 If this is called on the master, but one of the hosts in that match the
MK Ryu33889612015-09-04 14:32:35 -0700354 filters is assigned to a shard, this will call `modify_hosts_local` RPC
355 to the responsible shard.
356 When this is called on a shard, the shard just routes the RPC to the master
357 and does nothing.
Jakob Juelich50e91f72014-10-01 12:43:23 -0700358
359 The filters are always applied on the master, not on the shards. This means
360 if the states of a host differ on the master and a shard, the state on the
361 master will be used. I.e. this means:
362 A host was synced to Shard 1. On Shard 1 the status of the host was set to
363 'Repair Failed'.
364 - A call to modify_hosts with host_filter_data={'status': 'Ready'} will
365 update the host (both on the shard and on the master), because the state
366 of the host as the master knows it is still 'Ready'.
367 - A call to modify_hosts with host_filter_data={'status': 'Repair failed'
368 will not update the host, because the filter doesn't apply on the master.
369
showardbe0d8692009-08-20 23:42:44 +0000370 @param host_filter_data: Filters out which hosts to modify.
371 @param update_data: A dictionary with the changes to make to the hosts.
showard276f9442009-05-20 00:33:16 +0000372 """
MK Ryu93161712015-12-21 10:41:32 -0800373 update_data = update_data.copy()
showardbe0d8692009-08-20 23:42:44 +0000374 rpc_utils.check_modify_host(update_data)
showard276f9442009-05-20 00:33:16 +0000375 hosts = models.Host.query_objects(host_filter_data)
Jakob Juelich50e91f72014-10-01 12:43:23 -0700376
377 affected_shard_hostnames = set()
378 affected_host_ids = []
379
Alex Miller9658a952013-05-14 16:40:02 -0700380 # Check all hosts before changing data for exception safety.
381 for host in hosts:
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800382 try:
383 rpc_utils.check_modify_host_locking(host, update_data)
384 except model_logic.ValidationError as e:
385 if not update_data.get('force_modify_locking', False):
386 raise
387 logging.exception('The following exception will be ignored and '
388 'lock modification will be enforced. %s', e)
389
Jakob Juelich50e91f72014-10-01 12:43:23 -0700390 if host.shard:
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -0800391 affected_shard_hostnames.add(host.shard.rpc_hostname())
Jakob Juelich50e91f72014-10-01 12:43:23 -0700392 affected_host_ids.append(host.id)
393
MK Ryud53e1492015-12-15 12:09:03 -0800394 # This is required to make `lock_time` for a host be exactly same
395 # between the master and a shard.
396 if update_data.get('locked', None) and 'lock_time' not in update_data:
397 update_data['lock_time'] = datetime.datetime.now()
MK Ryu8e2c2d02016-01-06 15:24:38 -0800398 for host in hosts:
399 host.update_object(update_data)
MK Ryud53e1492015-12-15 12:09:03 -0800400
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800401 update_data.pop('force_modify_locking', None)
MK Ryu33889612015-09-04 14:32:35 -0700402 # Caution: Changing the filter from the original here. See docstring.
403 rpc_utils.run_rpc_on_multiple_hostnames(
404 'modify_hosts_local', affected_shard_hostnames,
Jakob Juelich50e91f72014-10-01 12:43:23 -0700405 host_filter_data={'id__in': affected_host_ids},
406 update_data=update_data)
407
showard276f9442009-05-20 00:33:16 +0000408
MK Ryu33889612015-09-04 14:32:35 -0700409def modify_hosts_local(host_filter_data, update_data):
410 """Modify attributes of hosts in local DB.
411
412 @param host_filter_data: Filters out which hosts to modify.
413 @param update_data: A dictionary with the changes to make to the hosts.
414 """
415 for host in models.Host.query_objects(host_filter_data):
416 host.update_object(update_data)
417
418
MK Ryufbb002c2015-06-08 14:13:16 -0700419def add_labels_to_host(id, labels):
420 """Adds labels to a given host only in local DB.
showardcafd16e2009-05-29 18:37:49 +0000421
MK Ryufbb002c2015-06-08 14:13:16 -0700422 @param id: id or hostname for a host.
423 @param labels: ids or names for labels.
424 """
425 label_objs = models.Label.smart_get_bulk(labels)
426 models.Host.smart_get(id).labels.add(*label_objs)
427
428
429@rpc_utils.route_rpc_to_master
430def host_add_labels(id, labels):
431 """Adds labels to a given host.
432
433 @param id: id or hostname for a host.
434 @param labels: ids or names for labels.
435
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700436 @raises ValidationError: If adding more than one platform/board label.
MK Ryufbb002c2015-06-08 14:13:16 -0700437 """
Kevin Chengbdfc57d2016-04-14 13:46:58 -0700438 # Create the labels on the master/shards.
439 for label in labels:
440 _create_label_everywhere(label, [id])
441
MK Ryufbb002c2015-06-08 14:13:16 -0700442 label_objs = models.Label.smart_get_bulk(labels)
443 platforms = [label.name for label in label_objs if label.platform]
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700444 boards = [label.name for label in label_objs
445 if label.name.startswith('board:')]
446 if len(platforms) > 1 or len(boards) > 1:
showardcafd16e2009-05-29 18:37:49 +0000447 raise model_logic.ValidationError(
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700448 {'labels': 'Adding more than one platform/board label: %s %s' %
449 (', '.join(platforms), ', '.join(boards))})
MK Ryufbb002c2015-06-08 14:13:16 -0700450
451 host_obj = models.Host.smart_get(id)
showardcafd16e2009-05-29 18:37:49 +0000452 if len(platforms) == 1:
MK Ryufbb002c2015-06-08 14:13:16 -0700453 models.Host.check_no_platform([host_obj])
Shuqian Zhao40e182b2016-10-11 11:55:11 -0700454 if len(boards) == 1:
455 models.Host.check_no_board([host_obj])
MK Ryu8e2c2d02016-01-06 15:24:38 -0800456 add_labels_to_host(id, labels)
MK Ryufbb002c2015-06-08 14:13:16 -0700457
458 rpc_utils.fanout_rpc([host_obj], 'add_labels_to_host', False,
459 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000460
461
MK Ryufbb002c2015-06-08 14:13:16 -0700462def remove_labels_from_host(id, labels):
463 """Removes labels from a given host only in local DB.
464
465 @param id: id or hostname for a host.
466 @param labels: ids or names for labels.
467 """
468 label_objs = models.Label.smart_get_bulk(labels)
469 models.Host.smart_get(id).labels.remove(*label_objs)
470
471
472@rpc_utils.route_rpc_to_master
mblighe8819cd2008-02-15 16:48:40 +0000473def host_remove_labels(id, labels):
MK Ryufbb002c2015-06-08 14:13:16 -0700474 """Removes labels from a given host.
475
476 @param id: id or hostname for a host.
477 @param labels: ids or names for labels.
478 """
MK Ryu8e2c2d02016-01-06 15:24:38 -0800479 remove_labels_from_host(id, labels)
480
MK Ryufbb002c2015-06-08 14:13:16 -0700481 host_obj = models.Host.smart_get(id)
482 rpc_utils.fanout_rpc([host_obj], 'remove_labels_from_host', False,
483 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000484
485
MK Ryuacf35922014-10-03 14:56:49 -0700486def get_host_attribute(attribute, **host_filter_data):
487 """
488 @param attribute: string name of attribute
489 @param host_filter_data: filter data to apply to Hosts to choose hosts to
490 act upon
491 """
492 hosts = rpc_utils.get_host_query((), False, False, True, host_filter_data)
493 hosts = list(hosts)
494 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
495 'attribute_list')
496 host_attr_dicts = []
497 for host_obj in hosts:
498 for attr_obj in host_obj.attribute_list:
499 if attr_obj.attribute == attribute:
500 host_attr_dicts.append(attr_obj.get_object_dict())
501 return rpc_utils.prepare_for_serialization(host_attr_dicts)
502
503
showard0957a842009-05-11 19:25:08 +0000504def set_host_attribute(attribute, value, **host_filter_data):
505 """
MK Ryu26f0c932015-05-28 18:14:33 -0700506 @param attribute: string name of attribute
507 @param value: string, or None to delete an attribute
508 @param host_filter_data: filter data to apply to Hosts to choose hosts to
509 act upon
showard0957a842009-05-11 19:25:08 +0000510 """
511 assert host_filter_data # disallow accidental actions on all hosts
512 hosts = models.Host.query_objects(host_filter_data)
513 models.AclGroup.check_for_acl_violation_hosts(hosts)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800514 for host in hosts:
515 host.set_or_delete_attribute(attribute, value)
showard0957a842009-05-11 19:25:08 +0000516
MK Ryu26f0c932015-05-28 18:14:33 -0700517 # Master forwards this RPC to shards.
518 if not utils.is_shard():
519 rpc_utils.fanout_rpc(hosts, 'set_host_attribute', False,
520 attribute=attribute, value=value, **host_filter_data)
521
showard0957a842009-05-11 19:25:08 +0000522
Jakob Juelich50e91f72014-10-01 12:43:23 -0700523@rpc_utils.forward_single_host_rpc_to_shard
mblighe8819cd2008-02-15 16:48:40 +0000524def delete_host(id):
jadmanski0afbb632008-06-06 21:10:57 +0000525 models.Host.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000526
527
showard87cc38f2009-08-20 23:37:04 +0000528def get_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
Dan Shi37df54d2015-12-14 11:16:28 -0800529 exclude_atomic_group_hosts=False, valid_only=True,
530 include_current_job=False, **filter_data):
531 """Get a list of dictionaries which contains the information of hosts.
532
showard87cc38f2009-08-20 23:37:04 +0000533 @param multiple_labels: match hosts in all of the labels given. Should
534 be a list of label names.
535 @param exclude_only_if_needed_labels: Exclude hosts with at least one
536 "only_if_needed" label applied.
537 @param exclude_atomic_group_hosts: Exclude hosts that have one or more
538 atomic group labels associated with them.
Dan Shi37df54d2015-12-14 11:16:28 -0800539 @param include_current_job: Set to True to include ids of currently running
540 job and special task.
jadmanski0afbb632008-06-06 21:10:57 +0000541 """
showard43a3d262008-11-12 18:17:05 +0000542 hosts = rpc_utils.get_host_query(multiple_labels,
543 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000544 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000545 valid_only, filter_data)
showard0957a842009-05-11 19:25:08 +0000546 hosts = list(hosts)
547 models.Host.objects.populate_relationships(hosts, models.Label,
548 'label_list')
549 models.Host.objects.populate_relationships(hosts, models.AclGroup,
550 'acl_list')
551 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
552 'attribute_list')
showard43a3d262008-11-12 18:17:05 +0000553 host_dicts = []
554 for host_obj in hosts:
555 host_dict = host_obj.get_object_dict()
showard0957a842009-05-11 19:25:08 +0000556 host_dict['labels'] = [label.name for label in host_obj.label_list]
showard909c9142009-07-07 20:54:42 +0000557 host_dict['platform'], host_dict['atomic_group'] = (rpc_utils.
558 find_platform_and_atomic_group(host_obj))
showard0957a842009-05-11 19:25:08 +0000559 host_dict['acls'] = [acl.name for acl in host_obj.acl_list]
560 host_dict['attributes'] = dict((attribute.attribute, attribute.value)
561 for attribute in host_obj.attribute_list)
Dan Shi37df54d2015-12-14 11:16:28 -0800562 if include_current_job:
563 host_dict['current_job'] = None
564 host_dict['current_special_task'] = None
565 entries = models.HostQueueEntry.objects.filter(
566 host_id=host_dict['id'], active=True, complete=False)
567 if entries:
568 host_dict['current_job'] = (
569 entries[0].get_object_dict()['job'])
570 tasks = models.SpecialTask.objects.filter(
571 host_id=host_dict['id'], is_active=True, is_complete=False)
572 if tasks:
573 host_dict['current_special_task'] = (
574 '%d-%s' % (tasks[0].get_object_dict()['id'],
575 tasks[0].get_object_dict()['task'].lower()))
showard43a3d262008-11-12 18:17:05 +0000576 host_dicts.append(host_dict)
577 return rpc_utils.prepare_for_serialization(host_dicts)
mblighe8819cd2008-02-15 16:48:40 +0000578
579
showard87cc38f2009-08-20 23:37:04 +0000580def get_num_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
showard8aa84fc2009-09-16 17:17:55 +0000581 exclude_atomic_group_hosts=False, valid_only=True,
582 **filter_data):
showard87cc38f2009-08-20 23:37:04 +0000583 """
584 Same parameters as get_hosts().
585
586 @returns The number of matching hosts.
587 """
showard43a3d262008-11-12 18:17:05 +0000588 hosts = rpc_utils.get_host_query(multiple_labels,
589 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000590 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000591 valid_only, filter_data)
showard43a3d262008-11-12 18:17:05 +0000592 return hosts.count()
showard1385b162008-03-13 15:59:40 +0000593
mblighe8819cd2008-02-15 16:48:40 +0000594
595# tests
596
showard909c7a62008-07-15 21:52:38 +0000597def add_test(name, test_type, path, author=None, dependencies=None,
showard3d9899a2008-07-31 02:11:58 +0000598 experimental=True, run_verify=None, test_class=None,
showard909c7a62008-07-15 21:52:38 +0000599 test_time=None, test_category=None, description=None,
600 sync_count=1):
jadmanski0afbb632008-06-06 21:10:57 +0000601 return models.Test.add_object(name=name, test_type=test_type, path=path,
showard909c7a62008-07-15 21:52:38 +0000602 author=author, dependencies=dependencies,
603 experimental=experimental,
604 run_verify=run_verify, test_time=test_time,
605 test_category=test_category,
606 sync_count=sync_count,
jadmanski0afbb632008-06-06 21:10:57 +0000607 test_class=test_class,
608 description=description).id
mblighe8819cd2008-02-15 16:48:40 +0000609
610
611def modify_test(id, **data):
jadmanski0afbb632008-06-06 21:10:57 +0000612 models.Test.smart_get(id).update_object(data)
mblighe8819cd2008-02-15 16:48:40 +0000613
614
615def delete_test(id):
jadmanski0afbb632008-06-06 21:10:57 +0000616 models.Test.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000617
618
619def get_tests(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000620 return rpc_utils.prepare_for_serialization(
621 models.Test.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000622
623
Moises Osorio2dc7a102014-12-02 18:24:02 -0800624@_timer.decorate
625def get_tests_status_counts_by_job_name_label(job_name_prefix, label_name):
626 """Gets the counts of all passed and failed tests from the matching jobs.
627
628 @param job_name_prefix: Name prefix of the jobs to get the summary from, e.g.,
629 'butterfly-release/R40-6457.21.0/bvt-cq/'.
630 @param label_name: Label that must be set in the jobs, e.g.,
631 'cros-version:butterfly-release/R40-6457.21.0'.
632
633 @returns A summary of the counts of all the passed and failed tests.
634 """
635 job_ids = list(models.Job.objects.filter(
636 name__startswith=job_name_prefix,
637 dependency_labels__name=label_name).values_list(
638 'pk', flat=True))
639 summary = {'passed': 0, 'failed': 0}
640 if not job_ids:
641 return summary
642
643 counts = (tko_models.TestView.objects.filter(
644 afe_job_id__in=job_ids).exclude(
645 test_name='SERVER_JOB').exclude(
646 test_name__startswith='CLIENT_JOB').values(
647 'status').annotate(
648 count=Count('status')))
649 for status in counts:
650 if status['status'] == 'GOOD':
651 summary['passed'] += status['count']
652 else:
653 summary['failed'] += status['count']
654 return summary
655
656
showard2b9a88b2008-06-13 20:55:03 +0000657# profilers
658
659def add_profiler(name, description=None):
660 return models.Profiler.add_object(name=name, description=description).id
661
662
663def modify_profiler(id, **data):
664 models.Profiler.smart_get(id).update_object(data)
665
666
667def delete_profiler(id):
668 models.Profiler.smart_get(id).delete()
669
670
671def get_profilers(**filter_data):
672 return rpc_utils.prepare_for_serialization(
673 models.Profiler.list_objects(filter_data))
674
675
mblighe8819cd2008-02-15 16:48:40 +0000676# users
677
678def add_user(login, access_level=None):
jadmanski0afbb632008-06-06 21:10:57 +0000679 return models.User.add_object(login=login, access_level=access_level).id
mblighe8819cd2008-02-15 16:48:40 +0000680
681
682def modify_user(id, **data):
jadmanski0afbb632008-06-06 21:10:57 +0000683 models.User.smart_get(id).update_object(data)
mblighe8819cd2008-02-15 16:48:40 +0000684
685
686def delete_user(id):
jadmanski0afbb632008-06-06 21:10:57 +0000687 models.User.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000688
689
690def get_users(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000691 return rpc_utils.prepare_for_serialization(
692 models.User.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000693
694
695# acl groups
696
697def add_acl_group(name, description=None):
showard04f2cd82008-07-25 20:53:31 +0000698 group = models.AclGroup.add_object(name=name, description=description)
showard64a95952010-01-13 21:27:16 +0000699 group.users.add(models.User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000700 return group.id
mblighe8819cd2008-02-15 16:48:40 +0000701
702
703def modify_acl_group(id, **data):
showard04f2cd82008-07-25 20:53:31 +0000704 group = models.AclGroup.smart_get(id)
705 group.check_for_acl_violation_acl_group()
706 group.update_object(data)
707 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000708
709
710def acl_group_add_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000711 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000712 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000713 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000714 group.users.add(*users)
mblighe8819cd2008-02-15 16:48:40 +0000715
716
717def acl_group_remove_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000718 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000719 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000720 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000721 group.users.remove(*users)
showard04f2cd82008-07-25 20:53:31 +0000722 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000723
724
725def acl_group_add_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000726 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000727 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000728 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000729 group.hosts.add(*hosts)
showard08f981b2008-06-24 21:59:03 +0000730 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000731
732
733def acl_group_remove_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000734 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000735 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000736 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000737 group.hosts.remove(*hosts)
showard08f981b2008-06-24 21:59:03 +0000738 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000739
740
741def delete_acl_group(id):
jadmanski0afbb632008-06-06 21:10:57 +0000742 models.AclGroup.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000743
744
745def get_acl_groups(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000746 acl_groups = models.AclGroup.list_objects(filter_data)
747 for acl_group in acl_groups:
748 acl_group_obj = models.AclGroup.objects.get(id=acl_group['id'])
749 acl_group['users'] = [user.login
750 for user in acl_group_obj.users.all()]
751 acl_group['hosts'] = [host.hostname
752 for host in acl_group_obj.hosts.all()]
753 return rpc_utils.prepare_for_serialization(acl_groups)
mblighe8819cd2008-02-15 16:48:40 +0000754
755
756# jobs
757
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700758def generate_control_file(tests=(), profilers=(),
showard91f85102009-10-12 20:34:52 +0000759 client_control_file='', use_container=False,
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700760 profile_only=None, db_tests=True,
761 test_source_build=None):
jadmanski0afbb632008-06-06 21:10:57 +0000762 """
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700763 Generates a client-side control file to run tests.
mbligh120351e2009-01-24 01:40:45 +0000764
Matthew Sartori10438092015-06-24 14:30:18 -0700765 @param tests List of tests to run. See db_tests for more information.
mbligh120351e2009-01-24 01:40:45 +0000766 @param profilers List of profilers to activate during the job.
767 @param client_control_file The contents of a client-side control file to
768 run at the end of all tests. If this is supplied, all tests must be
769 client side.
770 TODO: in the future we should support server control files directly
771 to wrap with a kernel. That'll require changing the parameter
772 name and adding a boolean to indicate if it is a client or server
773 control file.
774 @param use_container unused argument today. TODO: Enable containers
775 on the host during a client side test.
showard91f85102009-10-12 20:34:52 +0000776 @param profile_only A boolean that indicates what default profile_only
777 mode to use in the control file. Passing None will generate a
778 control file that does not explcitly set the default mode at all.
Matthew Sartori10438092015-06-24 14:30:18 -0700779 @param db_tests: if True, the test object can be found in the database
780 backing the test model. In this case, tests is a tuple
781 of test IDs which are used to retrieve the test objects
782 from the database. If False, tests is a tuple of test
783 dictionaries stored client-side in the AFE.
Michael Tang84a2ecf2016-06-07 15:10:53 -0700784 @param test_source_build: Build to be used to retrieve test code. Default
785 to None.
mbligh120351e2009-01-24 01:40:45 +0000786
787 @returns a dict with the following keys:
788 control_file: str, The control file text.
789 is_server: bool, is the control file a server-side control file?
790 synch_count: How many machines the job uses per autoserv execution.
791 synch_count == 1 means the job is asynchronous.
792 dependencies: A list of the names of labels on which the job depends.
793 """
showardd86debe2009-06-10 17:37:56 +0000794 if not tests and not client_control_file:
showard2bab8f42008-11-12 18:15:22 +0000795 return dict(control_file='', is_server=False, synch_count=1,
showard989f25d2008-10-01 11:38:11 +0000796 dependencies=[])
mblighe8819cd2008-02-15 16:48:40 +0000797
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700798 cf_info, test_objects, profiler_objects = (
799 rpc_utils.prepare_generate_control_file(tests, profilers,
800 db_tests))
showard989f25d2008-10-01 11:38:11 +0000801 cf_info['control_file'] = control_file.generate_control(
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700802 tests=test_objects, profilers=profiler_objects,
803 is_server=cf_info['is_server'],
showard232b7ae2009-11-10 00:46:48 +0000804 client_control_file=client_control_file, profile_only=profile_only,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700805 test_source_build=test_source_build)
showard989f25d2008-10-01 11:38:11 +0000806 return cf_info
mblighe8819cd2008-02-15 16:48:40 +0000807
808
Shuqian Zhao54a5b672016-05-11 22:12:17 +0000809def create_parameterized_job(name, priority, test, parameters, kernel=None,
810 label=None, profilers=(), profiler_parameters=None,
811 use_container=False, profile_only=None,
812 upload_kernel_config=False, hosts=(),
813 meta_hosts=(), one_time_hosts=(),
814 atomic_group_name=None, synch_count=None,
815 is_template=False, timeout=None,
816 timeout_mins=None, max_runtime_mins=None,
817 run_verify=False, email_list='', dependencies=(),
818 reboot_before=None, reboot_after=None,
819 parse_failed_repair=None, hostless=False,
820 keyvals=None, drone_set=None, run_reset=True,
821 require_ssp=None):
822 """
823 Creates and enqueues a parameterized job.
824
825 Most parameters a combination of the parameters for generate_control_file()
826 and create_job(), with the exception of:
827
828 @param test name or ID of the test to run
829 @param parameters a map of parameter name ->
830 tuple of (param value, param type)
831 @param profiler_parameters a dictionary of parameters for the profilers:
832 key: profiler name
833 value: dict of param name -> tuple of
834 (param value,
835 param type)
836 """
837 # Save the values of the passed arguments here. What we're going to do with
838 # them is pass them all to rpc_utils.get_create_job_common_args(), which
839 # will extract the subset of these arguments that apply for
840 # rpc_utils.create_job_common(), which we then pass in to that function.
841 args = locals()
842
843 # Set up the parameterized job configs
844 test_obj = models.Test.smart_get(test)
845 control_type = test_obj.test_type
846
847 try:
848 label = models.Label.smart_get(label)
849 except models.Label.DoesNotExist:
850 label = None
851
852 kernel_objs = models.Kernel.create_kernels(kernel)
853 profiler_objs = [models.Profiler.smart_get(profiler)
854 for profiler in profilers]
855
856 parameterized_job = models.ParameterizedJob.objects.create(
857 test=test_obj, label=label, use_container=use_container,
858 profile_only=profile_only,
859 upload_kernel_config=upload_kernel_config)
860 parameterized_job.kernels.add(*kernel_objs)
861
862 for profiler in profiler_objs:
863 parameterized_profiler = models.ParameterizedJobProfiler.objects.create(
864 parameterized_job=parameterized_job,
865 profiler=profiler)
866 profiler_params = profiler_parameters.get(profiler.name, {})
867 for name, (value, param_type) in profiler_params.iteritems():
868 models.ParameterizedJobProfilerParameter.objects.create(
869 parameterized_job_profiler=parameterized_profiler,
870 parameter_name=name,
871 parameter_value=value,
872 parameter_type=param_type)
873
874 try:
875 for parameter in test_obj.testparameter_set.all():
876 if parameter.name in parameters:
877 param_value, param_type = parameters.pop(parameter.name)
878 parameterized_job.parameterizedjobparameter_set.create(
879 test_parameter=parameter, parameter_value=param_value,
880 parameter_type=param_type)
881
882 if parameters:
883 raise Exception('Extra parameters remain: %r' % parameters)
884
885 return rpc_utils.create_job_common(
886 parameterized_job=parameterized_job.id,
887 control_type=control_type,
888 **rpc_utils.get_create_job_common_args(args))
889 except:
890 parameterized_job.delete()
891 raise
892
893
Simran Basib6ec8ae2014-04-23 12:05:08 -0700894def create_job_page_handler(name, priority, control_file, control_type,
Dan Shid215dbe2015-06-18 16:14:59 -0700895 image=None, hostless=False, firmware_rw_build=None,
896 firmware_ro_build=None, test_source_build=None,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700897 is_cloning=False, **kwargs):
Simran Basib6ec8ae2014-04-23 12:05:08 -0700898 """\
899 Create and enqueue a job.
900
901 @param name name of this job
902 @param priority Integer priority of this job. Higher is more important.
903 @param control_file String contents of the control file.
904 @param control_type Type of control file, Client or Server.
Dan Shid215dbe2015-06-18 16:14:59 -0700905 @param image: ChromeOS build to be installed in the dut. Default to None.
906 @param firmware_rw_build: Firmware build to update RW firmware. Default to
907 None, i.e., RW firmware will not be updated.
908 @param firmware_ro_build: Firmware build to update RO firmware. Default to
909 None, i.e., RO firmware will not be updated.
910 @param test_source_build: Build to be used to retrieve test code. Default
911 to None.
Michael Tang6dc174e2016-05-31 23:13:42 -0700912 @param is_cloning: True if creating a cloning job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700913 @param kwargs extra args that will be required by create_suite_job or
914 create_job.
915
916 @returns The created Job id number.
917 """
Michael Tang6dc174e2016-05-31 23:13:42 -0700918 if is_cloning:
919 logging.info('Start to clone a new job')
Shuqian Zhao61f5d312016-08-05 17:15:23 -0700920 # When cloning a job, hosts and meta_hosts should not exist together,
921 # which would cause host-scheduler to schedule two hqe jobs to one host
922 # at the same time, and crash itself. Clear meta_hosts for this case.
923 if kwargs.get('hosts') and kwargs.get('meta_hosts'):
924 kwargs['meta_hosts'] = []
Michael Tang6dc174e2016-05-31 23:13:42 -0700925 else:
926 logging.info('Start to create a new job')
Simran Basib6ec8ae2014-04-23 12:05:08 -0700927 control_file = rpc_utils.encode_ascii(control_file)
Jiaxi Luodd67beb2014-07-18 16:28:31 -0700928 if not control_file:
929 raise model_logic.ValidationError({
930 'control_file' : "Control file cannot be empty"})
Simran Basib6ec8ae2014-04-23 12:05:08 -0700931
932 if image and hostless:
Dan Shid215dbe2015-06-18 16:14:59 -0700933 builds = {}
934 builds[provision.CROS_VERSION_PREFIX] = image
935 if firmware_rw_build:
Dan Shi0723bf52015-06-24 10:52:38 -0700936 builds[provision.FW_RW_VERSION_PREFIX] = firmware_rw_build
Dan Shid215dbe2015-06-18 16:14:59 -0700937 if firmware_ro_build:
938 builds[provision.FW_RO_VERSION_PREFIX] = firmware_ro_build
Simran Basib6ec8ae2014-04-23 12:05:08 -0700939 return site_rpc_interface.create_suite_job(
940 name=name, control_file=control_file, priority=priority,
Michael Tang6dc174e2016-05-31 23:13:42 -0700941 builds=builds, test_source_build=test_source_build,
942 is_cloning=is_cloning, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700943 return create_job(name, priority, control_file, control_type, image=image,
Michael Tang6dc174e2016-05-31 23:13:42 -0700944 hostless=hostless, is_cloning=is_cloning, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700945
946
MK Ryue301eb72015-06-25 12:51:02 -0700947@rpc_utils.route_rpc_to_master
showard12f3e322009-05-13 21:27:42 +0000948def create_job(name, priority, control_file, control_type,
949 hosts=(), meta_hosts=(), one_time_hosts=(),
950 atomic_group_name=None, synch_count=None, is_template=False,
Simran Basi7e605742013-11-12 13:43:36 -0800951 timeout=None, timeout_mins=None, max_runtime_mins=None,
952 run_verify=False, email_list='', dependencies=(),
953 reboot_before=None, reboot_after=None, parse_failed_repair=None,
954 hostless=False, keyvals=None, drone_set=None, image=None,
Dan Shiec1d47d2015-02-13 11:38:13 -0800955 parent_job_id=None, test_retry=0, run_reset=True,
Michael Tang6dc174e2016-05-31 23:13:42 -0700956 require_ssp=None, args=(), is_cloning=False, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000957 """\
958 Create and enqueue a job.
mblighe8819cd2008-02-15 16:48:40 +0000959
showarda1e74b32009-05-12 17:32:04 +0000960 @param name name of this job
Alex Miller7d658cf2013-09-04 16:00:35 -0700961 @param priority Integer priority of this job. Higher is more important.
showarda1e74b32009-05-12 17:32:04 +0000962 @param control_file String contents of the control file.
963 @param control_type Type of control file, Client or Server.
964 @param synch_count How many machines the job uses per autoserv execution.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700965 synch_count == 1 means the job is asynchronous. If an atomic group is
966 given this value is treated as a minimum.
showarda1e74b32009-05-12 17:32:04 +0000967 @param is_template If true then create a template job.
968 @param timeout Hours after this call returns until the job times out.
Simran Basi7e605742013-11-12 13:43:36 -0800969 @param timeout_mins Minutes after this call returns until the job times
Jiaxi Luo90190c92014-06-18 12:35:57 -0700970 out.
Simran Basi34217022012-11-06 13:43:15 -0800971 @param max_runtime_mins Minutes from job starting time until job times out
showarda1e74b32009-05-12 17:32:04 +0000972 @param run_verify Should the host be verified before running the test?
973 @param email_list String containing emails to mail when the job is done
974 @param dependencies List of label names on which this job depends
975 @param reboot_before Never, If dirty, or Always
976 @param reboot_after Never, If all tests passed, or Always
977 @param parse_failed_repair if true, results of failed repairs launched by
Jiaxi Luo90190c92014-06-18 12:35:57 -0700978 this job will be parsed as part of the job.
showarda9545c02009-12-18 22:44:26 +0000979 @param hostless if true, create a hostless job
showardc1a98d12010-01-15 00:22:22 +0000980 @param keyvals dict of keyvals to associate with the job
showarda1e74b32009-05-12 17:32:04 +0000981 @param hosts List of hosts to run job on.
982 @param meta_hosts List where each entry is a label name, and for each entry
Jiaxi Luo90190c92014-06-18 12:35:57 -0700983 one host will be chosen from that label to run the job on.
showarda1e74b32009-05-12 17:32:04 +0000984 @param one_time_hosts List of hosts not in the database to run the job on.
985 @param atomic_group_name The name of an atomic group to schedule the job on.
jamesren76fcf192010-04-21 20:39:50 +0000986 @param drone_set The name of the drone set to run this test on.
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800987 @param image OS image to install before running job.
Aviv Keshet0b9cfc92013-02-05 11:36:02 -0800988 @param parent_job_id id of a job considered to be parent of created job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700989 @param test_retry Number of times to retry test if the test did not
Jiaxi Luo90190c92014-06-18 12:35:57 -0700990 complete successfully. (optional, default: 0)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700991 @param run_reset Should the host be reset before running the test?
Dan Shiec1d47d2015-02-13 11:38:13 -0800992 @param require_ssp Set to True to require server-side packaging to run the
993 test. If it's set to None, drone will still try to run
994 the server side with server-side packaging. If the
995 autotest-server package doesn't exist for the build or
996 image is not set, drone will run the test without server-
997 side packaging. Default is None.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700998 @param args A list of args to be injected into control file.
Michael Tang6dc174e2016-05-31 23:13:42 -0700999 @param is_cloning: True if creating a cloning job.
Simran Basib6ec8ae2014-04-23 12:05:08 -07001000 @param kwargs extra keyword args. NOT USED.
showardc92da832009-04-07 18:14:34 +00001001
1002 @returns The created Job id number.
jadmanski0afbb632008-06-06 21:10:57 +00001003 """
Jiaxi Luo90190c92014-06-18 12:35:57 -07001004 if args:
1005 control_file = tools.inject_vars({'args': args}, control_file)
1006
Simran Basiab5a1bf2014-05-28 15:39:44 -07001007 if image is None:
1008 return rpc_utils.create_job_common(
1009 **rpc_utils.get_create_job_common_args(locals()))
1010
Simran Basi6157e8e2015-12-07 18:22:34 -08001011 # Translate the image name, in case its a relative build name.
1012 ds = dev_server.ImageServer.resolve(image)
1013 image = ds.translate(image)
1014
Simran Basiab5a1bf2014-05-28 15:39:44 -07001015 # When image is supplied use a known parameterized test already in the
1016 # database to pass the OS image path from the front end, through the
1017 # scheduler, and finally to autoserv as the --image parameter.
1018
1019 # The test autoupdate_ParameterizedJob is in afe_autotests and used to
1020 # instantiate a Test object and from there a ParameterizedJob.
1021 known_test_obj = models.Test.smart_get('autoupdate_ParameterizedJob')
1022 known_parameterized_job = models.ParameterizedJob.objects.create(
1023 test=known_test_obj)
1024
1025 # autoupdate_ParameterizedJob has a single parameter, the image parameter,
1026 # stored in the table afe_test_parameters. We retrieve and set this
1027 # instance of the parameter to the OS image path.
1028 image_parameter = known_test_obj.testparameter_set.get(test=known_test_obj,
1029 name='image')
1030 known_parameterized_job.parameterizedjobparameter_set.create(
1031 test_parameter=image_parameter, parameter_value=image,
1032 parameter_type='string')
1033
Dan Shid215dbe2015-06-18 16:14:59 -07001034 # TODO(crbug.com/502638): save firmware build etc to parameterized_job.
1035
Simran Basiab5a1bf2014-05-28 15:39:44 -07001036 # By passing a parameterized_job to create_job_common the job entry in
1037 # the afe_jobs table will have the field parameterized_job_id set.
1038 # The scheduler uses this id in the afe_parameterized_jobs table to
1039 # match this job to our known test, and then with the
1040 # afe_parameterized_job_parameters table to get the actual image path.
jamesren4a41e012010-07-16 22:33:48 +00001041 return rpc_utils.create_job_common(
Simran Basiab5a1bf2014-05-28 15:39:44 -07001042 parameterized_job=known_parameterized_job.id,
jamesren4a41e012010-07-16 22:33:48 +00001043 **rpc_utils.get_create_job_common_args(locals()))
mblighe8819cd2008-02-15 16:48:40 +00001044
1045
showard9dbdcda2008-10-14 17:34:36 +00001046def abort_host_queue_entries(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001047 """\
showard9dbdcda2008-10-14 17:34:36 +00001048 Abort a set of host queue entries.
Fang Deng63b0e452014-12-19 14:38:15 -08001049
1050 @return: A list of dictionaries, each contains information
1051 about an aborted HQE.
jadmanski0afbb632008-06-06 21:10:57 +00001052 """
showard9dbdcda2008-10-14 17:34:36 +00001053 query = models.HostQueueEntry.query_objects(filter_data)
beepsfaecbce2013-10-29 11:35:10 -07001054
1055 # Dont allow aborts on:
1056 # 1. Jobs that have already completed (whether or not they were aborted)
1057 # 2. Jobs that we have already been aborted (but may not have completed)
1058 query = query.filter(complete=False).filter(aborted=False)
showarddc817512008-11-12 18:16:41 +00001059 models.AclGroup.check_abort_permissions(query)
showard9dbdcda2008-10-14 17:34:36 +00001060 host_queue_entries = list(query.select_related())
showard2bab8f42008-11-12 18:15:22 +00001061 rpc_utils.check_abort_synchronous_jobs(host_queue_entries)
mblighe8819cd2008-02-15 16:48:40 +00001062
Simran Basic1b26762013-06-26 14:23:21 -07001063 models.HostQueueEntry.abort_host_queue_entries(host_queue_entries)
Fang Deng63b0e452014-12-19 14:38:15 -08001064 hqe_info = [{'HostQueueEntry': hqe.id, 'Job': hqe.job_id,
1065 'Job name': hqe.job.name} for hqe in host_queue_entries]
1066 return hqe_info
showard9d821ab2008-07-11 16:54:29 +00001067
1068
beeps8bb1f7d2013-08-05 01:30:09 -07001069def abort_special_tasks(**filter_data):
1070 """\
1071 Abort the special task, or tasks, specified in the filter.
1072 """
1073 query = models.SpecialTask.query_objects(filter_data)
1074 special_tasks = query.filter(is_active=True)
1075 for task in special_tasks:
1076 task.abort()
1077
1078
Simran Basi73dae552013-02-25 14:57:46 -08001079def _call_special_tasks_on_hosts(task, hosts):
1080 """\
1081 Schedules a set of hosts for a special task.
1082
1083 @returns A list of hostnames that a special task was created for.
1084 """
1085 models.AclGroup.check_for_acl_violation_hosts(hosts)
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -08001086 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001087 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -08001088 raise ValueError('The following hosts are on shards, please '
1089 'follow the link to the shards and create jobs '
1090 'there instead. %s.' % shard_host_map)
Simran Basi73dae552013-02-25 14:57:46 -08001091 for host in hosts:
1092 models.SpecialTask.schedule_special_task(host, task)
1093 return list(sorted(host.hostname for host in hosts))
1094
1095
MK Ryu5aa25042015-07-28 16:08:04 -07001096def _forward_special_tasks_on_hosts(task, rpc, **filter_data):
1097 """Forward special tasks to corresponding shards.
mbligh4e545a52009-12-19 05:30:39 +00001098
MK Ryu5aa25042015-07-28 16:08:04 -07001099 For master, when special tasks are fired on hosts that are sharded,
1100 forward the RPC to corresponding shards.
1101
1102 For shard, create special task records in local DB.
1103
1104 @param task: Enum value of frontend.afe.models.SpecialTask.Task
1105 @param rpc: RPC name to forward.
1106 @param filter_data: Filter keywords to be used for DB query.
1107
1108 @return: A list of hostnames that a special task was created for.
showard1ff7b2e2009-05-15 23:17:18 +00001109 """
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001110 hosts = models.Host.query_objects(filter_data)
1111 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts, rpc_hostnames=True)
1112
1113 # Filter out hosts on a shard from those on the master, forward
1114 # rpcs to the shard with an additional hostname__in filter, and
1115 # create a local SpecialTask for each remaining host.
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001116 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001117 hosts = [h for h in hosts if h.shard is None]
1118 for shard, hostnames in shard_host_map.iteritems():
1119
1120 # The main client of this module is the frontend website, and
1121 # it invokes it with an 'id' or an 'id__in' filter. Regardless,
1122 # the 'hostname' filter should narrow down the list of hosts on
1123 # each shard even though we supply all the ids in filter_data.
1124 # This method uses hostname instead of id because it fits better
MK Ryu5aa25042015-07-28 16:08:04 -07001125 # with the overall architecture of redirection functions in
1126 # rpc_utils.
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001127 shard_filter = filter_data.copy()
1128 shard_filter['hostname__in'] = hostnames
1129 rpc_utils.run_rpc_on_multiple_hostnames(
MK Ryu5aa25042015-07-28 16:08:04 -07001130 rpc, [shard], **shard_filter)
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001131
1132 # There is a race condition here if someone assigns a shard to one of these
1133 # hosts before we create the task. The host will stay on the master if:
1134 # 1. The host is not Ready
1135 # 2. The host is Ready but has a task
1136 # But if the host is Ready and doesn't have a task yet, it will get sent
1137 # to the shard as we're creating a task here.
1138
1139 # Given that we only rarely verify Ready hosts it isn't worth putting this
1140 # entire method in a transaction. The worst case scenario is that we have
MK Ryu5aa25042015-07-28 16:08:04 -07001141 # a verify running on a Ready host while the shard is using it, if the
1142 # verify fails no subsequent tasks will be created against the host on the
1143 # master, and verifies are safe enough that this is OK.
1144 return _call_special_tasks_on_hosts(task, hosts)
1145
1146
1147def reverify_hosts(**filter_data):
1148 """\
1149 Schedules a set of hosts for verify.
1150
1151 @returns A list of hostnames that a verify task was created for.
1152 """
1153 return _forward_special_tasks_on_hosts(
1154 models.SpecialTask.Task.VERIFY, 'reverify_hosts', **filter_data)
Simran Basi73dae552013-02-25 14:57:46 -08001155
1156
1157def repair_hosts(**filter_data):
1158 """\
1159 Schedules a set of hosts for repair.
1160
1161 @returns A list of hostnames that a repair task was created for.
1162 """
MK Ryu5aa25042015-07-28 16:08:04 -07001163 return _forward_special_tasks_on_hosts(
1164 models.SpecialTask.Task.REPAIR, 'repair_hosts', **filter_data)
showard1ff7b2e2009-05-15 23:17:18 +00001165
1166
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001167def get_jobs(not_yet_run=False, running=False, finished=False,
1168 suite=False, sub=False, standalone=False, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001169 """\
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001170 Extra status filter args for get_jobs:
jadmanski0afbb632008-06-06 21:10:57 +00001171 -not_yet_run: Include only jobs that have not yet started running.
1172 -running: Include only jobs that have start running but for which not
1173 all hosts have completed.
1174 -finished: Include only jobs for which all hosts have completed (or
1175 aborted).
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001176
1177 Extra type filter args for get_jobs:
1178 -suite: Include only jobs with child jobs.
1179 -sub: Include only jobs with a parent job.
1180 -standalone: Inlcude only jobs with no child or parent jobs.
1181 At most one of these three fields should be specified.
jadmanski0afbb632008-06-06 21:10:57 +00001182 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001183 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1184 running,
1185 finished)
1186 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1187 suite,
1188 sub,
1189 standalone)
showard0957a842009-05-11 19:25:08 +00001190 job_dicts = []
1191 jobs = list(models.Job.query_objects(filter_data))
1192 models.Job.objects.populate_relationships(jobs, models.Label,
1193 'dependencies')
showardc1a98d12010-01-15 00:22:22 +00001194 models.Job.objects.populate_relationships(jobs, models.JobKeyval, 'keyvals')
showard0957a842009-05-11 19:25:08 +00001195 for job in jobs:
1196 job_dict = job.get_object_dict()
1197 job_dict['dependencies'] = ','.join(label.name
1198 for label in job.dependencies)
showardc1a98d12010-01-15 00:22:22 +00001199 job_dict['keyvals'] = dict((keyval.key, keyval.value)
1200 for keyval in job.keyvals)
Eric Lid23bc192011-02-09 14:38:57 -08001201 if job.parameterized_job:
1202 job_dict['image'] = get_parameterized_autoupdate_image_url(job)
showard0957a842009-05-11 19:25:08 +00001203 job_dicts.append(job_dict)
1204 return rpc_utils.prepare_for_serialization(job_dicts)
mblighe8819cd2008-02-15 16:48:40 +00001205
1206
1207def get_num_jobs(not_yet_run=False, running=False, finished=False,
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001208 suite=False, sub=False, standalone=False,
jadmanski0afbb632008-06-06 21:10:57 +00001209 **filter_data):
Aviv Keshet17660a52016-04-06 18:56:43 +00001210 """\
1211 See get_jobs() for documentation of extra filter parameters.
jadmanski0afbb632008-06-06 21:10:57 +00001212 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001213 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1214 running,
1215 finished)
1216 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1217 suite,
1218 sub,
1219 standalone)
Aviv Keshet17660a52016-04-06 18:56:43 +00001220 return models.Job.query_count(filter_data)
mblighe8819cd2008-02-15 16:48:40 +00001221
1222
mblighe8819cd2008-02-15 16:48:40 +00001223def get_jobs_summary(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001224 """\
Jiaxi Luoaac54572014-06-04 13:57:02 -07001225 Like get_jobs(), but adds 'status_counts' and 'result_counts' field.
1226
1227 'status_counts' filed is a dictionary mapping status strings to the number
1228 of hosts currently with that status, i.e. {'Queued' : 4, 'Running' : 2}.
1229
1230 'result_counts' field is piped to tko's rpc_interface and has the return
1231 format specified under get_group_counts.
jadmanski0afbb632008-06-06 21:10:57 +00001232 """
1233 jobs = get_jobs(**filter_data)
1234 ids = [job['id'] for job in jobs]
1235 all_status_counts = models.Job.objects.get_status_counts(ids)
1236 for job in jobs:
1237 job['status_counts'] = all_status_counts[job['id']]
Jiaxi Luoaac54572014-06-04 13:57:02 -07001238 job['result_counts'] = tko_rpc_interface.get_status_counts(
1239 ['afe_job_id', 'afe_job_id'],
1240 header_groups=[['afe_job_id'], ['afe_job_id']],
1241 **{'afe_job_id': job['id']})
jadmanski0afbb632008-06-06 21:10:57 +00001242 return rpc_utils.prepare_for_serialization(jobs)
mblighe8819cd2008-02-15 16:48:40 +00001243
1244
showarda965cef2009-05-15 23:17:41 +00001245def get_info_for_clone(id, preserve_metahosts, queue_entry_filter_data=None):
showarda8709c52008-07-03 19:44:54 +00001246 """\
1247 Retrieves all the information needed to clone a job.
1248 """
showarda8709c52008-07-03 19:44:54 +00001249 job = models.Job.objects.get(id=id)
showard29f7cd22009-04-29 21:16:24 +00001250 job_info = rpc_utils.get_job_info(job,
showarda965cef2009-05-15 23:17:41 +00001251 preserve_metahosts,
1252 queue_entry_filter_data)
showard945072f2008-09-03 20:34:59 +00001253
showardd9992fe2008-07-31 02:15:03 +00001254 host_dicts = []
showard29f7cd22009-04-29 21:16:24 +00001255 for host in job_info['hosts']:
1256 host_dict = get_hosts(id=host.id)[0]
1257 other_labels = host_dict['labels']
1258 if host_dict['platform']:
1259 other_labels.remove(host_dict['platform'])
1260 host_dict['other_labels'] = ', '.join(other_labels)
showardd9992fe2008-07-31 02:15:03 +00001261 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001262
showard29f7cd22009-04-29 21:16:24 +00001263 for host in job_info['one_time_hosts']:
1264 host_dict = dict(hostname=host.hostname,
1265 id=host.id,
1266 platform='(one-time host)',
1267 locked_text='')
1268 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001269
showard4d077562009-05-08 18:24:36 +00001270 # convert keys from Label objects to strings (names of labels)
showard29f7cd22009-04-29 21:16:24 +00001271 meta_host_counts = dict((meta_host.name, count) for meta_host, count
showard4d077562009-05-08 18:24:36 +00001272 in job_info['meta_host_counts'].iteritems())
showard29f7cd22009-04-29 21:16:24 +00001273
1274 info = dict(job=job.get_object_dict(),
1275 meta_host_counts=meta_host_counts,
1276 hosts=host_dicts)
1277 info['job']['dependencies'] = job_info['dependencies']
1278 if job_info['atomic_group']:
1279 info['atomic_group_name'] = (job_info['atomic_group']).name
1280 else:
1281 info['atomic_group_name'] = None
jamesren2275ef12010-04-12 18:25:06 +00001282 info['hostless'] = job_info['hostless']
jamesren76fcf192010-04-21 20:39:50 +00001283 info['drone_set'] = job.drone_set and job.drone_set.name
showarda8709c52008-07-03 19:44:54 +00001284
Michael Tang6dc174e2016-05-31 23:13:42 -07001285 image = _get_image_for_job(job, job_info['hostless'])
1286 if image:
1287 info['job']['image'] = image
Eric Lid23bc192011-02-09 14:38:57 -08001288
showarda8709c52008-07-03 19:44:54 +00001289 return rpc_utils.prepare_for_serialization(info)
1290
1291
Michael Tang6dc174e2016-05-31 23:13:42 -07001292def _get_image_for_job(job, hostless):
1293 """ Gets the image used for a job.
1294
1295 Gets the image used for an AFE job. If the job is a parameterized job, get
1296 the image from the job parameter; otherwise, tries to get the image from
1297 the job's keyvals 'build' or 'builds'. As a last resort, if the job is a
1298 hostless job, tries to get the image from its control file attributes
1299 'build' or 'builds'.
1300
1301 TODO(ntang): Needs to handle FAFT with two builds for ro/rw.
1302
1303 @param job An AFE job object.
1304 @param hostless Boolean on of the job is hostless.
1305
1306 @returns The image build used for the job.
1307 """
1308 image = None
1309 if job.parameterized_job:
1310 image = get_parameterized_autoupdate_image_url(job)
1311 else:
1312 keyvals = job.keyval_dict()
Michael Tang84a2ecf2016-06-07 15:10:53 -07001313 image = keyvals.get('build')
Michael Tang6dc174e2016-05-31 23:13:42 -07001314 if not image:
1315 value = keyvals.get('builds')
1316 builds = None
1317 if isinstance(value, dict):
1318 builds = value
1319 elif isinstance(value, basestring):
1320 builds = ast.literal_eval(value)
1321 if builds:
1322 image = builds.get('cros-version')
1323 if not image and hostless and job.control_file:
1324 try:
1325 control_obj = control_data.parse_control_string(
1326 job.control_file)
1327 if hasattr(control_obj, 'build'):
1328 image = getattr(control_obj, 'build')
1329 if not image and hasattr(control_obj, 'builds'):
1330 builds = getattr(control_obj, 'builds')
1331 image = builds.get('cros-version')
1332 except:
1333 logging.warning('Failed to parse control file for job: %s',
1334 job.name)
1335 return image
1336
showard34dc5fa2008-04-24 20:58:40 +00001337
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001338def get_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001339 """\
showardc92da832009-04-07 18:14:34 +00001340 @returns A sequence of nested dictionaries of host and job information.
jadmanski0afbb632008-06-06 21:10:57 +00001341 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001342 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1343 'started_on__lte',
1344 start_time,
1345 end_time,
1346 **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001347 return rpc_utils.prepare_rows_as_nested_dicts(
1348 models.HostQueueEntry.query_objects(filter_data),
1349 ('host', 'atomic_group', 'job'))
showard34dc5fa2008-04-24 20:58:40 +00001350
1351
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001352def get_num_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001353 """\
1354 Get the number of host queue entries associated with this job.
1355 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001356 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1357 'started_on__lte',
1358 start_time,
1359 end_time,
1360 **filter_data)
jadmanski0afbb632008-06-06 21:10:57 +00001361 return models.HostQueueEntry.query_count(filter_data)
showard34dc5fa2008-04-24 20:58:40 +00001362
1363
showard1e935f12008-07-11 00:11:36 +00001364def get_hqe_percentage_complete(**filter_data):
1365 """
showardc92da832009-04-07 18:14:34 +00001366 Computes the fraction of host queue entries matching the given filter data
showard1e935f12008-07-11 00:11:36 +00001367 that are complete.
1368 """
1369 query = models.HostQueueEntry.query_objects(filter_data)
1370 complete_count = query.filter(complete=True).count()
1371 total_count = query.count()
1372 if total_count == 0:
1373 return 1
1374 return float(complete_count) / total_count
1375
1376
showard1a5a4082009-07-28 20:01:37 +00001377# special tasks
1378
1379def get_special_tasks(**filter_data):
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001380 """Get special task entries from the local database.
1381
1382 Query the special tasks table for tasks matching the given
1383 `filter_data`, and return a list of the results. No attempt is
1384 made to forward the call to shards; the buck will stop here.
1385 The caller is expected to know the target shard for such reasons
1386 as:
1387 * The caller is a service (such as gs_offloader) configured
1388 to operate on behalf of one specific shard, and no other.
1389 * The caller has a host as a parameter, and knows that this is
1390 the shard assigned to that host.
1391
1392 @param filter_data Filter keywords to pass to the underlying
1393 database query.
1394
1395 """
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001396 return rpc_utils.prepare_rows_as_nested_dicts(
1397 models.SpecialTask.query_objects(filter_data),
1398 ('host', 'queue_entry'))
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001399
1400
1401def get_host_special_tasks(host_id, **filter_data):
1402 """Get special task entries for a given host.
1403
1404 Query the special tasks table for tasks that ran on the host
1405 given by `host_id` and matching the given `filter_data`.
1406 Return a list of the results. If the host is assigned to a
1407 shard, forward this call to that shard.
1408
1409 @param host_id Id in the database of the target host.
1410 @param filter_data Filter keywords to pass to the underlying
1411 database query.
1412
1413 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001414 # Retrieve host data even if the host is in an invalid state.
1415 host = models.Host.smart_get(host_id, False)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001416 if not host.shard:
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001417 return get_special_tasks(host_id=host_id, **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001418 else:
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001419 # The return values from AFE methods are post-processed
1420 # objects that aren't JSON-serializable. So, we have to
1421 # call AFE.run() to get the raw, serializable output from
1422 # the shard.
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001423 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1424 return shard_afe.run('get_special_tasks',
1425 host_id=host_id, **filter_data)
showard1a5a4082009-07-28 20:01:37 +00001426
1427
MK Ryu0c1a37d2015-04-30 12:00:55 -07001428def get_num_special_tasks(**kwargs):
1429 """Get the number of special task entries from the local database.
1430
1431 Query the special tasks table for tasks matching the given 'kwargs',
1432 and return the number of the results. No attempt is made to forward
1433 the call to shards; the buck will stop here.
1434
1435 @param kwargs Filter keywords to pass to the underlying database query.
1436
1437 """
1438 return models.SpecialTask.query_count(kwargs)
1439
1440
1441def get_host_num_special_tasks(host, **kwargs):
1442 """Get special task entries for a given host.
1443
1444 Query the special tasks table for tasks that ran on the host
1445 given by 'host' and matching the given 'kwargs'.
1446 Return a list of the results. If the host is assigned to a
1447 shard, forward this call to that shard.
1448
1449 @param host id or name of a host. More often a hostname.
1450 @param kwargs Filter keywords to pass to the underlying database query.
1451
1452 """
1453 # Retrieve host data even if the host is in an invalid state.
1454 host_model = models.Host.smart_get(host, False)
1455 if not host_model.shard:
1456 return get_num_special_tasks(host=host, **kwargs)
1457 else:
1458 shard_afe = frontend.AFE(server=host_model.shard.rpc_hostname())
1459 return shard_afe.run('get_num_special_tasks', host=host, **kwargs)
1460
1461
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001462def get_status_task(host_id, end_time):
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001463 """Get the "status task" for a host from the local shard.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001464
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001465 Returns a single special task representing the given host's
1466 "status task". The status task is a completed special task that
1467 identifies whether the corresponding host was working or broken
1468 when it completed. A successful task indicates a working host;
1469 a failed task indicates broken.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001470
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001471 This call will not be forward to a shard; the receiving server
1472 must be the shard that owns the host.
1473
1474 @param host_id Id in the database of the target host.
1475 @param end_time Time reference for the host's status.
1476
1477 @return A single task; its status (successful or not)
1478 corresponds to the status of the host (working or
1479 broken) at the given time. If no task is found, return
1480 `None`.
1481
1482 """
1483 tasklist = rpc_utils.prepare_rows_as_nested_dicts(
1484 status_history.get_status_task(host_id, end_time),
1485 ('host', 'queue_entry'))
1486 return tasklist[0] if tasklist else None
1487
1488
1489def get_host_status_task(host_id, end_time):
1490 """Get the "status task" for a host from its owning shard.
1491
1492 Finds the given host's owning shard, and forwards to it a call
1493 to `get_status_task()` (see above).
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001494
1495 @param host_id Id in the database of the target host.
1496 @param end_time Time reference for the host's status.
1497
1498 @return A single task; its status (successful or not)
1499 corresponds to the status of the host (working or
1500 broken) at the given time. If no task is found, return
1501 `None`.
1502
1503 """
1504 host = models.Host.smart_get(host_id)
1505 if not host.shard:
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001506 return get_status_task(host_id, end_time)
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001507 else:
1508 # The return values from AFE methods are post-processed
1509 # objects that aren't JSON-serializable. So, we have to
1510 # call AFE.run() to get the raw, serializable output from
1511 # the shard.
1512 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1513 return shard_afe.run('get_status_task',
1514 host_id=host_id, end_time=end_time)
1515
1516
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001517def get_host_diagnosis_interval(host_id, end_time, success):
1518 """Find a "diagnosis interval" for a given host.
1519
1520 A "diagnosis interval" identifies a start and end time where
1521 the host went from "working" to "broken", or vice versa. The
1522 interval's starting time is the starting time of the last status
1523 task with the old status; the end time is the finish time of the
1524 first status task with the new status.
1525
1526 This routine finds the most recent diagnosis interval for the
1527 given host prior to `end_time`, with a starting status matching
1528 `success`. If `success` is true, the interval will start with a
1529 successful status task; if false the interval will start with a
1530 failed status task.
1531
1532 @param host_id Id in the database of the target host.
1533 @param end_time Time reference for the diagnosis interval.
1534 @param success Whether the diagnosis interval should start
1535 with a successful or failed status task.
1536
1537 @return A list of two strings. The first is the timestamp for
1538 the beginning of the interval; the second is the
1539 timestamp for the end. If the host has never changed
1540 state, the list is empty.
1541
1542 """
1543 host = models.Host.smart_get(host_id)
J. Richard Barnette78f281a2015-06-29 13:24:51 -07001544 if not host.shard or utils.is_shard():
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001545 return status_history.get_diagnosis_interval(
1546 host_id, end_time, success)
1547 else:
1548 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1549 return shard_afe.get_host_diagnosis_interval(
1550 host_id, end_time, success)
1551
1552
showardc0ac3a72009-07-08 21:14:45 +00001553# support for host detail view
1554
MK Ryu0c1a37d2015-04-30 12:00:55 -07001555def get_host_queue_entries_and_special_tasks(host, query_start=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001556 query_limit=None, start_time=None,
1557 end_time=None):
showardc0ac3a72009-07-08 21:14:45 +00001558 """
1559 @returns an interleaved list of HostQueueEntries and SpecialTasks,
1560 in approximate run order. each dict contains keys for type, host,
1561 job, status, started_on, execution_path, and ID.
1562 """
1563 total_limit = None
1564 if query_limit is not None:
1565 total_limit = query_start + query_limit
MK Ryu0c1a37d2015-04-30 12:00:55 -07001566 filter_data_common = {'host': host,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001567 'query_limit': total_limit,
1568 'sort_by': ['-id']}
showardc0ac3a72009-07-08 21:14:45 +00001569
MK Ryu0c1a37d2015-04-30 12:00:55 -07001570 filter_data_special_tasks = rpc_utils.inject_times_to_filter(
1571 'time_started__gte', 'time_started__lte', start_time, end_time,
1572 **filter_data_common)
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001573
MK Ryu0c1a37d2015-04-30 12:00:55 -07001574 queue_entries = get_host_queue_entries(
1575 start_time, end_time, **filter_data_common)
1576 special_tasks = get_host_special_tasks(host, **filter_data_special_tasks)
showardc0ac3a72009-07-08 21:14:45 +00001577
1578 interleaved_entries = rpc_utils.interleave_entries(queue_entries,
1579 special_tasks)
1580 if query_start is not None:
1581 interleaved_entries = interleaved_entries[query_start:]
1582 if query_limit is not None:
1583 interleaved_entries = interleaved_entries[:query_limit]
MK Ryu0c1a37d2015-04-30 12:00:55 -07001584 return rpc_utils.prepare_host_queue_entries_and_special_tasks(
1585 interleaved_entries, queue_entries)
showardc0ac3a72009-07-08 21:14:45 +00001586
1587
MK Ryu0c1a37d2015-04-30 12:00:55 -07001588def get_num_host_queue_entries_and_special_tasks(host, start_time=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001589 end_time=None):
MK Ryu0c1a37d2015-04-30 12:00:55 -07001590 filter_data_common = {'host': host}
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001591
1592 filter_data_queue_entries, filter_data_special_tasks = (
1593 rpc_utils.inject_times_to_hqe_special_tasks_filters(
1594 filter_data_common, start_time, end_time))
1595
1596 return (models.HostQueueEntry.query_count(filter_data_queue_entries)
MK Ryu0c1a37d2015-04-30 12:00:55 -07001597 + get_host_num_special_tasks(**filter_data_special_tasks))
showardc0ac3a72009-07-08 21:14:45 +00001598
1599
showard29f7cd22009-04-29 21:16:24 +00001600# recurring run
1601
1602def get_recurring(**filter_data):
1603 return rpc_utils.prepare_rows_as_nested_dicts(
1604 models.RecurringRun.query_objects(filter_data),
1605 ('job', 'owner'))
1606
1607
1608def get_num_recurring(**filter_data):
1609 return models.RecurringRun.query_count(filter_data)
1610
1611
1612def delete_recurring_runs(**filter_data):
1613 to_delete = models.RecurringRun.query_objects(filter_data)
1614 to_delete.delete()
1615
1616
1617def create_recurring_run(job_id, start_date, loop_period, loop_count):
showard64a95952010-01-13 21:27:16 +00001618 owner = models.User.current_user().login
showard29f7cd22009-04-29 21:16:24 +00001619 job = models.Job.objects.get(id=job_id)
1620 return job.create_recurring_job(start_date=start_date,
1621 loop_period=loop_period,
1622 loop_count=loop_count,
1623 owner=owner)
1624
1625
mblighe8819cd2008-02-15 16:48:40 +00001626# other
1627
showarde0b63622008-08-04 20:58:47 +00001628def echo(data=""):
1629 """\
1630 Returns a passed in string. For doing a basic test to see if RPC calls
1631 can successfully be made.
1632 """
1633 return data
1634
1635
showardb7a52fd2009-04-27 20:10:56 +00001636def get_motd():
1637 """\
1638 Returns the message of the day as a string.
1639 """
1640 return rpc_utils.get_motd()
1641
1642
mblighe8819cd2008-02-15 16:48:40 +00001643def get_static_data():
jadmanski0afbb632008-06-06 21:10:57 +00001644 """\
1645 Returns a dictionary containing a bunch of data that shouldn't change
1646 often and is otherwise inaccessible. This includes:
showardc92da832009-04-07 18:14:34 +00001647
1648 priorities: List of job priority choices.
1649 default_priority: Default priority value for new jobs.
1650 users: Sorted list of all users.
Jiaxi Luo31874592014-06-11 10:36:35 -07001651 labels: Sorted list of labels not start with 'cros-version' and
1652 'fw-version'.
showardc92da832009-04-07 18:14:34 +00001653 atomic_groups: Sorted list of all atomic groups.
1654 tests: Sorted list of all tests.
1655 profilers: Sorted list of all profilers.
1656 current_user: Logged-in username.
1657 host_statuses: Sorted list of possible Host statuses.
1658 job_statuses: Sorted list of possible HostQueueEntry statuses.
Simran Basi7e605742013-11-12 13:43:36 -08001659 job_timeout_default: The default job timeout length in minutes.
showarda1e74b32009-05-12 17:32:04 +00001660 parse_failed_repair_default: Default value for the parse_failed_repair job
Jiaxi Luo31874592014-06-11 10:36:35 -07001661 option.
showardc92da832009-04-07 18:14:34 +00001662 reboot_before_options: A list of valid RebootBefore string enums.
1663 reboot_after_options: A list of valid RebootAfter string enums.
1664 motd: Server's message of the day.
1665 status_dictionary: A mapping from one word job status names to a more
1666 informative description.
jadmanski0afbb632008-06-06 21:10:57 +00001667 """
showard21baa452008-10-21 00:08:39 +00001668
1669 job_fields = models.Job.get_field_dict()
jamesren76fcf192010-04-21 20:39:50 +00001670 default_drone_set_name = models.DroneSet.default_drone_set_name()
1671 drone_sets = ([default_drone_set_name] +
1672 sorted(drone_set.name for drone_set in
1673 models.DroneSet.objects.exclude(
1674 name=default_drone_set_name)))
showard21baa452008-10-21 00:08:39 +00001675
jadmanski0afbb632008-06-06 21:10:57 +00001676 result = {}
Alex Miller7d658cf2013-09-04 16:00:35 -07001677 result['priorities'] = priorities.Priority.choices()
1678 default_priority = priorities.Priority.DEFAULT
1679 result['default_priority'] = 'Default'
1680 result['max_schedulable_priority'] = priorities.Priority.DEFAULT
jadmanski0afbb632008-06-06 21:10:57 +00001681 result['users'] = get_users(sort_by=['login'])
Jiaxi Luo31874592014-06-11 10:36:35 -07001682
1683 label_exclude_filters = [{'name__startswith': 'cros-version'},
Dan Shi65351d62015-08-03 12:03:23 -07001684 {'name__startswith': 'fw-version'},
1685 {'name__startswith': 'fwrw-version'},
Dan Shi27516972016-03-16 14:03:41 -07001686 {'name__startswith': 'fwro-version'},
1687 {'name__startswith': 'ab-version'},
1688 {'name__startswith': 'testbed-version'}]
Jiaxi Luo31874592014-06-11 10:36:35 -07001689 result['labels'] = get_labels(
1690 label_exclude_filters,
1691 sort_by=['-platform', 'name'])
1692
showardc92da832009-04-07 18:14:34 +00001693 result['atomic_groups'] = get_atomic_groups(sort_by=['name'])
jadmanski0afbb632008-06-06 21:10:57 +00001694 result['tests'] = get_tests(sort_by=['name'])
showard2b9a88b2008-06-13 20:55:03 +00001695 result['profilers'] = get_profilers(sort_by=['name'])
showard0fc38302008-10-23 00:44:07 +00001696 result['current_user'] = rpc_utils.prepare_for_serialization(
showard64a95952010-01-13 21:27:16 +00001697 models.User.current_user().get_object_dict())
showard2b9a88b2008-06-13 20:55:03 +00001698 result['host_statuses'] = sorted(models.Host.Status.names)
mbligh5a198b92008-12-11 19:33:29 +00001699 result['job_statuses'] = sorted(models.HostQueueEntry.Status.names)
Simran Basi7e605742013-11-12 13:43:36 -08001700 result['job_timeout_mins_default'] = models.Job.DEFAULT_TIMEOUT_MINS
Simran Basi34217022012-11-06 13:43:15 -08001701 result['job_max_runtime_mins_default'] = (
1702 models.Job.DEFAULT_MAX_RUNTIME_MINS)
showarda1e74b32009-05-12 17:32:04 +00001703 result['parse_failed_repair_default'] = bool(
1704 models.Job.DEFAULT_PARSE_FAILED_REPAIR)
jamesrendd855242010-03-02 22:23:44 +00001705 result['reboot_before_options'] = model_attributes.RebootBefore.names
1706 result['reboot_after_options'] = model_attributes.RebootAfter.names
showard8fbae652009-01-20 23:23:10 +00001707 result['motd'] = rpc_utils.get_motd()
jamesren76fcf192010-04-21 20:39:50 +00001708 result['drone_sets_enabled'] = models.DroneSet.drone_sets_enabled()
1709 result['drone_sets'] = drone_sets
jamesren4a41e012010-07-16 22:33:48 +00001710 result['parameterized_jobs'] = models.Job.parameterized_jobs_enabled()
showard8ac29b42008-07-17 17:01:55 +00001711
showardd3dc1992009-04-22 21:01:40 +00001712 result['status_dictionary'] = {"Aborted": "Aborted",
showard8ac29b42008-07-17 17:01:55 +00001713 "Verifying": "Verifying Host",
Alex Millerdfff2fd2013-05-28 13:05:06 -07001714 "Provisioning": "Provisioning Host",
showard8ac29b42008-07-17 17:01:55 +00001715 "Pending": "Waiting on other hosts",
1716 "Running": "Running autoserv",
1717 "Completed": "Autoserv completed",
1718 "Failed": "Failed to complete",
showardd823b362008-07-24 16:35:46 +00001719 "Queued": "Queued",
showard5deb6772008-11-04 21:54:33 +00001720 "Starting": "Next in host's queue",
1721 "Stopped": "Other host(s) failed verify",
showardd3dc1992009-04-22 21:01:40 +00001722 "Parsing": "Awaiting parse of final results",
showard29f7cd22009-04-29 21:16:24 +00001723 "Gathering": "Gathering log files",
showard8cc058f2009-09-08 16:26:33 +00001724 "Template": "Template job for recurring run",
mbligh4608b002010-01-05 18:22:35 +00001725 "Waiting": "Waiting for scheduler action",
Dan Shi07e09af2013-04-12 09:31:29 -07001726 "Archiving": "Archiving results",
1727 "Resetting": "Resetting hosts"}
Jiaxi Luo421608e2014-07-07 14:38:00 -07001728
1729 result['wmatrix_url'] = rpc_utils.get_wmatrix_url()
Simran Basi71206ef2014-08-13 13:51:18 -07001730 result['is_moblab'] = bool(utils.is_moblab())
Jiaxi Luo421608e2014-07-07 14:38:00 -07001731
jadmanski0afbb632008-06-06 21:10:57 +00001732 return result
showard29f7cd22009-04-29 21:16:24 +00001733
1734
1735def get_server_time():
1736 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
Kevin Cheng19521982016-09-22 12:27:23 -07001737
1738
1739def get_hosts_by_attribute(attribute, value):
1740 """
1741 Get the list of valid hosts that share the same host attribute value.
1742
1743 @param attribute: String of the host attribute to check.
1744 @param value: String of the value that is shared between hosts.
1745
1746 @returns List of hostnames that all have the same host attribute and
1747 value.
1748 """
1749 hosts = models.HostAttribute.query_objects({'attribute': attribute,
1750 'value': value})
1751 return [row.host.hostname for row in hosts if row.host.invalid == 0]