blob: 519987a81e32fb975b4a3c1a9a44ed1d5cffe7d7 [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
showard29f7cd22009-04-29 21:16:24 +000035import datetime
Shuqian Zhao4c0d2902016-01-12 17:03:15 -080036import logging
Dan Shi4a3deb82016-10-27 21:32:30 -070037import sys
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:'):
Dan Shib5b8b4f2016-11-02 14:04:02 -0700148 models.Host.check_board_labels_allowed(host_objs, [label.name])
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:')]
Dan Shib5b8b4f2016-11-02 14:04:02 -0700446 if len(platforms) > 1 or not utils.board_labels_allowed(boards):
showardcafd16e2009-05-29 18:37:49 +0000447 raise model_logic.ValidationError(
Dan Shib5b8b4f2016-11-02 14:04:02 -0700448 {'labels': ('Adding more than one platform label, or a list of '
449 'non-compatible board labels.: %s %s' %
450 (', '.join(platforms), ', '.join(boards)))})
MK Ryufbb002c2015-06-08 14:13:16 -0700451
452 host_obj = models.Host.smart_get(id)
Dan Shi4a3deb82016-10-27 21:32:30 -0700453 if platforms:
MK Ryufbb002c2015-06-08 14:13:16 -0700454 models.Host.check_no_platform([host_obj])
Dan Shi4a3deb82016-10-27 21:32:30 -0700455 if boards:
Dan Shib5b8b4f2016-11-02 14:04:02 -0700456 models.Host.check_board_labels_allowed([host_obj], labels)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800457 add_labels_to_host(id, labels)
MK Ryufbb002c2015-06-08 14:13:16 -0700458
459 rpc_utils.fanout_rpc([host_obj], 'add_labels_to_host', False,
460 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000461
462
MK Ryufbb002c2015-06-08 14:13:16 -0700463def remove_labels_from_host(id, labels):
464 """Removes labels from a given host only in local DB.
465
466 @param id: id or hostname for a host.
467 @param labels: ids or names for labels.
468 """
469 label_objs = models.Label.smart_get_bulk(labels)
470 models.Host.smart_get(id).labels.remove(*label_objs)
471
472
473@rpc_utils.route_rpc_to_master
mblighe8819cd2008-02-15 16:48:40 +0000474def host_remove_labels(id, labels):
MK Ryufbb002c2015-06-08 14:13:16 -0700475 """Removes labels from a given host.
476
477 @param id: id or hostname for a host.
478 @param labels: ids or names for labels.
479 """
MK Ryu8e2c2d02016-01-06 15:24:38 -0800480 remove_labels_from_host(id, labels)
481
MK Ryufbb002c2015-06-08 14:13:16 -0700482 host_obj = models.Host.smart_get(id)
483 rpc_utils.fanout_rpc([host_obj], 'remove_labels_from_host', False,
484 id=id, labels=labels)
mblighe8819cd2008-02-15 16:48:40 +0000485
486
MK Ryuacf35922014-10-03 14:56:49 -0700487def get_host_attribute(attribute, **host_filter_data):
488 """
489 @param attribute: string name of attribute
490 @param host_filter_data: filter data to apply to Hosts to choose hosts to
491 act upon
492 """
493 hosts = rpc_utils.get_host_query((), False, False, True, host_filter_data)
494 hosts = list(hosts)
495 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
496 'attribute_list')
497 host_attr_dicts = []
498 for host_obj in hosts:
499 for attr_obj in host_obj.attribute_list:
500 if attr_obj.attribute == attribute:
501 host_attr_dicts.append(attr_obj.get_object_dict())
502 return rpc_utils.prepare_for_serialization(host_attr_dicts)
503
504
showard0957a842009-05-11 19:25:08 +0000505def set_host_attribute(attribute, value, **host_filter_data):
506 """
MK Ryu26f0c932015-05-28 18:14:33 -0700507 @param attribute: string name of attribute
508 @param value: string, or None to delete an attribute
509 @param host_filter_data: filter data to apply to Hosts to choose hosts to
510 act upon
showard0957a842009-05-11 19:25:08 +0000511 """
512 assert host_filter_data # disallow accidental actions on all hosts
513 hosts = models.Host.query_objects(host_filter_data)
514 models.AclGroup.check_for_acl_violation_hosts(hosts)
MK Ryu8e2c2d02016-01-06 15:24:38 -0800515 for host in hosts:
516 host.set_or_delete_attribute(attribute, value)
showard0957a842009-05-11 19:25:08 +0000517
MK Ryu26f0c932015-05-28 18:14:33 -0700518 # Master forwards this RPC to shards.
519 if not utils.is_shard():
520 rpc_utils.fanout_rpc(hosts, 'set_host_attribute', False,
521 attribute=attribute, value=value, **host_filter_data)
522
showard0957a842009-05-11 19:25:08 +0000523
Jakob Juelich50e91f72014-10-01 12:43:23 -0700524@rpc_utils.forward_single_host_rpc_to_shard
mblighe8819cd2008-02-15 16:48:40 +0000525def delete_host(id):
jadmanski0afbb632008-06-06 21:10:57 +0000526 models.Host.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000527
528
showard87cc38f2009-08-20 23:37:04 +0000529def get_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
Dan Shi37df54d2015-12-14 11:16:28 -0800530 exclude_atomic_group_hosts=False, valid_only=True,
531 include_current_job=False, **filter_data):
532 """Get a list of dictionaries which contains the information of hosts.
533
showard87cc38f2009-08-20 23:37:04 +0000534 @param multiple_labels: match hosts in all of the labels given. Should
535 be a list of label names.
536 @param exclude_only_if_needed_labels: Exclude hosts with at least one
537 "only_if_needed" label applied.
538 @param exclude_atomic_group_hosts: Exclude hosts that have one or more
539 atomic group labels associated with them.
Dan Shi37df54d2015-12-14 11:16:28 -0800540 @param include_current_job: Set to True to include ids of currently running
541 job and special task.
jadmanski0afbb632008-06-06 21:10:57 +0000542 """
showard43a3d262008-11-12 18:17:05 +0000543 hosts = rpc_utils.get_host_query(multiple_labels,
544 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000545 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000546 valid_only, filter_data)
showard0957a842009-05-11 19:25:08 +0000547 hosts = list(hosts)
548 models.Host.objects.populate_relationships(hosts, models.Label,
549 'label_list')
550 models.Host.objects.populate_relationships(hosts, models.AclGroup,
551 'acl_list')
552 models.Host.objects.populate_relationships(hosts, models.HostAttribute,
553 'attribute_list')
showard43a3d262008-11-12 18:17:05 +0000554 host_dicts = []
555 for host_obj in hosts:
556 host_dict = host_obj.get_object_dict()
showard0957a842009-05-11 19:25:08 +0000557 host_dict['labels'] = [label.name for label in host_obj.label_list]
showard909c9142009-07-07 20:54:42 +0000558 host_dict['platform'], host_dict['atomic_group'] = (rpc_utils.
559 find_platform_and_atomic_group(host_obj))
showard0957a842009-05-11 19:25:08 +0000560 host_dict['acls'] = [acl.name for acl in host_obj.acl_list]
561 host_dict['attributes'] = dict((attribute.attribute, attribute.value)
562 for attribute in host_obj.attribute_list)
Dan Shi37df54d2015-12-14 11:16:28 -0800563 if include_current_job:
564 host_dict['current_job'] = None
565 host_dict['current_special_task'] = None
566 entries = models.HostQueueEntry.objects.filter(
567 host_id=host_dict['id'], active=True, complete=False)
568 if entries:
569 host_dict['current_job'] = (
570 entries[0].get_object_dict()['job'])
571 tasks = models.SpecialTask.objects.filter(
572 host_id=host_dict['id'], is_active=True, is_complete=False)
573 if tasks:
574 host_dict['current_special_task'] = (
575 '%d-%s' % (tasks[0].get_object_dict()['id'],
576 tasks[0].get_object_dict()['task'].lower()))
showard43a3d262008-11-12 18:17:05 +0000577 host_dicts.append(host_dict)
578 return rpc_utils.prepare_for_serialization(host_dicts)
mblighe8819cd2008-02-15 16:48:40 +0000579
580
showard87cc38f2009-08-20 23:37:04 +0000581def get_num_hosts(multiple_labels=(), exclude_only_if_needed_labels=False,
showard8aa84fc2009-09-16 17:17:55 +0000582 exclude_atomic_group_hosts=False, valid_only=True,
583 **filter_data):
showard87cc38f2009-08-20 23:37:04 +0000584 """
585 Same parameters as get_hosts().
586
587 @returns The number of matching hosts.
588 """
showard43a3d262008-11-12 18:17:05 +0000589 hosts = rpc_utils.get_host_query(multiple_labels,
590 exclude_only_if_needed_labels,
showard87cc38f2009-08-20 23:37:04 +0000591 exclude_atomic_group_hosts,
showard8aa84fc2009-09-16 17:17:55 +0000592 valid_only, filter_data)
showard43a3d262008-11-12 18:17:05 +0000593 return hosts.count()
showard1385b162008-03-13 15:59:40 +0000594
mblighe8819cd2008-02-15 16:48:40 +0000595
596# tests
597
showard909c7a62008-07-15 21:52:38 +0000598def add_test(name, test_type, path, author=None, dependencies=None,
showard3d9899a2008-07-31 02:11:58 +0000599 experimental=True, run_verify=None, test_class=None,
showard909c7a62008-07-15 21:52:38 +0000600 test_time=None, test_category=None, description=None,
601 sync_count=1):
jadmanski0afbb632008-06-06 21:10:57 +0000602 return models.Test.add_object(name=name, test_type=test_type, path=path,
showard909c7a62008-07-15 21:52:38 +0000603 author=author, dependencies=dependencies,
604 experimental=experimental,
605 run_verify=run_verify, test_time=test_time,
606 test_category=test_category,
607 sync_count=sync_count,
jadmanski0afbb632008-06-06 21:10:57 +0000608 test_class=test_class,
609 description=description).id
mblighe8819cd2008-02-15 16:48:40 +0000610
611
612def modify_test(id, **data):
jadmanski0afbb632008-06-06 21:10:57 +0000613 models.Test.smart_get(id).update_object(data)
mblighe8819cd2008-02-15 16:48:40 +0000614
615
616def delete_test(id):
jadmanski0afbb632008-06-06 21:10:57 +0000617 models.Test.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000618
619
620def get_tests(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000621 return rpc_utils.prepare_for_serialization(
622 models.Test.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000623
624
Moises Osorio2dc7a102014-12-02 18:24:02 -0800625@_timer.decorate
626def get_tests_status_counts_by_job_name_label(job_name_prefix, label_name):
627 """Gets the counts of all passed and failed tests from the matching jobs.
628
629 @param job_name_prefix: Name prefix of the jobs to get the summary from, e.g.,
630 'butterfly-release/R40-6457.21.0/bvt-cq/'.
631 @param label_name: Label that must be set in the jobs, e.g.,
632 'cros-version:butterfly-release/R40-6457.21.0'.
633
634 @returns A summary of the counts of all the passed and failed tests.
635 """
636 job_ids = list(models.Job.objects.filter(
637 name__startswith=job_name_prefix,
638 dependency_labels__name=label_name).values_list(
639 'pk', flat=True))
640 summary = {'passed': 0, 'failed': 0}
641 if not job_ids:
642 return summary
643
644 counts = (tko_models.TestView.objects.filter(
645 afe_job_id__in=job_ids).exclude(
646 test_name='SERVER_JOB').exclude(
647 test_name__startswith='CLIENT_JOB').values(
648 'status').annotate(
649 count=Count('status')))
650 for status in counts:
651 if status['status'] == 'GOOD':
652 summary['passed'] += status['count']
653 else:
654 summary['failed'] += status['count']
655 return summary
656
657
showard2b9a88b2008-06-13 20:55:03 +0000658# profilers
659
660def add_profiler(name, description=None):
661 return models.Profiler.add_object(name=name, description=description).id
662
663
664def modify_profiler(id, **data):
665 models.Profiler.smart_get(id).update_object(data)
666
667
668def delete_profiler(id):
669 models.Profiler.smart_get(id).delete()
670
671
672def get_profilers(**filter_data):
673 return rpc_utils.prepare_for_serialization(
674 models.Profiler.list_objects(filter_data))
675
676
mblighe8819cd2008-02-15 16:48:40 +0000677# users
678
679def add_user(login, access_level=None):
jadmanski0afbb632008-06-06 21:10:57 +0000680 return models.User.add_object(login=login, access_level=access_level).id
mblighe8819cd2008-02-15 16:48:40 +0000681
682
683def modify_user(id, **data):
jadmanski0afbb632008-06-06 21:10:57 +0000684 models.User.smart_get(id).update_object(data)
mblighe8819cd2008-02-15 16:48:40 +0000685
686
687def delete_user(id):
jadmanski0afbb632008-06-06 21:10:57 +0000688 models.User.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000689
690
691def get_users(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000692 return rpc_utils.prepare_for_serialization(
693 models.User.list_objects(filter_data))
mblighe8819cd2008-02-15 16:48:40 +0000694
695
696# acl groups
697
698def add_acl_group(name, description=None):
showard04f2cd82008-07-25 20:53:31 +0000699 group = models.AclGroup.add_object(name=name, description=description)
showard64a95952010-01-13 21:27:16 +0000700 group.users.add(models.User.current_user())
showard04f2cd82008-07-25 20:53:31 +0000701 return group.id
mblighe8819cd2008-02-15 16:48:40 +0000702
703
704def modify_acl_group(id, **data):
showard04f2cd82008-07-25 20:53:31 +0000705 group = models.AclGroup.smart_get(id)
706 group.check_for_acl_violation_acl_group()
707 group.update_object(data)
708 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000709
710
711def acl_group_add_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000712 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000713 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000714 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000715 group.users.add(*users)
mblighe8819cd2008-02-15 16:48:40 +0000716
717
718def acl_group_remove_users(id, users):
jadmanski0afbb632008-06-06 21:10:57 +0000719 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000720 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000721 users = models.User.smart_get_bulk(users)
jadmanski0afbb632008-06-06 21:10:57 +0000722 group.users.remove(*users)
showard04f2cd82008-07-25 20:53:31 +0000723 group.add_current_user_if_empty()
mblighe8819cd2008-02-15 16:48:40 +0000724
725
726def acl_group_add_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000727 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000728 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000729 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000730 group.hosts.add(*hosts)
showard08f981b2008-06-24 21:59:03 +0000731 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000732
733
734def acl_group_remove_hosts(id, hosts):
jadmanski0afbb632008-06-06 21:10:57 +0000735 group = models.AclGroup.smart_get(id)
showard04f2cd82008-07-25 20:53:31 +0000736 group.check_for_acl_violation_acl_group()
showardbe3ec042008-11-12 18:16:07 +0000737 hosts = models.Host.smart_get_bulk(hosts)
jadmanski0afbb632008-06-06 21:10:57 +0000738 group.hosts.remove(*hosts)
showard08f981b2008-06-24 21:59:03 +0000739 group.on_host_membership_change()
mblighe8819cd2008-02-15 16:48:40 +0000740
741
742def delete_acl_group(id):
jadmanski0afbb632008-06-06 21:10:57 +0000743 models.AclGroup.smart_get(id).delete()
mblighe8819cd2008-02-15 16:48:40 +0000744
745
746def get_acl_groups(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +0000747 acl_groups = models.AclGroup.list_objects(filter_data)
748 for acl_group in acl_groups:
749 acl_group_obj = models.AclGroup.objects.get(id=acl_group['id'])
750 acl_group['users'] = [user.login
751 for user in acl_group_obj.users.all()]
752 acl_group['hosts'] = [host.hostname
753 for host in acl_group_obj.hosts.all()]
754 return rpc_utils.prepare_for_serialization(acl_groups)
mblighe8819cd2008-02-15 16:48:40 +0000755
756
757# jobs
758
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700759def generate_control_file(tests=(), profilers=(),
showard91f85102009-10-12 20:34:52 +0000760 client_control_file='', use_container=False,
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700761 profile_only=None, db_tests=True,
762 test_source_build=None):
jadmanski0afbb632008-06-06 21:10:57 +0000763 """
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700764 Generates a client-side control file to run tests.
mbligh120351e2009-01-24 01:40:45 +0000765
Matthew Sartori10438092015-06-24 14:30:18 -0700766 @param tests List of tests to run. See db_tests for more information.
mbligh120351e2009-01-24 01:40:45 +0000767 @param profilers List of profilers to activate during the job.
768 @param client_control_file The contents of a client-side control file to
769 run at the end of all tests. If this is supplied, all tests must be
770 client side.
771 TODO: in the future we should support server control files directly
772 to wrap with a kernel. That'll require changing the parameter
773 name and adding a boolean to indicate if it is a client or server
774 control file.
775 @param use_container unused argument today. TODO: Enable containers
776 on the host during a client side test.
showard91f85102009-10-12 20:34:52 +0000777 @param profile_only A boolean that indicates what default profile_only
778 mode to use in the control file. Passing None will generate a
779 control file that does not explcitly set the default mode at all.
Matthew Sartori10438092015-06-24 14:30:18 -0700780 @param db_tests: if True, the test object can be found in the database
781 backing the test model. In this case, tests is a tuple
782 of test IDs which are used to retrieve the test objects
783 from the database. If False, tests is a tuple of test
784 dictionaries stored client-side in the AFE.
Michael Tang84a2ecf2016-06-07 15:10:53 -0700785 @param test_source_build: Build to be used to retrieve test code. Default
786 to None.
mbligh120351e2009-01-24 01:40:45 +0000787
788 @returns a dict with the following keys:
789 control_file: str, The control file text.
790 is_server: bool, is the control file a server-side control file?
791 synch_count: How many machines the job uses per autoserv execution.
792 synch_count == 1 means the job is asynchronous.
793 dependencies: A list of the names of labels on which the job depends.
794 """
showardd86debe2009-06-10 17:37:56 +0000795 if not tests and not client_control_file:
showard2bab8f42008-11-12 18:15:22 +0000796 return dict(control_file='', is_server=False, synch_count=1,
showard989f25d2008-10-01 11:38:11 +0000797 dependencies=[])
mblighe8819cd2008-02-15 16:48:40 +0000798
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700799 cf_info, test_objects, profiler_objects = (
800 rpc_utils.prepare_generate_control_file(tests, profilers,
801 db_tests))
showard989f25d2008-10-01 11:38:11 +0000802 cf_info['control_file'] = control_file.generate_control(
Richard Barnette8e33b4e2016-05-21 12:12:26 -0700803 tests=test_objects, profilers=profiler_objects,
804 is_server=cf_info['is_server'],
showard232b7ae2009-11-10 00:46:48 +0000805 client_control_file=client_control_file, profile_only=profile_only,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700806 test_source_build=test_source_build)
showard989f25d2008-10-01 11:38:11 +0000807 return cf_info
mblighe8819cd2008-02-15 16:48:40 +0000808
809
Shuqian Zhao54a5b672016-05-11 22:12:17 +0000810def create_parameterized_job(name, priority, test, parameters, kernel=None,
811 label=None, profilers=(), profiler_parameters=None,
812 use_container=False, profile_only=None,
813 upload_kernel_config=False, hosts=(),
814 meta_hosts=(), one_time_hosts=(),
815 atomic_group_name=None, synch_count=None,
816 is_template=False, timeout=None,
817 timeout_mins=None, max_runtime_mins=None,
818 run_verify=False, email_list='', dependencies=(),
819 reboot_before=None, reboot_after=None,
820 parse_failed_repair=None, hostless=False,
821 keyvals=None, drone_set=None, run_reset=True,
822 require_ssp=None):
823 """
824 Creates and enqueues a parameterized job.
825
826 Most parameters a combination of the parameters for generate_control_file()
827 and create_job(), with the exception of:
828
829 @param test name or ID of the test to run
830 @param parameters a map of parameter name ->
831 tuple of (param value, param type)
832 @param profiler_parameters a dictionary of parameters for the profilers:
833 key: profiler name
834 value: dict of param name -> tuple of
835 (param value,
836 param type)
837 """
838 # Save the values of the passed arguments here. What we're going to do with
839 # them is pass them all to rpc_utils.get_create_job_common_args(), which
840 # will extract the subset of these arguments that apply for
841 # rpc_utils.create_job_common(), which we then pass in to that function.
842 args = locals()
843
844 # Set up the parameterized job configs
845 test_obj = models.Test.smart_get(test)
846 control_type = test_obj.test_type
847
848 try:
849 label = models.Label.smart_get(label)
850 except models.Label.DoesNotExist:
851 label = None
852
853 kernel_objs = models.Kernel.create_kernels(kernel)
854 profiler_objs = [models.Profiler.smart_get(profiler)
855 for profiler in profilers]
856
857 parameterized_job = models.ParameterizedJob.objects.create(
858 test=test_obj, label=label, use_container=use_container,
859 profile_only=profile_only,
860 upload_kernel_config=upload_kernel_config)
861 parameterized_job.kernels.add(*kernel_objs)
862
863 for profiler in profiler_objs:
864 parameterized_profiler = models.ParameterizedJobProfiler.objects.create(
865 parameterized_job=parameterized_job,
866 profiler=profiler)
867 profiler_params = profiler_parameters.get(profiler.name, {})
868 for name, (value, param_type) in profiler_params.iteritems():
869 models.ParameterizedJobProfilerParameter.objects.create(
870 parameterized_job_profiler=parameterized_profiler,
871 parameter_name=name,
872 parameter_value=value,
873 parameter_type=param_type)
874
875 try:
876 for parameter in test_obj.testparameter_set.all():
877 if parameter.name in parameters:
878 param_value, param_type = parameters.pop(parameter.name)
879 parameterized_job.parameterizedjobparameter_set.create(
880 test_parameter=parameter, parameter_value=param_value,
881 parameter_type=param_type)
882
883 if parameters:
884 raise Exception('Extra parameters remain: %r' % parameters)
885
886 return rpc_utils.create_job_common(
887 parameterized_job=parameterized_job.id,
888 control_type=control_type,
889 **rpc_utils.get_create_job_common_args(args))
890 except:
891 parameterized_job.delete()
892 raise
893
894
Simran Basib6ec8ae2014-04-23 12:05:08 -0700895def create_job_page_handler(name, priority, control_file, control_type,
Dan Shid215dbe2015-06-18 16:14:59 -0700896 image=None, hostless=False, firmware_rw_build=None,
897 firmware_ro_build=None, test_source_build=None,
Michael Tang84a2ecf2016-06-07 15:10:53 -0700898 is_cloning=False, **kwargs):
Simran Basib6ec8ae2014-04-23 12:05:08 -0700899 """\
900 Create and enqueue a job.
901
902 @param name name of this job
903 @param priority Integer priority of this job. Higher is more important.
904 @param control_file String contents of the control file.
905 @param control_type Type of control file, Client or Server.
Dan Shid215dbe2015-06-18 16:14:59 -0700906 @param image: ChromeOS build to be installed in the dut. Default to None.
907 @param firmware_rw_build: Firmware build to update RW firmware. Default to
908 None, i.e., RW firmware will not be updated.
909 @param firmware_ro_build: Firmware build to update RO firmware. Default to
910 None, i.e., RO firmware will not be updated.
911 @param test_source_build: Build to be used to retrieve test code. Default
912 to None.
Michael Tang6dc174e2016-05-31 23:13:42 -0700913 @param is_cloning: True if creating a cloning job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700914 @param kwargs extra args that will be required by create_suite_job or
915 create_job.
916
917 @returns The created Job id number.
918 """
Michael Tang6dc174e2016-05-31 23:13:42 -0700919 if is_cloning:
920 logging.info('Start to clone a new job')
Shuqian Zhao61f5d312016-08-05 17:15:23 -0700921 # When cloning a job, hosts and meta_hosts should not exist together,
922 # which would cause host-scheduler to schedule two hqe jobs to one host
923 # at the same time, and crash itself. Clear meta_hosts for this case.
924 if kwargs.get('hosts') and kwargs.get('meta_hosts'):
925 kwargs['meta_hosts'] = []
Michael Tang6dc174e2016-05-31 23:13:42 -0700926 else:
927 logging.info('Start to create a new job')
Simran Basib6ec8ae2014-04-23 12:05:08 -0700928 control_file = rpc_utils.encode_ascii(control_file)
Jiaxi Luodd67beb2014-07-18 16:28:31 -0700929 if not control_file:
930 raise model_logic.ValidationError({
931 'control_file' : "Control file cannot be empty"})
Simran Basib6ec8ae2014-04-23 12:05:08 -0700932
933 if image and hostless:
Dan Shid215dbe2015-06-18 16:14:59 -0700934 builds = {}
935 builds[provision.CROS_VERSION_PREFIX] = image
936 if firmware_rw_build:
Dan Shi0723bf52015-06-24 10:52:38 -0700937 builds[provision.FW_RW_VERSION_PREFIX] = firmware_rw_build
Dan Shid215dbe2015-06-18 16:14:59 -0700938 if firmware_ro_build:
939 builds[provision.FW_RO_VERSION_PREFIX] = firmware_ro_build
Simran Basib6ec8ae2014-04-23 12:05:08 -0700940 return site_rpc_interface.create_suite_job(
941 name=name, control_file=control_file, priority=priority,
Michael Tang6dc174e2016-05-31 23:13:42 -0700942 builds=builds, test_source_build=test_source_build,
943 is_cloning=is_cloning, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700944 return create_job(name, priority, control_file, control_type, image=image,
Michael Tang6dc174e2016-05-31 23:13:42 -0700945 hostless=hostless, is_cloning=is_cloning, **kwargs)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700946
947
MK Ryue301eb72015-06-25 12:51:02 -0700948@rpc_utils.route_rpc_to_master
showard12f3e322009-05-13 21:27:42 +0000949def create_job(name, priority, control_file, control_type,
950 hosts=(), meta_hosts=(), one_time_hosts=(),
951 atomic_group_name=None, synch_count=None, is_template=False,
Simran Basi7e605742013-11-12 13:43:36 -0800952 timeout=None, timeout_mins=None, max_runtime_mins=None,
953 run_verify=False, email_list='', dependencies=(),
954 reboot_before=None, reboot_after=None, parse_failed_repair=None,
955 hostless=False, keyvals=None, drone_set=None, image=None,
Dan Shiec1d47d2015-02-13 11:38:13 -0800956 parent_job_id=None, test_retry=0, run_reset=True,
Michael Tang6dc174e2016-05-31 23:13:42 -0700957 require_ssp=None, args=(), is_cloning=False, **kwargs):
jadmanski0afbb632008-06-06 21:10:57 +0000958 """\
959 Create and enqueue a job.
mblighe8819cd2008-02-15 16:48:40 +0000960
showarda1e74b32009-05-12 17:32:04 +0000961 @param name name of this job
Alex Miller7d658cf2013-09-04 16:00:35 -0700962 @param priority Integer priority of this job. Higher is more important.
showarda1e74b32009-05-12 17:32:04 +0000963 @param control_file String contents of the control file.
964 @param control_type Type of control file, Client or Server.
965 @param synch_count How many machines the job uses per autoserv execution.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700966 synch_count == 1 means the job is asynchronous. If an atomic group is
967 given this value is treated as a minimum.
showarda1e74b32009-05-12 17:32:04 +0000968 @param is_template If true then create a template job.
969 @param timeout Hours after this call returns until the job times out.
Simran Basi7e605742013-11-12 13:43:36 -0800970 @param timeout_mins Minutes after this call returns until the job times
Jiaxi Luo90190c92014-06-18 12:35:57 -0700971 out.
Simran Basi34217022012-11-06 13:43:15 -0800972 @param max_runtime_mins Minutes from job starting time until job times out
showarda1e74b32009-05-12 17:32:04 +0000973 @param run_verify Should the host be verified before running the test?
974 @param email_list String containing emails to mail when the job is done
975 @param dependencies List of label names on which this job depends
976 @param reboot_before Never, If dirty, or Always
977 @param reboot_after Never, If all tests passed, or Always
978 @param parse_failed_repair if true, results of failed repairs launched by
Jiaxi Luo90190c92014-06-18 12:35:57 -0700979 this job will be parsed as part of the job.
showarda9545c02009-12-18 22:44:26 +0000980 @param hostless if true, create a hostless job
showardc1a98d12010-01-15 00:22:22 +0000981 @param keyvals dict of keyvals to associate with the job
showarda1e74b32009-05-12 17:32:04 +0000982 @param hosts List of hosts to run job on.
983 @param meta_hosts List where each entry is a label name, and for each entry
Jiaxi Luo90190c92014-06-18 12:35:57 -0700984 one host will be chosen from that label to run the job on.
showarda1e74b32009-05-12 17:32:04 +0000985 @param one_time_hosts List of hosts not in the database to run the job on.
986 @param atomic_group_name The name of an atomic group to schedule the job on.
jamesren76fcf192010-04-21 20:39:50 +0000987 @param drone_set The name of the drone set to run this test on.
Paul Pendlebury5a8c6ad2011-02-01 07:20:17 -0800988 @param image OS image to install before running job.
Aviv Keshet0b9cfc92013-02-05 11:36:02 -0800989 @param parent_job_id id of a job considered to be parent of created job.
Simran Basib6ec8ae2014-04-23 12:05:08 -0700990 @param test_retry Number of times to retry test if the test did not
Jiaxi Luo90190c92014-06-18 12:35:57 -0700991 complete successfully. (optional, default: 0)
Simran Basib6ec8ae2014-04-23 12:05:08 -0700992 @param run_reset Should the host be reset before running the test?
Dan Shiec1d47d2015-02-13 11:38:13 -0800993 @param require_ssp Set to True to require server-side packaging to run the
994 test. If it's set to None, drone will still try to run
995 the server side with server-side packaging. If the
996 autotest-server package doesn't exist for the build or
997 image is not set, drone will run the test without server-
998 side packaging. Default is None.
Jiaxi Luo90190c92014-06-18 12:35:57 -0700999 @param args A list of args to be injected into control file.
Michael Tang6dc174e2016-05-31 23:13:42 -07001000 @param is_cloning: True if creating a cloning job.
Simran Basib6ec8ae2014-04-23 12:05:08 -07001001 @param kwargs extra keyword args. NOT USED.
showardc92da832009-04-07 18:14:34 +00001002
1003 @returns The created Job id number.
jadmanski0afbb632008-06-06 21:10:57 +00001004 """
Jiaxi Luo90190c92014-06-18 12:35:57 -07001005 if args:
1006 control_file = tools.inject_vars({'args': args}, control_file)
1007
Simran Basiab5a1bf2014-05-28 15:39:44 -07001008 if image is None:
1009 return rpc_utils.create_job_common(
1010 **rpc_utils.get_create_job_common_args(locals()))
1011
Simran Basi6157e8e2015-12-07 18:22:34 -08001012 # Translate the image name, in case its a relative build name.
1013 ds = dev_server.ImageServer.resolve(image)
1014 image = ds.translate(image)
1015
Simran Basiab5a1bf2014-05-28 15:39:44 -07001016 # When image is supplied use a known parameterized test already in the
1017 # database to pass the OS image path from the front end, through the
1018 # scheduler, and finally to autoserv as the --image parameter.
1019
1020 # The test autoupdate_ParameterizedJob is in afe_autotests and used to
1021 # instantiate a Test object and from there a ParameterizedJob.
1022 known_test_obj = models.Test.smart_get('autoupdate_ParameterizedJob')
1023 known_parameterized_job = models.ParameterizedJob.objects.create(
1024 test=known_test_obj)
1025
1026 # autoupdate_ParameterizedJob has a single parameter, the image parameter,
1027 # stored in the table afe_test_parameters. We retrieve and set this
1028 # instance of the parameter to the OS image path.
1029 image_parameter = known_test_obj.testparameter_set.get(test=known_test_obj,
1030 name='image')
1031 known_parameterized_job.parameterizedjobparameter_set.create(
1032 test_parameter=image_parameter, parameter_value=image,
1033 parameter_type='string')
1034
Dan Shid215dbe2015-06-18 16:14:59 -07001035 # TODO(crbug.com/502638): save firmware build etc to parameterized_job.
1036
Simran Basiab5a1bf2014-05-28 15:39:44 -07001037 # By passing a parameterized_job to create_job_common the job entry in
1038 # the afe_jobs table will have the field parameterized_job_id set.
1039 # The scheduler uses this id in the afe_parameterized_jobs table to
1040 # match this job to our known test, and then with the
1041 # afe_parameterized_job_parameters table to get the actual image path.
jamesren4a41e012010-07-16 22:33:48 +00001042 return rpc_utils.create_job_common(
Simran Basiab5a1bf2014-05-28 15:39:44 -07001043 parameterized_job=known_parameterized_job.id,
jamesren4a41e012010-07-16 22:33:48 +00001044 **rpc_utils.get_create_job_common_args(locals()))
mblighe8819cd2008-02-15 16:48:40 +00001045
1046
showard9dbdcda2008-10-14 17:34:36 +00001047def abort_host_queue_entries(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001048 """\
showard9dbdcda2008-10-14 17:34:36 +00001049 Abort a set of host queue entries.
Fang Deng63b0e452014-12-19 14:38:15 -08001050
1051 @return: A list of dictionaries, each contains information
1052 about an aborted HQE.
jadmanski0afbb632008-06-06 21:10:57 +00001053 """
showard9dbdcda2008-10-14 17:34:36 +00001054 query = models.HostQueueEntry.query_objects(filter_data)
beepsfaecbce2013-10-29 11:35:10 -07001055
1056 # Dont allow aborts on:
1057 # 1. Jobs that have already completed (whether or not they were aborted)
1058 # 2. Jobs that we have already been aborted (but may not have completed)
1059 query = query.filter(complete=False).filter(aborted=False)
showarddc817512008-11-12 18:16:41 +00001060 models.AclGroup.check_abort_permissions(query)
showard9dbdcda2008-10-14 17:34:36 +00001061 host_queue_entries = list(query.select_related())
showard2bab8f42008-11-12 18:15:22 +00001062 rpc_utils.check_abort_synchronous_jobs(host_queue_entries)
mblighe8819cd2008-02-15 16:48:40 +00001063
Simran Basic1b26762013-06-26 14:23:21 -07001064 models.HostQueueEntry.abort_host_queue_entries(host_queue_entries)
Fang Deng63b0e452014-12-19 14:38:15 -08001065 hqe_info = [{'HostQueueEntry': hqe.id, 'Job': hqe.job_id,
1066 'Job name': hqe.job.name} for hqe in host_queue_entries]
1067 return hqe_info
showard9d821ab2008-07-11 16:54:29 +00001068
1069
beeps8bb1f7d2013-08-05 01:30:09 -07001070def abort_special_tasks(**filter_data):
1071 """\
1072 Abort the special task, or tasks, specified in the filter.
1073 """
1074 query = models.SpecialTask.query_objects(filter_data)
1075 special_tasks = query.filter(is_active=True)
1076 for task in special_tasks:
1077 task.abort()
1078
1079
Simran Basi73dae552013-02-25 14:57:46 -08001080def _call_special_tasks_on_hosts(task, hosts):
1081 """\
1082 Schedules a set of hosts for a special task.
1083
1084 @returns A list of hostnames that a special task was created for.
1085 """
1086 models.AclGroup.check_for_acl_violation_hosts(hosts)
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -08001087 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts)
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001088 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian6edaaf92014-11-24 16:36:25 -08001089 raise ValueError('The following hosts are on shards, please '
1090 'follow the link to the shards and create jobs '
1091 'there instead. %s.' % shard_host_map)
Simran Basi73dae552013-02-25 14:57:46 -08001092 for host in hosts:
1093 models.SpecialTask.schedule_special_task(host, task)
1094 return list(sorted(host.hostname for host in hosts))
1095
1096
MK Ryu5aa25042015-07-28 16:08:04 -07001097def _forward_special_tasks_on_hosts(task, rpc, **filter_data):
1098 """Forward special tasks to corresponding shards.
mbligh4e545a52009-12-19 05:30:39 +00001099
MK Ryu5aa25042015-07-28 16:08:04 -07001100 For master, when special tasks are fired on hosts that are sharded,
1101 forward the RPC to corresponding shards.
1102
1103 For shard, create special task records in local DB.
1104
1105 @param task: Enum value of frontend.afe.models.SpecialTask.Task
1106 @param rpc: RPC name to forward.
1107 @param filter_data: Filter keywords to be used for DB query.
1108
1109 @return: A list of hostnames that a special task was created for.
showard1ff7b2e2009-05-15 23:17:18 +00001110 """
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001111 hosts = models.Host.query_objects(filter_data)
1112 shard_host_map = rpc_utils.bucket_hosts_by_shard(hosts, rpc_hostnames=True)
1113
1114 # Filter out hosts on a shard from those on the master, forward
1115 # rpcs to the shard with an additional hostname__in filter, and
1116 # create a local SpecialTask for each remaining host.
Prashanth Balasubramanian8c98ac12014-12-23 11:26:44 -08001117 if shard_host_map and not utils.is_shard():
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001118 hosts = [h for h in hosts if h.shard is None]
1119 for shard, hostnames in shard_host_map.iteritems():
1120
1121 # The main client of this module is the frontend website, and
1122 # it invokes it with an 'id' or an 'id__in' filter. Regardless,
1123 # the 'hostname' filter should narrow down the list of hosts on
1124 # each shard even though we supply all the ids in filter_data.
1125 # This method uses hostname instead of id because it fits better
MK Ryu5aa25042015-07-28 16:08:04 -07001126 # with the overall architecture of redirection functions in
1127 # rpc_utils.
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001128 shard_filter = filter_data.copy()
1129 shard_filter['hostname__in'] = hostnames
1130 rpc_utils.run_rpc_on_multiple_hostnames(
MK Ryu5aa25042015-07-28 16:08:04 -07001131 rpc, [shard], **shard_filter)
Prashanth Balasubramanian40981232014-12-16 19:01:58 -08001132
1133 # There is a race condition here if someone assigns a shard to one of these
1134 # hosts before we create the task. The host will stay on the master if:
1135 # 1. The host is not Ready
1136 # 2. The host is Ready but has a task
1137 # But if the host is Ready and doesn't have a task yet, it will get sent
1138 # to the shard as we're creating a task here.
1139
1140 # Given that we only rarely verify Ready hosts it isn't worth putting this
1141 # entire method in a transaction. The worst case scenario is that we have
MK Ryu5aa25042015-07-28 16:08:04 -07001142 # a verify running on a Ready host while the shard is using it, if the
1143 # verify fails no subsequent tasks will be created against the host on the
1144 # master, and verifies are safe enough that this is OK.
1145 return _call_special_tasks_on_hosts(task, hosts)
1146
1147
1148def reverify_hosts(**filter_data):
1149 """\
1150 Schedules a set of hosts for verify.
1151
1152 @returns A list of hostnames that a verify task was created for.
1153 """
1154 return _forward_special_tasks_on_hosts(
1155 models.SpecialTask.Task.VERIFY, 'reverify_hosts', **filter_data)
Simran Basi73dae552013-02-25 14:57:46 -08001156
1157
1158def repair_hosts(**filter_data):
1159 """\
1160 Schedules a set of hosts for repair.
1161
1162 @returns A list of hostnames that a repair task was created for.
1163 """
MK Ryu5aa25042015-07-28 16:08:04 -07001164 return _forward_special_tasks_on_hosts(
1165 models.SpecialTask.Task.REPAIR, 'repair_hosts', **filter_data)
showard1ff7b2e2009-05-15 23:17:18 +00001166
1167
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001168def get_jobs(not_yet_run=False, running=False, finished=False,
1169 suite=False, sub=False, standalone=False, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001170 """\
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001171 Extra status filter args for get_jobs:
jadmanski0afbb632008-06-06 21:10:57 +00001172 -not_yet_run: Include only jobs that have not yet started running.
1173 -running: Include only jobs that have start running but for which not
1174 all hosts have completed.
1175 -finished: Include only jobs for which all hosts have completed (or
1176 aborted).
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001177
1178 Extra type filter args for get_jobs:
1179 -suite: Include only jobs with child jobs.
1180 -sub: Include only jobs with a parent job.
1181 -standalone: Inlcude only jobs with no child or parent jobs.
1182 At most one of these three fields should be specified.
jadmanski0afbb632008-06-06 21:10:57 +00001183 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001184 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1185 running,
1186 finished)
1187 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1188 suite,
1189 sub,
1190 standalone)
showard0957a842009-05-11 19:25:08 +00001191 job_dicts = []
1192 jobs = list(models.Job.query_objects(filter_data))
1193 models.Job.objects.populate_relationships(jobs, models.Label,
1194 'dependencies')
showardc1a98d12010-01-15 00:22:22 +00001195 models.Job.objects.populate_relationships(jobs, models.JobKeyval, 'keyvals')
showard0957a842009-05-11 19:25:08 +00001196 for job in jobs:
1197 job_dict = job.get_object_dict()
1198 job_dict['dependencies'] = ','.join(label.name
1199 for label in job.dependencies)
showardc1a98d12010-01-15 00:22:22 +00001200 job_dict['keyvals'] = dict((keyval.key, keyval.value)
1201 for keyval in job.keyvals)
Eric Lid23bc192011-02-09 14:38:57 -08001202 if job.parameterized_job:
1203 job_dict['image'] = get_parameterized_autoupdate_image_url(job)
showard0957a842009-05-11 19:25:08 +00001204 job_dicts.append(job_dict)
1205 return rpc_utils.prepare_for_serialization(job_dicts)
mblighe8819cd2008-02-15 16:48:40 +00001206
1207
1208def get_num_jobs(not_yet_run=False, running=False, finished=False,
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001209 suite=False, sub=False, standalone=False,
jadmanski0afbb632008-06-06 21:10:57 +00001210 **filter_data):
Aviv Keshet17660a52016-04-06 18:56:43 +00001211 """\
1212 See get_jobs() for documentation of extra filter parameters.
jadmanski0afbb632008-06-06 21:10:57 +00001213 """
Jiaxi Luo15cbf372014-07-01 19:20:20 -07001214 extra_args = rpc_utils.extra_job_status_filters(not_yet_run,
1215 running,
1216 finished)
1217 filter_data['extra_args'] = rpc_utils.extra_job_type_filters(extra_args,
1218 suite,
1219 sub,
1220 standalone)
Aviv Keshet17660a52016-04-06 18:56:43 +00001221 return models.Job.query_count(filter_data)
mblighe8819cd2008-02-15 16:48:40 +00001222
1223
mblighe8819cd2008-02-15 16:48:40 +00001224def get_jobs_summary(**filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001225 """\
Jiaxi Luoaac54572014-06-04 13:57:02 -07001226 Like get_jobs(), but adds 'status_counts' and 'result_counts' field.
1227
1228 'status_counts' filed is a dictionary mapping status strings to the number
1229 of hosts currently with that status, i.e. {'Queued' : 4, 'Running' : 2}.
1230
1231 'result_counts' field is piped to tko's rpc_interface and has the return
1232 format specified under get_group_counts.
jadmanski0afbb632008-06-06 21:10:57 +00001233 """
1234 jobs = get_jobs(**filter_data)
1235 ids = [job['id'] for job in jobs]
1236 all_status_counts = models.Job.objects.get_status_counts(ids)
1237 for job in jobs:
1238 job['status_counts'] = all_status_counts[job['id']]
Jiaxi Luoaac54572014-06-04 13:57:02 -07001239 job['result_counts'] = tko_rpc_interface.get_status_counts(
1240 ['afe_job_id', 'afe_job_id'],
1241 header_groups=[['afe_job_id'], ['afe_job_id']],
1242 **{'afe_job_id': job['id']})
jadmanski0afbb632008-06-06 21:10:57 +00001243 return rpc_utils.prepare_for_serialization(jobs)
mblighe8819cd2008-02-15 16:48:40 +00001244
1245
showarda965cef2009-05-15 23:17:41 +00001246def get_info_for_clone(id, preserve_metahosts, queue_entry_filter_data=None):
showarda8709c52008-07-03 19:44:54 +00001247 """\
1248 Retrieves all the information needed to clone a job.
1249 """
showarda8709c52008-07-03 19:44:54 +00001250 job = models.Job.objects.get(id=id)
showard29f7cd22009-04-29 21:16:24 +00001251 job_info = rpc_utils.get_job_info(job,
showarda965cef2009-05-15 23:17:41 +00001252 preserve_metahosts,
1253 queue_entry_filter_data)
showard945072f2008-09-03 20:34:59 +00001254
showardd9992fe2008-07-31 02:15:03 +00001255 host_dicts = []
showard29f7cd22009-04-29 21:16:24 +00001256 for host in job_info['hosts']:
1257 host_dict = get_hosts(id=host.id)[0]
1258 other_labels = host_dict['labels']
1259 if host_dict['platform']:
1260 other_labels.remove(host_dict['platform'])
1261 host_dict['other_labels'] = ', '.join(other_labels)
showardd9992fe2008-07-31 02:15:03 +00001262 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001263
showard29f7cd22009-04-29 21:16:24 +00001264 for host in job_info['one_time_hosts']:
1265 host_dict = dict(hostname=host.hostname,
1266 id=host.id,
1267 platform='(one-time host)',
1268 locked_text='')
1269 host_dicts.append(host_dict)
showarda8709c52008-07-03 19:44:54 +00001270
showard4d077562009-05-08 18:24:36 +00001271 # convert keys from Label objects to strings (names of labels)
showard29f7cd22009-04-29 21:16:24 +00001272 meta_host_counts = dict((meta_host.name, count) for meta_host, count
showard4d077562009-05-08 18:24:36 +00001273 in job_info['meta_host_counts'].iteritems())
showard29f7cd22009-04-29 21:16:24 +00001274
1275 info = dict(job=job.get_object_dict(),
1276 meta_host_counts=meta_host_counts,
1277 hosts=host_dicts)
1278 info['job']['dependencies'] = job_info['dependencies']
1279 if job_info['atomic_group']:
1280 info['atomic_group_name'] = (job_info['atomic_group']).name
1281 else:
1282 info['atomic_group_name'] = None
jamesren2275ef12010-04-12 18:25:06 +00001283 info['hostless'] = job_info['hostless']
jamesren76fcf192010-04-21 20:39:50 +00001284 info['drone_set'] = job.drone_set and job.drone_set.name
showarda8709c52008-07-03 19:44:54 +00001285
Michael Tang6dc174e2016-05-31 23:13:42 -07001286 image = _get_image_for_job(job, job_info['hostless'])
1287 if image:
1288 info['job']['image'] = image
Eric Lid23bc192011-02-09 14:38:57 -08001289
showarda8709c52008-07-03 19:44:54 +00001290 return rpc_utils.prepare_for_serialization(info)
1291
1292
Michael Tang6dc174e2016-05-31 23:13:42 -07001293def _get_image_for_job(job, hostless):
1294 """ Gets the image used for a job.
1295
1296 Gets the image used for an AFE job. If the job is a parameterized job, get
1297 the image from the job parameter; otherwise, tries to get the image from
1298 the job's keyvals 'build' or 'builds'. As a last resort, if the job is a
1299 hostless job, tries to get the image from its control file attributes
1300 'build' or 'builds'.
1301
1302 TODO(ntang): Needs to handle FAFT with two builds for ro/rw.
1303
1304 @param job An AFE job object.
1305 @param hostless Boolean on of the job is hostless.
1306
1307 @returns The image build used for the job.
1308 """
1309 image = None
1310 if job.parameterized_job:
1311 image = get_parameterized_autoupdate_image_url(job)
1312 else:
1313 keyvals = job.keyval_dict()
Michael Tang84a2ecf2016-06-07 15:10:53 -07001314 image = keyvals.get('build')
Michael Tang6dc174e2016-05-31 23:13:42 -07001315 if not image:
1316 value = keyvals.get('builds')
1317 builds = None
1318 if isinstance(value, dict):
1319 builds = value
1320 elif isinstance(value, basestring):
1321 builds = ast.literal_eval(value)
1322 if builds:
1323 image = builds.get('cros-version')
1324 if not image and hostless and job.control_file:
1325 try:
1326 control_obj = control_data.parse_control_string(
1327 job.control_file)
1328 if hasattr(control_obj, 'build'):
1329 image = getattr(control_obj, 'build')
1330 if not image and hasattr(control_obj, 'builds'):
1331 builds = getattr(control_obj, 'builds')
1332 image = builds.get('cros-version')
1333 except:
1334 logging.warning('Failed to parse control file for job: %s',
1335 job.name)
1336 return image
1337
showard34dc5fa2008-04-24 20:58:40 +00001338
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001339def get_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001340 """\
showardc92da832009-04-07 18:14:34 +00001341 @returns A sequence of nested dictionaries of host and job information.
jadmanski0afbb632008-06-06 21:10:57 +00001342 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001343 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1344 'started_on__lte',
1345 start_time,
1346 end_time,
1347 **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001348 return rpc_utils.prepare_rows_as_nested_dicts(
1349 models.HostQueueEntry.query_objects(filter_data),
1350 ('host', 'atomic_group', 'job'))
showard34dc5fa2008-04-24 20:58:40 +00001351
1352
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001353def get_num_host_queue_entries(start_time=None, end_time=None, **filter_data):
jadmanski0afbb632008-06-06 21:10:57 +00001354 """\
1355 Get the number of host queue entries associated with this job.
1356 """
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001357 filter_data = rpc_utils.inject_times_to_filter('started_on__gte',
1358 'started_on__lte',
1359 start_time,
1360 end_time,
1361 **filter_data)
jadmanski0afbb632008-06-06 21:10:57 +00001362 return models.HostQueueEntry.query_count(filter_data)
showard34dc5fa2008-04-24 20:58:40 +00001363
1364
showard1e935f12008-07-11 00:11:36 +00001365def get_hqe_percentage_complete(**filter_data):
1366 """
showardc92da832009-04-07 18:14:34 +00001367 Computes the fraction of host queue entries matching the given filter data
showard1e935f12008-07-11 00:11:36 +00001368 that are complete.
1369 """
1370 query = models.HostQueueEntry.query_objects(filter_data)
1371 complete_count = query.filter(complete=True).count()
1372 total_count = query.count()
1373 if total_count == 0:
1374 return 1
1375 return float(complete_count) / total_count
1376
1377
showard1a5a4082009-07-28 20:01:37 +00001378# special tasks
1379
1380def get_special_tasks(**filter_data):
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001381 """Get special task entries from the local database.
1382
1383 Query the special tasks table for tasks matching the given
1384 `filter_data`, and return a list of the results. No attempt is
1385 made to forward the call to shards; the buck will stop here.
1386 The caller is expected to know the target shard for such reasons
1387 as:
1388 * The caller is a service (such as gs_offloader) configured
1389 to operate on behalf of one specific shard, and no other.
1390 * The caller has a host as a parameter, and knows that this is
1391 the shard assigned to that host.
1392
1393 @param filter_data Filter keywords to pass to the underlying
1394 database query.
1395
1396 """
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001397 return rpc_utils.prepare_rows_as_nested_dicts(
1398 models.SpecialTask.query_objects(filter_data),
1399 ('host', 'queue_entry'))
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001400
1401
1402def get_host_special_tasks(host_id, **filter_data):
1403 """Get special task entries for a given host.
1404
1405 Query the special tasks table for tasks that ran on the host
1406 given by `host_id` and matching the given `filter_data`.
1407 Return a list of the results. If the host is assigned to a
1408 shard, forward this call to that shard.
1409
1410 @param host_id Id in the database of the target host.
1411 @param filter_data Filter keywords to pass to the underlying
1412 database query.
1413
1414 """
MK Ryu0c1a37d2015-04-30 12:00:55 -07001415 # Retrieve host data even if the host is in an invalid state.
1416 host = models.Host.smart_get(host_id, False)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001417 if not host.shard:
J. Richard Barnettefdfcd662015-04-13 17:20:29 -07001418 return get_special_tasks(host_id=host_id, **filter_data)
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001419 else:
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001420 # The return values from AFE methods are post-processed
1421 # objects that aren't JSON-serializable. So, we have to
1422 # call AFE.run() to get the raw, serializable output from
1423 # the shard.
J. Richard Barnetteb5164d62015-04-13 12:59:31 -07001424 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1425 return shard_afe.run('get_special_tasks',
1426 host_id=host_id, **filter_data)
showard1a5a4082009-07-28 20:01:37 +00001427
1428
MK Ryu0c1a37d2015-04-30 12:00:55 -07001429def get_num_special_tasks(**kwargs):
1430 """Get the number of special task entries from the local database.
1431
1432 Query the special tasks table for tasks matching the given 'kwargs',
1433 and return the number of the results. No attempt is made to forward
1434 the call to shards; the buck will stop here.
1435
1436 @param kwargs Filter keywords to pass to the underlying database query.
1437
1438 """
1439 return models.SpecialTask.query_count(kwargs)
1440
1441
1442def get_host_num_special_tasks(host, **kwargs):
1443 """Get special task entries for a given host.
1444
1445 Query the special tasks table for tasks that ran on the host
1446 given by 'host' and matching the given 'kwargs'.
1447 Return a list of the results. If the host is assigned to a
1448 shard, forward this call to that shard.
1449
1450 @param host id or name of a host. More often a hostname.
1451 @param kwargs Filter keywords to pass to the underlying database query.
1452
1453 """
1454 # Retrieve host data even if the host is in an invalid state.
1455 host_model = models.Host.smart_get(host, False)
1456 if not host_model.shard:
1457 return get_num_special_tasks(host=host, **kwargs)
1458 else:
1459 shard_afe = frontend.AFE(server=host_model.shard.rpc_hostname())
1460 return shard_afe.run('get_num_special_tasks', host=host, **kwargs)
1461
1462
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001463def get_status_task(host_id, end_time):
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001464 """Get the "status task" for a host from the local shard.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001465
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001466 Returns a single special task representing the given host's
1467 "status task". The status task is a completed special task that
1468 identifies whether the corresponding host was working or broken
1469 when it completed. A successful task indicates a working host;
1470 a failed task indicates broken.
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001471
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001472 This call will not be forward to a shard; the receiving server
1473 must be the shard that owns the host.
1474
1475 @param host_id Id in the database of the target host.
1476 @param end_time Time reference for the host's status.
1477
1478 @return A single task; its status (successful or not)
1479 corresponds to the status of the host (working or
1480 broken) at the given time. If no task is found, return
1481 `None`.
1482
1483 """
1484 tasklist = rpc_utils.prepare_rows_as_nested_dicts(
1485 status_history.get_status_task(host_id, end_time),
1486 ('host', 'queue_entry'))
1487 return tasklist[0] if tasklist else None
1488
1489
1490def get_host_status_task(host_id, end_time):
1491 """Get the "status task" for a host from its owning shard.
1492
1493 Finds the given host's owning shard, and forwards to it a call
1494 to `get_status_task()` (see above).
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001495
1496 @param host_id Id in the database of the target host.
1497 @param end_time Time reference for the host's status.
1498
1499 @return A single task; its status (successful or not)
1500 corresponds to the status of the host (working or
1501 broken) at the given time. If no task is found, return
1502 `None`.
1503
1504 """
1505 host = models.Host.smart_get(host_id)
1506 if not host.shard:
J. Richard Barnette4d7e6e62015-05-01 10:47:34 -07001507 return get_status_task(host_id, end_time)
J. Richard Barnette39255fa2015-04-14 17:23:41 -07001508 else:
1509 # The return values from AFE methods are post-processed
1510 # objects that aren't JSON-serializable. So, we have to
1511 # call AFE.run() to get the raw, serializable output from
1512 # the shard.
1513 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1514 return shard_afe.run('get_status_task',
1515 host_id=host_id, end_time=end_time)
1516
1517
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001518def get_host_diagnosis_interval(host_id, end_time, success):
1519 """Find a "diagnosis interval" for a given host.
1520
1521 A "diagnosis interval" identifies a start and end time where
1522 the host went from "working" to "broken", or vice versa. The
1523 interval's starting time is the starting time of the last status
1524 task with the old status; the end time is the finish time of the
1525 first status task with the new status.
1526
1527 This routine finds the most recent diagnosis interval for the
1528 given host prior to `end_time`, with a starting status matching
1529 `success`. If `success` is true, the interval will start with a
1530 successful status task; if false the interval will start with a
1531 failed status task.
1532
1533 @param host_id Id in the database of the target host.
1534 @param end_time Time reference for the diagnosis interval.
1535 @param success Whether the diagnosis interval should start
1536 with a successful or failed status task.
1537
1538 @return A list of two strings. The first is the timestamp for
1539 the beginning of the interval; the second is the
1540 timestamp for the end. If the host has never changed
1541 state, the list is empty.
1542
1543 """
1544 host = models.Host.smart_get(host_id)
J. Richard Barnette78f281a2015-06-29 13:24:51 -07001545 if not host.shard or utils.is_shard():
J. Richard Barnette8abbfd62015-06-23 12:46:54 -07001546 return status_history.get_diagnosis_interval(
1547 host_id, end_time, success)
1548 else:
1549 shard_afe = frontend.AFE(server=host.shard.rpc_hostname())
1550 return shard_afe.get_host_diagnosis_interval(
1551 host_id, end_time, success)
1552
1553
showardc0ac3a72009-07-08 21:14:45 +00001554# support for host detail view
1555
MK Ryu0c1a37d2015-04-30 12:00:55 -07001556def get_host_queue_entries_and_special_tasks(host, query_start=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001557 query_limit=None, start_time=None,
1558 end_time=None):
showardc0ac3a72009-07-08 21:14:45 +00001559 """
1560 @returns an interleaved list of HostQueueEntries and SpecialTasks,
1561 in approximate run order. each dict contains keys for type, host,
1562 job, status, started_on, execution_path, and ID.
1563 """
1564 total_limit = None
1565 if query_limit is not None:
1566 total_limit = query_start + query_limit
MK Ryu0c1a37d2015-04-30 12:00:55 -07001567 filter_data_common = {'host': host,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001568 'query_limit': total_limit,
1569 'sort_by': ['-id']}
showardc0ac3a72009-07-08 21:14:45 +00001570
MK Ryu0c1a37d2015-04-30 12:00:55 -07001571 filter_data_special_tasks = rpc_utils.inject_times_to_filter(
1572 'time_started__gte', 'time_started__lte', start_time, end_time,
1573 **filter_data_common)
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001574
MK Ryu0c1a37d2015-04-30 12:00:55 -07001575 queue_entries = get_host_queue_entries(
1576 start_time, end_time, **filter_data_common)
1577 special_tasks = get_host_special_tasks(host, **filter_data_special_tasks)
showardc0ac3a72009-07-08 21:14:45 +00001578
1579 interleaved_entries = rpc_utils.interleave_entries(queue_entries,
1580 special_tasks)
1581 if query_start is not None:
1582 interleaved_entries = interleaved_entries[query_start:]
1583 if query_limit is not None:
1584 interleaved_entries = interleaved_entries[:query_limit]
MK Ryu0c1a37d2015-04-30 12:00:55 -07001585 return rpc_utils.prepare_host_queue_entries_and_special_tasks(
1586 interleaved_entries, queue_entries)
showardc0ac3a72009-07-08 21:14:45 +00001587
1588
MK Ryu0c1a37d2015-04-30 12:00:55 -07001589def get_num_host_queue_entries_and_special_tasks(host, start_time=None,
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001590 end_time=None):
MK Ryu0c1a37d2015-04-30 12:00:55 -07001591 filter_data_common = {'host': host}
Jiaxi Luo57bc1952014-07-22 15:27:30 -07001592
1593 filter_data_queue_entries, filter_data_special_tasks = (
1594 rpc_utils.inject_times_to_hqe_special_tasks_filters(
1595 filter_data_common, start_time, end_time))
1596
1597 return (models.HostQueueEntry.query_count(filter_data_queue_entries)
MK Ryu0c1a37d2015-04-30 12:00:55 -07001598 + get_host_num_special_tasks(**filter_data_special_tasks))
showardc0ac3a72009-07-08 21:14:45 +00001599
1600
showard29f7cd22009-04-29 21:16:24 +00001601# recurring run
1602
1603def get_recurring(**filter_data):
1604 return rpc_utils.prepare_rows_as_nested_dicts(
1605 models.RecurringRun.query_objects(filter_data),
1606 ('job', 'owner'))
1607
1608
1609def get_num_recurring(**filter_data):
1610 return models.RecurringRun.query_count(filter_data)
1611
1612
1613def delete_recurring_runs(**filter_data):
1614 to_delete = models.RecurringRun.query_objects(filter_data)
1615 to_delete.delete()
1616
1617
1618def create_recurring_run(job_id, start_date, loop_period, loop_count):
showard64a95952010-01-13 21:27:16 +00001619 owner = models.User.current_user().login
showard29f7cd22009-04-29 21:16:24 +00001620 job = models.Job.objects.get(id=job_id)
1621 return job.create_recurring_job(start_date=start_date,
1622 loop_period=loop_period,
1623 loop_count=loop_count,
1624 owner=owner)
1625
1626
mblighe8819cd2008-02-15 16:48:40 +00001627# other
1628
showarde0b63622008-08-04 20:58:47 +00001629def echo(data=""):
1630 """\
1631 Returns a passed in string. For doing a basic test to see if RPC calls
1632 can successfully be made.
1633 """
1634 return data
1635
1636
showardb7a52fd2009-04-27 20:10:56 +00001637def get_motd():
1638 """\
1639 Returns the message of the day as a string.
1640 """
1641 return rpc_utils.get_motd()
1642
1643
mblighe8819cd2008-02-15 16:48:40 +00001644def get_static_data():
jadmanski0afbb632008-06-06 21:10:57 +00001645 """\
1646 Returns a dictionary containing a bunch of data that shouldn't change
1647 often and is otherwise inaccessible. This includes:
showardc92da832009-04-07 18:14:34 +00001648
1649 priorities: List of job priority choices.
1650 default_priority: Default priority value for new jobs.
1651 users: Sorted list of all users.
Jiaxi Luo31874592014-06-11 10:36:35 -07001652 labels: Sorted list of labels not start with 'cros-version' and
1653 'fw-version'.
showardc92da832009-04-07 18:14:34 +00001654 atomic_groups: Sorted list of all atomic groups.
1655 tests: Sorted list of all tests.
1656 profilers: Sorted list of all profilers.
1657 current_user: Logged-in username.
1658 host_statuses: Sorted list of possible Host statuses.
1659 job_statuses: Sorted list of possible HostQueueEntry statuses.
Simran Basi7e605742013-11-12 13:43:36 -08001660 job_timeout_default: The default job timeout length in minutes.
showarda1e74b32009-05-12 17:32:04 +00001661 parse_failed_repair_default: Default value for the parse_failed_repair job
Jiaxi Luo31874592014-06-11 10:36:35 -07001662 option.
showardc92da832009-04-07 18:14:34 +00001663 reboot_before_options: A list of valid RebootBefore string enums.
1664 reboot_after_options: A list of valid RebootAfter string enums.
1665 motd: Server's message of the day.
1666 status_dictionary: A mapping from one word job status names to a more
1667 informative description.
jadmanski0afbb632008-06-06 21:10:57 +00001668 """
showard21baa452008-10-21 00:08:39 +00001669
1670 job_fields = models.Job.get_field_dict()
jamesren76fcf192010-04-21 20:39:50 +00001671 default_drone_set_name = models.DroneSet.default_drone_set_name()
1672 drone_sets = ([default_drone_set_name] +
1673 sorted(drone_set.name for drone_set in
1674 models.DroneSet.objects.exclude(
1675 name=default_drone_set_name)))
showard21baa452008-10-21 00:08:39 +00001676
jadmanski0afbb632008-06-06 21:10:57 +00001677 result = {}
Alex Miller7d658cf2013-09-04 16:00:35 -07001678 result['priorities'] = priorities.Priority.choices()
1679 default_priority = priorities.Priority.DEFAULT
1680 result['default_priority'] = 'Default'
1681 result['max_schedulable_priority'] = priorities.Priority.DEFAULT
jadmanski0afbb632008-06-06 21:10:57 +00001682 result['users'] = get_users(sort_by=['login'])
Jiaxi Luo31874592014-06-11 10:36:35 -07001683
1684 label_exclude_filters = [{'name__startswith': 'cros-version'},
Dan Shi65351d62015-08-03 12:03:23 -07001685 {'name__startswith': 'fw-version'},
1686 {'name__startswith': 'fwrw-version'},
Dan Shi27516972016-03-16 14:03:41 -07001687 {'name__startswith': 'fwro-version'},
1688 {'name__startswith': 'ab-version'},
1689 {'name__startswith': 'testbed-version'}]
Jiaxi Luo31874592014-06-11 10:36:35 -07001690 result['labels'] = get_labels(
1691 label_exclude_filters,
1692 sort_by=['-platform', 'name'])
1693
showardc92da832009-04-07 18:14:34 +00001694 result['atomic_groups'] = get_atomic_groups(sort_by=['name'])
jadmanski0afbb632008-06-06 21:10:57 +00001695 result['tests'] = get_tests(sort_by=['name'])
showard2b9a88b2008-06-13 20:55:03 +00001696 result['profilers'] = get_profilers(sort_by=['name'])
showard0fc38302008-10-23 00:44:07 +00001697 result['current_user'] = rpc_utils.prepare_for_serialization(
showard64a95952010-01-13 21:27:16 +00001698 models.User.current_user().get_object_dict())
showard2b9a88b2008-06-13 20:55:03 +00001699 result['host_statuses'] = sorted(models.Host.Status.names)
mbligh5a198b92008-12-11 19:33:29 +00001700 result['job_statuses'] = sorted(models.HostQueueEntry.Status.names)
Simran Basi7e605742013-11-12 13:43:36 -08001701 result['job_timeout_mins_default'] = models.Job.DEFAULT_TIMEOUT_MINS
Simran Basi34217022012-11-06 13:43:15 -08001702 result['job_max_runtime_mins_default'] = (
1703 models.Job.DEFAULT_MAX_RUNTIME_MINS)
showarda1e74b32009-05-12 17:32:04 +00001704 result['parse_failed_repair_default'] = bool(
1705 models.Job.DEFAULT_PARSE_FAILED_REPAIR)
jamesrendd855242010-03-02 22:23:44 +00001706 result['reboot_before_options'] = model_attributes.RebootBefore.names
1707 result['reboot_after_options'] = model_attributes.RebootAfter.names
showard8fbae652009-01-20 23:23:10 +00001708 result['motd'] = rpc_utils.get_motd()
jamesren76fcf192010-04-21 20:39:50 +00001709 result['drone_sets_enabled'] = models.DroneSet.drone_sets_enabled()
1710 result['drone_sets'] = drone_sets
jamesren4a41e012010-07-16 22:33:48 +00001711 result['parameterized_jobs'] = models.Job.parameterized_jobs_enabled()
showard8ac29b42008-07-17 17:01:55 +00001712
showardd3dc1992009-04-22 21:01:40 +00001713 result['status_dictionary'] = {"Aborted": "Aborted",
showard8ac29b42008-07-17 17:01:55 +00001714 "Verifying": "Verifying Host",
Alex Millerdfff2fd2013-05-28 13:05:06 -07001715 "Provisioning": "Provisioning Host",
showard8ac29b42008-07-17 17:01:55 +00001716 "Pending": "Waiting on other hosts",
1717 "Running": "Running autoserv",
1718 "Completed": "Autoserv completed",
1719 "Failed": "Failed to complete",
showardd823b362008-07-24 16:35:46 +00001720 "Queued": "Queued",
showard5deb6772008-11-04 21:54:33 +00001721 "Starting": "Next in host's queue",
1722 "Stopped": "Other host(s) failed verify",
showardd3dc1992009-04-22 21:01:40 +00001723 "Parsing": "Awaiting parse of final results",
showard29f7cd22009-04-29 21:16:24 +00001724 "Gathering": "Gathering log files",
showard8cc058f2009-09-08 16:26:33 +00001725 "Template": "Template job for recurring run",
mbligh4608b002010-01-05 18:22:35 +00001726 "Waiting": "Waiting for scheduler action",
Dan Shi07e09af2013-04-12 09:31:29 -07001727 "Archiving": "Archiving results",
1728 "Resetting": "Resetting hosts"}
Jiaxi Luo421608e2014-07-07 14:38:00 -07001729
1730 result['wmatrix_url'] = rpc_utils.get_wmatrix_url()
Simran Basi71206ef2014-08-13 13:51:18 -07001731 result['is_moblab'] = bool(utils.is_moblab())
Jiaxi Luo421608e2014-07-07 14:38:00 -07001732
jadmanski0afbb632008-06-06 21:10:57 +00001733 return result
showard29f7cd22009-04-29 21:16:24 +00001734
1735
1736def get_server_time():
1737 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
Kevin Cheng19521982016-09-22 12:27:23 -07001738
1739
1740def get_hosts_by_attribute(attribute, value):
1741 """
1742 Get the list of valid hosts that share the same host attribute value.
1743
1744 @param attribute: String of the host attribute to check.
1745 @param value: String of the value that is shared between hosts.
1746
1747 @returns List of hostnames that all have the same host attribute and
1748 value.
1749 """
1750 hosts = models.HostAttribute.query_objects({'attribute': attribute,
1751 'value': value})
1752 return [row.host.hostname for row in hosts if row.host.invalid == 0]