blob: b2b27c25c1498caede5f8152b2d2f16016879d9b [file] [log] [blame]
mbligh67647152008-11-19 00:18:14 +00001# Copyright Martin J. Bligh, Google Inc 2008
2# Released under the GPL v2
3
4"""
5This class allows you to communicate with the frontend to submit jobs etc
6It is designed for writing more sophisiticated server-side control files that
7can recursively add and manage other jobs.
8
9We turn the JSON dictionaries into real objects that are more idiomatic
10
mblighc31e4022008-12-11 19:32:30 +000011For docs, see:
jamesren1a2914a2010-02-12 00:44:31 +000012 http://autotest/afe/server/rpc_doc/
13 http://autotest/new_tko/server/rpc_doc/
mblighc31e4022008-12-11 19:32:30 +000014 http://docs.djangoproject.com/en/dev/ref/models/querysets/#queryset-api
mbligh67647152008-11-19 00:18:14 +000015"""
16
mblighdb59e3c2009-11-21 01:45:18 +000017import getpass, os, time, traceback, re
mbligh67647152008-11-19 00:18:14 +000018import common
19from autotest_lib.frontend.afe import rpc_client_lib
mbligh37eceaa2008-12-15 22:56:37 +000020from autotest_lib.client.common_lib import global_config
mbligh67647152008-11-19 00:18:14 +000021from autotest_lib.client.common_lib import utils
Scott Zawalski63470dd2012-09-05 00:49:43 -040022from autotest_lib.tko import db
23
24
mbligh4e576612008-12-22 14:56:36 +000025try:
26 from autotest_lib.server.site_common import site_utils as server_utils
27except:
28 from autotest_lib.server import utils as server_utils
29form_ntuples_from_machines = server_utils.form_ntuples_from_machines
mbligh67647152008-11-19 00:18:14 +000030
mbligh37eceaa2008-12-15 22:56:37 +000031GLOBAL_CONFIG = global_config.global_config
32DEFAULT_SERVER = 'autotest'
33
mbligh67647152008-11-19 00:18:14 +000034def dump_object(header, obj):
35 """
36 Standard way to print out the frontend objects (eg job, host, acl, label)
37 in a human-readable fashion for debugging
38 """
39 result = header + '\n'
40 for key in obj.hash:
41 if key == 'afe' or key == 'hash':
42 continue
43 result += '%20s: %s\n' % (key, obj.hash[key])
44 return result
45
46
mbligh5280e3b2008-12-22 14:39:28 +000047class RpcClient(object):
mbligh67647152008-11-19 00:18:14 +000048 """
mbligh451ede12009-02-12 21:54:03 +000049 Abstract RPC class for communicating with the autotest frontend
50 Inherited for both TKO and AFE uses.
mbligh67647152008-11-19 00:18:14 +000051
mbligh1ef218d2009-08-03 16:57:56 +000052 All the constructors go in the afe / tko class.
mbligh451ede12009-02-12 21:54:03 +000053 Manipulating methods go in the object classes themselves
mbligh67647152008-11-19 00:18:14 +000054 """
mbligh99b24f42009-06-08 16:45:55 +000055 def __init__(self, path, user, server, print_log, debug, reply_debug):
mbligh67647152008-11-19 00:18:14 +000056 """
mbligh451ede12009-02-12 21:54:03 +000057 Create a cached instance of a connection to the frontend
mbligh67647152008-11-19 00:18:14 +000058
59 user: username to connect as
mbligh451ede12009-02-12 21:54:03 +000060 server: frontend server to connect to
mbligh67647152008-11-19 00:18:14 +000061 print_log: pring a logging message to stdout on every operation
62 debug: print out all RPC traffic
63 """
mblighc31e4022008-12-11 19:32:30 +000064 if not user:
mblighdb59e3c2009-11-21 01:45:18 +000065 user = getpass.getuser()
mbligh451ede12009-02-12 21:54:03 +000066 if not server:
mbligh475f7762009-01-30 00:34:04 +000067 if 'AUTOTEST_WEB' in os.environ:
mbligh451ede12009-02-12 21:54:03 +000068 server = os.environ['AUTOTEST_WEB']
mbligh475f7762009-01-30 00:34:04 +000069 else:
mbligh451ede12009-02-12 21:54:03 +000070 server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
71 default=DEFAULT_SERVER)
72 self.server = server
mbligh67647152008-11-19 00:18:14 +000073 self.user = user
74 self.print_log = print_log
75 self.debug = debug
mbligh99b24f42009-06-08 16:45:55 +000076 self.reply_debug = reply_debug
Scott Zawalski347aaf42012-04-03 16:33:00 -040077 headers = {'AUTHORIZATION': self.user}
78 rpc_server = 'http://' + server + path
mbligh1354c9d2008-12-22 14:56:13 +000079 if debug:
80 print 'SERVER: %s' % rpc_server
81 print 'HEADERS: %s' % headers
mbligh67647152008-11-19 00:18:14 +000082 self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
83
84
85 def run(self, call, **dargs):
86 """
87 Make a RPC call to the AFE server
88 """
89 rpc_call = getattr(self.proxy, call)
90 if self.debug:
91 print 'DEBUG: %s %s' % (call, dargs)
mbligh451ede12009-02-12 21:54:03 +000092 try:
mbligh99b24f42009-06-08 16:45:55 +000093 result = utils.strip_unicode(rpc_call(**dargs))
94 if self.reply_debug:
95 print result
96 return result
mbligh451ede12009-02-12 21:54:03 +000097 except Exception:
98 print 'FAILED RPC CALL: %s %s' % (call, dargs)
99 raise
mbligh67647152008-11-19 00:18:14 +0000100
101
102 def log(self, message):
103 if self.print_log:
104 print message
105
106
jamesrenc3940222010-02-19 21:57:37 +0000107class Planner(RpcClient):
108 def __init__(self, user=None, server=None, print_log=True, debug=False,
109 reply_debug=False):
110 super(Planner, self).__init__(path='/planner/server/rpc/',
111 user=user,
112 server=server,
113 print_log=print_log,
114 debug=debug,
115 reply_debug=reply_debug)
116
117
mbligh5280e3b2008-12-22 14:39:28 +0000118class TKO(RpcClient):
mbligh99b24f42009-06-08 16:45:55 +0000119 def __init__(self, user=None, server=None, print_log=True, debug=False,
120 reply_debug=False):
Scott Zawalski347aaf42012-04-03 16:33:00 -0400121 super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000122 user=user,
123 server=server,
124 print_log=print_log,
125 debug=debug,
126 reply_debug=reply_debug)
Scott Zawalski63470dd2012-09-05 00:49:43 -0400127 self._db = None
128
129
130 def get_job_test_statuses_from_db(self, job_id):
131 """Get job test statuses from the database.
132
133 Retrieve a set of fields from a job that reflect the status of each test
134 run within a job.
135 fields retrieved: status, test_name, reason, test_started_time,
136 test_finished_time, afe_job_id, job_owner, hostname.
137
138 @param job_id: The afe job id to look up.
139 @returns a TestStatus object of the resulting information.
140 """
141 if self._db is None:
142 self._db = db.db()
143 fields = ['status', 'test_name', 'reason', 'test_started_time',
144 'test_finished_time', 'afe_job_id', 'job_owner', 'hostname']
145 table = 'tko_test_view_2'
146 where = 'job_tag like "%s-%%"' % job_id
147 test_status = []
148 # Run commit before we query to ensure that we are pulling the latest
149 # results.
150 self._db.commit()
151 for entry in self._db.select(','.join(fields), table, (where, None)):
152 status_dict = {}
153 for key,value in zip(fields, entry):
154 # All callers expect values to be a str object.
155 status_dict[key] = str(value)
156 # id is used by TestStatus to uniquely identify each Test Status
157 # obj.
158 status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
159 status_dict['test_name']]
160 test_status.append(status_dict)
161
162 return [TestStatus(self, e) for e in test_status]
mblighc31e4022008-12-11 19:32:30 +0000163
164
165 def get_status_counts(self, job, **data):
166 entries = self.run('get_status_counts',
mbligh1ef218d2009-08-03 16:57:56 +0000167 group_by=['hostname', 'test_name', 'reason'],
mblighc31e4022008-12-11 19:32:30 +0000168 job_tag__startswith='%s-' % job, **data)
mbligh5280e3b2008-12-22 14:39:28 +0000169 return [TestStatus(self, e) for e in entries['groups']]
mblighc31e4022008-12-11 19:32:30 +0000170
171
mbligh5280e3b2008-12-22 14:39:28 +0000172class AFE(RpcClient):
mbligh17c75e62009-06-08 16:18:21 +0000173 def __init__(self, user=None, server=None, print_log=True, debug=False,
mbligh99b24f42009-06-08 16:45:55 +0000174 reply_debug=False, job=None):
mbligh17c75e62009-06-08 16:18:21 +0000175 self.job = job
Scott Zawalski347aaf42012-04-03 16:33:00 -0400176 super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
mbligh99b24f42009-06-08 16:45:55 +0000177 user=user,
178 server=server,
179 print_log=print_log,
180 debug=debug,
181 reply_debug=reply_debug)
mblighc31e4022008-12-11 19:32:30 +0000182
mbligh1ef218d2009-08-03 16:57:56 +0000183
mbligh67647152008-11-19 00:18:14 +0000184 def host_statuses(self, live=None):
jamesren121eee62010-04-13 19:10:12 +0000185 dead_statuses = ['Repair Failed', 'Repairing']
mbligh67647152008-11-19 00:18:14 +0000186 statuses = self.run('get_static_data')['host_statuses']
187 if live == True:
mblighc2847b72009-03-25 19:32:20 +0000188 return list(set(statuses) - set(dead_statuses))
mbligh67647152008-11-19 00:18:14 +0000189 if live == False:
190 return dead_statuses
191 else:
192 return statuses
193
194
mbligh71094012009-12-19 05:35:21 +0000195 @staticmethod
196 def _dict_for_host_query(hostnames=(), status=None, label=None):
197 query_args = {}
mbligh4e545a52009-12-19 05:30:39 +0000198 if hostnames:
199 query_args['hostname__in'] = hostnames
200 if status:
201 query_args['status'] = status
202 if label:
203 query_args['labels__name'] = label
mbligh71094012009-12-19 05:35:21 +0000204 return query_args
205
206
207 def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
208 query_args = dict(dargs)
209 query_args.update(self._dict_for_host_query(hostnames=hostnames,
210 status=status,
211 label=label))
212 hosts = self.run('get_hosts', **query_args)
213 return [Host(self, h) for h in hosts]
214
215
216 def get_hostnames(self, status=None, label=None, **dargs):
217 """Like get_hosts() but returns hostnames instead of Host objects."""
218 # This implementation can be replaced with a more efficient one
219 # that does not query for entire host objects in the future.
220 return [host_obj.hostname for host_obj in
221 self.get_hosts(status=status, label=label, **dargs)]
222
223
224 def reverify_hosts(self, hostnames=(), status=None, label=None):
225 query_args = dict(locked=False,
226 aclgroup__users__login=self.user)
227 query_args.update(self._dict_for_host_query(hostnames=hostnames,
228 status=status,
229 label=label))
mbligh4e545a52009-12-19 05:30:39 +0000230 return self.run('reverify_hosts', **query_args)
231
232
mbligh67647152008-11-19 00:18:14 +0000233 def create_host(self, hostname, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000234 id = self.run('add_host', hostname=hostname, **dargs)
mbligh67647152008-11-19 00:18:14 +0000235 return self.get_hosts(id=id)[0]
236
237
Chris Masone8abb6fc2012-01-31 09:27:36 -0800238 def set_host_attribute(self, attr, val, **dargs):
239 self.run('set_host_attribute', attribute=attr, value=val, **dargs)
240
241
mbligh67647152008-11-19 00:18:14 +0000242 def get_labels(self, **dargs):
243 labels = self.run('get_labels', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000244 return [Label(self, l) for l in labels]
mbligh67647152008-11-19 00:18:14 +0000245
246
247 def create_label(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000248 id = self.run('add_label', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000249 return self.get_labels(id=id)[0]
250
251
252 def get_acls(self, **dargs):
253 acls = self.run('get_acl_groups', **dargs)
mbligh5280e3b2008-12-22 14:39:28 +0000254 return [Acl(self, a) for a in acls]
mbligh67647152008-11-19 00:18:14 +0000255
256
257 def create_acl(self, name, **dargs):
mbligh54459c72009-01-21 19:26:44 +0000258 id = self.run('add_acl_group', name=name, **dargs)
mbligh67647152008-11-19 00:18:14 +0000259 return self.get_acls(id=id)[0]
260
261
mbligh54459c72009-01-21 19:26:44 +0000262 def get_users(self, **dargs):
263 users = self.run('get_users', **dargs)
264 return [User(self, u) for u in users]
265
266
mbligh1354c9d2008-12-22 14:56:13 +0000267 def generate_control_file(self, tests, **dargs):
268 ret = self.run('generate_control_file', tests=tests, **dargs)
269 return ControlFile(self, ret)
270
271
mbligh67647152008-11-19 00:18:14 +0000272 def get_jobs(self, summary=False, **dargs):
273 if summary:
274 jobs_data = self.run('get_jobs_summary', **dargs)
275 else:
276 jobs_data = self.run('get_jobs', **dargs)
mblighafbba0c2009-06-08 16:44:45 +0000277 jobs = []
278 for j in jobs_data:
279 job = Job(self, j)
280 # Set up some extra information defaults
281 job.testname = re.sub('\s.*', '', job.name) # arbitrary default
282 job.platform_results = {}
283 job.platform_reasons = {}
284 jobs.append(job)
285 return jobs
mbligh67647152008-11-19 00:18:14 +0000286
287
288 def get_host_queue_entries(self, **data):
289 entries = self.run('get_host_queue_entries', **data)
mblighf9e35862009-02-26 01:03:11 +0000290 job_statuses = [JobStatus(self, e) for e in entries]
mbligh99b24f42009-06-08 16:45:55 +0000291
292 # Sadly, get_host_queue_entries doesn't return platforms, we have
293 # to get those back from an explicit get_hosts queury, then patch
294 # the new host objects back into the host list.
295 hostnames = [s.host.hostname for s in job_statuses if s.host]
296 host_hash = {}
297 for host in self.get_hosts(hostname__in=hostnames):
298 host_hash[host.hostname] = host
299 for status in job_statuses:
300 if status.host:
301 status.host = host_hash[status.host.hostname]
mblighf9e35862009-02-26 01:03:11 +0000302 # filter job statuses that have either host or meta_host
303 return [status for status in job_statuses if (status.host or
304 status.meta_host)]
mbligh67647152008-11-19 00:18:14 +0000305
306
mblighb9db5162009-04-17 22:21:41 +0000307 def create_job_by_test(self, tests, kernel=None, use_container=False,
Eric Lie0493a42010-11-15 13:05:43 -0800308 kernel_cmdline=None, **dargs):
mbligh67647152008-11-19 00:18:14 +0000309 """
310 Given a test name, fetch the appropriate control file from the server
mbligh4e576612008-12-22 14:56:36 +0000311 and submit it.
312
Eric Lie0493a42010-11-15 13:05:43 -0800313 @param kernel: A comma separated list of kernel versions to boot.
314 @param kernel_cmdline: The command line used to boot all kernels listed
315 in the kernel parameter.
316
mbligh4e576612008-12-22 14:56:36 +0000317 Returns a list of job objects
mbligh67647152008-11-19 00:18:14 +0000318 """
mblighb9db5162009-04-17 22:21:41 +0000319 assert ('hosts' in dargs or
320 'atomic_group_name' in dargs and 'synch_count' in dargs)
showarda2cd72b2009-10-01 18:43:53 +0000321 if kernel:
322 kernel_list = re.split('[\s,]+', kernel.strip())
Eric Lie0493a42010-11-15 13:05:43 -0800323 kernel_info = []
324 for version in kernel_list:
325 kernel_dict = {'version': version}
326 if kernel_cmdline is not None:
327 kernel_dict['cmdline'] = kernel_cmdline
328 kernel_info.append(kernel_dict)
showarda2cd72b2009-10-01 18:43:53 +0000329 else:
330 kernel_info = None
331 control_file = self.generate_control_file(
Dale Curtis74a314b2011-06-23 14:55:46 -0700332 tests=tests, kernel=kernel_info, use_container=use_container)
mbligh1354c9d2008-12-22 14:56:13 +0000333 if control_file.is_server:
mbligh67647152008-11-19 00:18:14 +0000334 dargs['control_type'] = 'Server'
335 else:
336 dargs['control_type'] = 'Client'
337 dargs['dependencies'] = dargs.get('dependencies', []) + \
mbligh1354c9d2008-12-22 14:56:13 +0000338 control_file.dependencies
339 dargs['control_file'] = control_file.control_file
mbligh672666c2009-07-28 23:22:13 +0000340 if not dargs.get('synch_count', None):
mblighc99fccf2009-07-11 00:59:33 +0000341 dargs['synch_count'] = control_file.synch_count
mblighb9db5162009-04-17 22:21:41 +0000342 if 'hosts' in dargs and len(dargs['hosts']) < dargs['synch_count']:
343 # will not be able to satisfy this request
mbligh38b09152009-04-28 18:34:25 +0000344 return None
345 return self.create_job(**dargs)
mbligh67647152008-11-19 00:18:14 +0000346
347
348 def create_job(self, control_file, name=' ', priority='Medium',
349 control_type='Client', **dargs):
350 id = self.run('create_job', name=name, priority=priority,
351 control_file=control_file, control_type=control_type, **dargs)
352 return self.get_jobs(id=id)[0]
353
354
mbligh282ce892010-01-06 18:40:17 +0000355 def run_test_suites(self, pairings, kernel, kernel_label=None,
356 priority='Medium', wait=True, poll_interval=10,
jamesren37d4a612010-06-04 22:30:56 +0000357 email_from=None, email_to=None, timeout=168,
Simran Basi34217022012-11-06 13:43:15 -0800358 max_runtime_mins=10080, kernel_cmdline=None):
mbligh5b618382008-12-03 15:24:01 +0000359 """
360 Run a list of test suites on a particular kernel.
mbligh1ef218d2009-08-03 16:57:56 +0000361
mbligh5b618382008-12-03 15:24:01 +0000362 Poll for them to complete, and return whether they worked or not.
mbligh1ef218d2009-08-03 16:57:56 +0000363
mbligh282ce892010-01-06 18:40:17 +0000364 @param pairings: List of MachineTestPairing objects to invoke.
365 @param kernel: Name of the kernel to run.
366 @param kernel_label: Label (string) of the kernel to run such as
367 '<kernel-version> : <config> : <date>'
368 If any pairing object has its job_label attribute set it
369 will override this value for that particular job.
Eric Lie0493a42010-11-15 13:05:43 -0800370 @param kernel_cmdline: The command line to boot the kernel(s) with.
mbligh282ce892010-01-06 18:40:17 +0000371 @param wait: boolean - Wait for the results to come back?
372 @param poll_interval: Interval between polling for job results (in mins)
373 @param email_from: Send notification email upon completion from here.
374 @param email_from: Send notification email upon completion to here.
mbligh5b618382008-12-03 15:24:01 +0000375 """
376 jobs = []
377 for pairing in pairings:
mbligh0c4f8d72009-05-12 20:52:18 +0000378 try:
379 new_job = self.invoke_test(pairing, kernel, kernel_label,
jamesren37d4a612010-06-04 22:30:56 +0000380 priority, timeout=timeout,
Eric Lie0493a42010-11-15 13:05:43 -0800381 kernel_cmdline=kernel_cmdline,
Simran Basi34217022012-11-06 13:43:15 -0800382 max_runtime_mins=max_runtime_mins)
mbligh0c4f8d72009-05-12 20:52:18 +0000383 if not new_job:
384 continue
mbligh0c4f8d72009-05-12 20:52:18 +0000385 jobs.append(new_job)
386 except Exception, e:
387 traceback.print_exc()
mblighb9db5162009-04-17 22:21:41 +0000388 if not wait or not jobs:
mbligh5b618382008-12-03 15:24:01 +0000389 return
mbligh5280e3b2008-12-22 14:39:28 +0000390 tko = TKO()
mbligh5b618382008-12-03 15:24:01 +0000391 while True:
392 time.sleep(60 * poll_interval)
mbligh5280e3b2008-12-22 14:39:28 +0000393 result = self.poll_all_jobs(tko, jobs, email_from, email_to)
mbligh5b618382008-12-03 15:24:01 +0000394 if result is not None:
395 return result
396
397
mbligh45ffc432008-12-09 23:35:17 +0000398 def result_notify(self, job, email_from, email_to):
mbligh5b618382008-12-03 15:24:01 +0000399 """
mbligh45ffc432008-12-09 23:35:17 +0000400 Notify about the result of a job. Will always print, if email data
401 is provided, will send email for it as well.
402
403 job: job object to notify about
404 email_from: send notification email upon completion from here
405 email_from: send notification email upon completion to here
406 """
407 if job.result == True:
408 subject = 'Testing PASSED: '
409 else:
410 subject = 'Testing FAILED: '
411 subject += '%s : %s\n' % (job.name, job.id)
412 text = []
413 for platform in job.results_platform_map:
414 for status in job.results_platform_map[platform]:
415 if status == 'Total':
416 continue
mbligh451ede12009-02-12 21:54:03 +0000417 for host in job.results_platform_map[platform][status]:
418 text.append('%20s %10s %10s' % (platform, status, host))
419 if status == 'Failed':
420 for test_status in job.test_status[host].fail:
421 text.append('(%s, %s) : %s' % \
422 (host, test_status.test_name,
423 test_status.reason))
424 text.append('')
mbligh37eceaa2008-12-15 22:56:37 +0000425
mbligh451ede12009-02-12 21:54:03 +0000426 base_url = 'http://' + self.server
mbligh37eceaa2008-12-15 22:56:37 +0000427
428 params = ('columns=test',
429 'rows=machine_group',
430 "condition=tag~'%s-%%25'" % job.id,
431 'title=Report')
432 query_string = '&'.join(params)
mbligh451ede12009-02-12 21:54:03 +0000433 url = '%s/tko/compose_query.cgi?%s' % (base_url, query_string)
434 text.append(url + '\n')
435 url = '%s/afe/#tab_id=view_job&object_id=%s' % (base_url, job.id)
436 text.append(url + '\n')
mbligh37eceaa2008-12-15 22:56:37 +0000437
438 body = '\n'.join(text)
439 print '---------------------------------------------------'
440 print 'Subject: ', subject
mbligh45ffc432008-12-09 23:35:17 +0000441 print body
mbligh37eceaa2008-12-15 22:56:37 +0000442 print '---------------------------------------------------'
mbligh45ffc432008-12-09 23:35:17 +0000443 if email_from and email_to:
mbligh37eceaa2008-12-15 22:56:37 +0000444 print 'Sending email ...'
mbligh45ffc432008-12-09 23:35:17 +0000445 utils.send_email(email_from, email_to, subject, body)
446 print
mbligh37eceaa2008-12-15 22:56:37 +0000447
mbligh45ffc432008-12-09 23:35:17 +0000448
mbligh1354c9d2008-12-22 14:56:13 +0000449 def print_job_result(self, job):
450 """
451 Print the result of a single job.
452 job: a job object
453 """
454 if job.result is None:
455 print 'PENDING',
456 elif job.result == True:
457 print 'PASSED',
458 elif job.result == False:
459 print 'FAILED',
mbligh912c3f32009-03-25 19:31:30 +0000460 elif job.result == "Abort":
461 print 'ABORT',
mbligh1354c9d2008-12-22 14:56:13 +0000462 print ' %s : %s' % (job.id, job.name)
463
464
mbligh451ede12009-02-12 21:54:03 +0000465 def poll_all_jobs(self, tko, jobs, email_from=None, email_to=None):
mbligh45ffc432008-12-09 23:35:17 +0000466 """
467 Poll all jobs in a list.
468 jobs: list of job objects to poll
469 email_from: send notification email upon completion from here
470 email_from: send notification email upon completion to here
471
472 Returns:
mbligh5b618382008-12-03 15:24:01 +0000473 a) All complete successfully (return True)
474 b) One or more has failed (return False)
475 c) Cannot tell yet (return None)
476 """
mbligh45ffc432008-12-09 23:35:17 +0000477 results = []
mbligh5b618382008-12-03 15:24:01 +0000478 for job in jobs:
mbligh676dcbe2009-06-15 21:57:27 +0000479 if getattr(job, 'result', None) is None:
Chris Masone6fed6462011-10-20 16:36:43 -0700480 job.result = self.poll_job_results(tko, job)
mbligh676dcbe2009-06-15 21:57:27 +0000481 if job.result is not None:
482 self.result_notify(job, email_from, email_to)
mbligh45ffc432008-12-09 23:35:17 +0000483
mbligh676dcbe2009-06-15 21:57:27 +0000484 results.append(job.result)
mbligh1354c9d2008-12-22 14:56:13 +0000485 self.print_job_result(job)
mbligh45ffc432008-12-09 23:35:17 +0000486
487 if None in results:
488 return None
mbligh912c3f32009-03-25 19:31:30 +0000489 elif False in results or "Abort" in results:
mbligh45ffc432008-12-09 23:35:17 +0000490 return False
491 else:
492 return True
mbligh5b618382008-12-03 15:24:01 +0000493
494
mbligh1f23f362008-12-22 14:46:12 +0000495 def _included_platform(self, host, platforms):
496 """
497 See if host's platforms matches any of the patterns in the included
498 platforms list.
499 """
500 if not platforms:
501 return True # No filtering of platforms
502 for platform in platforms:
503 if re.search(platform, host.platform):
504 return True
505 return False
506
507
mbligh7b312282009-01-07 16:45:43 +0000508 def invoke_test(self, pairing, kernel, kernel_label, priority='Medium',
Eric Lie0493a42010-11-15 13:05:43 -0800509 kernel_cmdline=None, **dargs):
mbligh5b618382008-12-03 15:24:01 +0000510 """
511 Given a pairing of a control file to a machine label, find all machines
512 with that label, and submit that control file to them.
mbligh1ef218d2009-08-03 16:57:56 +0000513
mbligh282ce892010-01-06 18:40:17 +0000514 @param kernel_label: Label (string) of the kernel to run such as
515 '<kernel-version> : <config> : <date>'
516 If any pairing object has its job_label attribute set it
517 will override this value for that particular job.
518
519 @returns A list of job objects.
mbligh5b618382008-12-03 15:24:01 +0000520 """
mbligh282ce892010-01-06 18:40:17 +0000521 # The pairing can override the job label.
522 if pairing.job_label:
523 kernel_label = pairing.job_label
mbligh5b618382008-12-03 15:24:01 +0000524 job_name = '%s : %s' % (pairing.machine_label, kernel_label)
525 hosts = self.get_hosts(multiple_labels=[pairing.machine_label])
mbligh1f23f362008-12-22 14:46:12 +0000526 platforms = pairing.platforms
527 hosts = [h for h in hosts if self._included_platform(h, platforms)]
mblighc2847b72009-03-25 19:32:20 +0000528 dead_statuses = self.host_statuses(live=False)
529 host_list = [h.hostname for h in hosts if h.status not in dead_statuses]
mbligh1f23f362008-12-22 14:46:12 +0000530 print 'HOSTS: %s' % host_list
mblighb9db5162009-04-17 22:21:41 +0000531 if pairing.atomic_group_sched:
mblighc99fccf2009-07-11 00:59:33 +0000532 dargs['synch_count'] = pairing.synch_count
mblighb9db5162009-04-17 22:21:41 +0000533 dargs['atomic_group_name'] = pairing.machine_label
534 else:
535 dargs['hosts'] = host_list
mbligh38b09152009-04-28 18:34:25 +0000536 new_job = self.create_job_by_test(name=job_name,
mbligh17c75e62009-06-08 16:18:21 +0000537 dependencies=[pairing.machine_label],
538 tests=[pairing.control_file],
539 priority=priority,
540 kernel=kernel,
Eric Lie0493a42010-11-15 13:05:43 -0800541 kernel_cmdline=kernel_cmdline,
mbligh17c75e62009-06-08 16:18:21 +0000542 use_container=pairing.container,
543 **dargs)
mbligh38b09152009-04-28 18:34:25 +0000544 if new_job:
mbligh17c75e62009-06-08 16:18:21 +0000545 if pairing.testname:
546 new_job.testname = pairing.testname
mbligh4e576612008-12-22 14:56:36 +0000547 print 'Invoked test %s : %s' % (new_job.id, job_name)
mbligh38b09152009-04-28 18:34:25 +0000548 return new_job
mbligh5b618382008-12-03 15:24:01 +0000549
550
mblighb9db5162009-04-17 22:21:41 +0000551 def _job_test_results(self, tko, job, debug, tests=[]):
mbligh5b618382008-12-03 15:24:01 +0000552 """
mbligh5280e3b2008-12-22 14:39:28 +0000553 Retrieve test results for a job
mbligh5b618382008-12-03 15:24:01 +0000554 """
mbligh5280e3b2008-12-22 14:39:28 +0000555 job.test_status = {}
556 try:
557 test_statuses = tko.get_status_counts(job=job.id)
558 except Exception:
559 print "Ignoring exception on poll job; RPC interface is flaky"
560 traceback.print_exc()
561 return
562
563 for test_status in test_statuses:
mbligh7479a182009-01-07 16:46:24 +0000564 # SERVER_JOB is buggy, and often gives false failures. Ignore it.
565 if test_status.test_name == 'SERVER_JOB':
566 continue
mblighb9db5162009-04-17 22:21:41 +0000567 # if tests is not empty, restrict list of test_statuses to tests
568 if tests and test_status.test_name not in tests:
569 continue
mbligh451ede12009-02-12 21:54:03 +0000570 if debug:
571 print test_status
mbligh5280e3b2008-12-22 14:39:28 +0000572 hostname = test_status.hostname
573 if hostname not in job.test_status:
574 job.test_status[hostname] = TestResults()
575 job.test_status[hostname].add(test_status)
576
577
mbligh451ede12009-02-12 21:54:03 +0000578 def _job_results_platform_map(self, job, debug):
mblighc9e427e2009-04-28 18:35:06 +0000579 # Figure out which hosts passed / failed / aborted in a job
580 # Creates a 2-dimensional hash, stored as job.results_platform_map
581 # 1st index - platform type (string)
582 # 2nd index - Status (string)
583 # 'Completed' / 'Failed' / 'Aborted'
584 # Data indexed by this hash is a list of hostnames (text strings)
mbligh5280e3b2008-12-22 14:39:28 +0000585 job.results_platform_map = {}
mbligh5b618382008-12-03 15:24:01 +0000586 try:
mbligh45ffc432008-12-09 23:35:17 +0000587 job_statuses = self.get_host_queue_entries(job=job.id)
mbligh5b618382008-12-03 15:24:01 +0000588 except Exception:
589 print "Ignoring exception on poll job; RPC interface is flaky"
590 traceback.print_exc()
591 return None
mbligh5280e3b2008-12-22 14:39:28 +0000592
mbligh5b618382008-12-03 15:24:01 +0000593 platform_map = {}
mbligh5280e3b2008-12-22 14:39:28 +0000594 job.job_status = {}
mbligh451ede12009-02-12 21:54:03 +0000595 job.metahost_index = {}
mbligh5b618382008-12-03 15:24:01 +0000596 for job_status in job_statuses:
mblighc9e427e2009-04-28 18:35:06 +0000597 # This is basically "for each host / metahost in the job"
mbligh451ede12009-02-12 21:54:03 +0000598 if job_status.host:
599 hostname = job_status.host.hostname
600 else: # This is a metahost
601 metahost = job_status.meta_host
602 index = job.metahost_index.get(metahost, 1)
603 job.metahost_index[metahost] = index + 1
604 hostname = '%s.%s' % (metahost, index)
mbligh5280e3b2008-12-22 14:39:28 +0000605 job.job_status[hostname] = job_status.status
mbligh5b618382008-12-03 15:24:01 +0000606 status = job_status.status
mbligh0ecbe632009-05-13 21:34:56 +0000607 # Skip hosts that failed verify or repair:
608 # that's a machine failure, not a job failure
mbligh451ede12009-02-12 21:54:03 +0000609 if hostname in job.test_status:
610 verify_failed = False
611 for failure in job.test_status[hostname].fail:
mbligh0ecbe632009-05-13 21:34:56 +0000612 if (failure.test_name == 'verify' or
613 failure.test_name == 'repair'):
mbligh451ede12009-02-12 21:54:03 +0000614 verify_failed = True
615 break
616 if verify_failed:
617 continue
mblighc9e427e2009-04-28 18:35:06 +0000618 if hostname in job.test_status and job.test_status[hostname].fail:
619 # If the any tests failed in the job, we want to mark the
620 # job result as failed, overriding the default job status.
621 if status != "Aborted": # except if it's an aborted job
622 status = 'Failed'
mbligh451ede12009-02-12 21:54:03 +0000623 if job_status.host:
624 platform = job_status.host.platform
625 else: # This is a metahost
626 platform = job_status.meta_host
mbligh5b618382008-12-03 15:24:01 +0000627 if platform not in platform_map:
628 platform_map[platform] = {'Total' : [hostname]}
629 else:
630 platform_map[platform]['Total'].append(hostname)
631 new_host_list = platform_map[platform].get(status, []) + [hostname]
632 platform_map[platform][status] = new_host_list
mbligh45ffc432008-12-09 23:35:17 +0000633 job.results_platform_map = platform_map
mbligh5280e3b2008-12-22 14:39:28 +0000634
635
mbligh17c75e62009-06-08 16:18:21 +0000636 def set_platform_results(self, test_job, platform, result):
637 """
638 Result must be None, 'FAIL', 'WARN' or 'GOOD'
639 """
640 if test_job.platform_results[platform] is not None:
641 # We're already done, and results recorded. This can't change later.
642 return
643 test_job.platform_results[platform] = result
644 # Note that self.job refers to the metajob we're IN, not the job
645 # that we're excuting from here.
646 testname = '%s.%s' % (test_job.testname, platform)
647 if self.job:
648 self.job.record(result, None, testname, status='')
649
Chris Masone6fed6462011-10-20 16:36:43 -0700650 def poll_job_results(self, tko, job, enough=1, debug=False):
mbligh5280e3b2008-12-22 14:39:28 +0000651 """
Chris Masone3a560bd2011-11-14 16:53:56 -0800652 Analyse all job results by platform
mbligh1ef218d2009-08-03 16:57:56 +0000653
Chris Masone3a560bd2011-11-14 16:53:56 -0800654 params:
655 tko: a TKO object representing the results DB.
656 job: the job to be examined.
Chris Masone6fed6462011-10-20 16:36:43 -0700657 enough: the acceptable delta between the number of completed
658 tests and the total number of tests.
Chris Masone3a560bd2011-11-14 16:53:56 -0800659 debug: enable debugging output.
660
661 returns:
Chris Masone6fed6462011-10-20 16:36:43 -0700662 False: if any platform has more than |enough| failures
663 None: if any platform has less than |enough| machines
Chris Masone3a560bd2011-11-14 16:53:56 -0800664 not yet Good.
Chris Masone6fed6462011-10-20 16:36:43 -0700665 True: if all platforms have at least |enough| machines
Chris Masone3a560bd2011-11-14 16:53:56 -0800666 Good.
mbligh5280e3b2008-12-22 14:39:28 +0000667 """
mbligh451ede12009-02-12 21:54:03 +0000668 self._job_test_results(tko, job, debug)
mblighe7fcf562009-05-21 01:43:17 +0000669 if job.test_status == {}:
670 return None
mbligh451ede12009-02-12 21:54:03 +0000671 self._job_results_platform_map(job, debug)
mbligh5280e3b2008-12-22 14:39:28 +0000672
mbligh5b618382008-12-03 15:24:01 +0000673 good_platforms = []
mbligh912c3f32009-03-25 19:31:30 +0000674 failed_platforms = []
675 aborted_platforms = []
mbligh5b618382008-12-03 15:24:01 +0000676 unknown_platforms = []
mbligh5280e3b2008-12-22 14:39:28 +0000677 platform_map = job.results_platform_map
mbligh5b618382008-12-03 15:24:01 +0000678 for platform in platform_map:
mbligh17c75e62009-06-08 16:18:21 +0000679 if not job.platform_results.has_key(platform):
680 # record test start, but there's no way to do this right now
681 job.platform_results[platform] = None
mbligh5b618382008-12-03 15:24:01 +0000682 total = len(platform_map[platform]['Total'])
683 completed = len(platform_map[platform].get('Completed', []))
mbligh912c3f32009-03-25 19:31:30 +0000684 failed = len(platform_map[platform].get('Failed', []))
685 aborted = len(platform_map[platform].get('Aborted', []))
mbligh17c75e62009-06-08 16:18:21 +0000686
mbligh1ef218d2009-08-03 16:57:56 +0000687 # We set up what we want to record here, but don't actually do
mbligh17c75e62009-06-08 16:18:21 +0000688 # it yet, until we have a decisive answer for this platform
689 if aborted or failed:
690 bad = aborted + failed
691 if (bad > 1) or (bad * 2 >= total):
692 platform_test_result = 'FAIL'
693 else:
694 platform_test_result = 'WARN'
695
Chris Masone6fed6462011-10-20 16:36:43 -0700696 if aborted > enough:
mbligh912c3f32009-03-25 19:31:30 +0000697 aborted_platforms.append(platform)
mbligh17c75e62009-06-08 16:18:21 +0000698 self.set_platform_results(job, platform, platform_test_result)
Chris Masone6fed6462011-10-20 16:36:43 -0700699 elif (failed * 2 >= total) or (failed > enough):
mbligh912c3f32009-03-25 19:31:30 +0000700 failed_platforms.append(platform)
mbligh17c75e62009-06-08 16:18:21 +0000701 self.set_platform_results(job, platform, platform_test_result)
Chris Masone6fed6462011-10-20 16:36:43 -0700702 elif (completed >= enough) and (completed + enough >= total):
mbligh5b618382008-12-03 15:24:01 +0000703 good_platforms.append(platform)
mbligh17c75e62009-06-08 16:18:21 +0000704 self.set_platform_results(job, platform, 'GOOD')
mbligh5b618382008-12-03 15:24:01 +0000705 else:
706 unknown_platforms.append(platform)
707 detail = []
708 for status in platform_map[platform]:
709 if status == 'Total':
710 continue
711 detail.append('%s=%s' % (status,platform_map[platform][status]))
712 if debug:
mbligh1ef218d2009-08-03 16:57:56 +0000713 print '%20s %d/%d %s' % (platform, completed, total,
mbligh5b618382008-12-03 15:24:01 +0000714 ' '.join(detail))
715 print
mbligh1ef218d2009-08-03 16:57:56 +0000716
mbligh912c3f32009-03-25 19:31:30 +0000717 if len(aborted_platforms) > 0:
mbligh5b618382008-12-03 15:24:01 +0000718 if debug:
mbligh17c75e62009-06-08 16:18:21 +0000719 print 'Result aborted - platforms: ',
720 print ' '.join(aborted_platforms)
mbligh912c3f32009-03-25 19:31:30 +0000721 return "Abort"
722 if len(failed_platforms) > 0:
723 if debug:
724 print 'Result bad - platforms: ' + ' '.join(failed_platforms)
mbligh5b618382008-12-03 15:24:01 +0000725 return False
726 if len(unknown_platforms) > 0:
727 if debug:
728 platform_list = ' '.join(unknown_platforms)
729 print 'Result unknown - platforms: ', platform_list
730 return None
731 if debug:
732 platform_list = ' '.join(good_platforms)
733 print 'Result good - all platforms passed: ', platform_list
734 return True
735
736
mbligh5280e3b2008-12-22 14:39:28 +0000737class TestResults(object):
738 """
739 Container class used to hold the results of the tests for a job
740 """
741 def __init__(self):
742 self.good = []
743 self.fail = []
mbligh451ede12009-02-12 21:54:03 +0000744 self.pending = []
mbligh5280e3b2008-12-22 14:39:28 +0000745
746
747 def add(self, result):
mbligh451ede12009-02-12 21:54:03 +0000748 if result.complete_count > result.pass_count:
749 self.fail.append(result)
750 elif result.incomplete_count > 0:
751 self.pending.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000752 else:
mbligh451ede12009-02-12 21:54:03 +0000753 self.good.append(result)
mbligh5280e3b2008-12-22 14:39:28 +0000754
755
756class RpcObject(object):
mbligh67647152008-11-19 00:18:14 +0000757 """
758 Generic object used to construct python objects from rpc calls
759 """
760 def __init__(self, afe, hash):
761 self.afe = afe
762 self.hash = hash
763 self.__dict__.update(hash)
764
765
766 def __str__(self):
767 return dump_object(self.__repr__(), self)
768
769
mbligh1354c9d2008-12-22 14:56:13 +0000770class ControlFile(RpcObject):
771 """
772 AFE control file object
773
774 Fields: synch_count, dependencies, control_file, is_server
775 """
776 def __repr__(self):
777 return 'CONTROL FILE: %s' % self.control_file
778
779
mbligh5280e3b2008-12-22 14:39:28 +0000780class Label(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000781 """
782 AFE label object
783
784 Fields:
785 name, invalid, platform, kernel_config, id, only_if_needed
786 """
787 def __repr__(self):
788 return 'LABEL: %s' % self.name
789
790
791 def add_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800792 return self.afe.run('label_add_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000793
794
795 def remove_hosts(self, hosts):
Chris Masone3a560bd2011-11-14 16:53:56 -0800796 return self.afe.run('label_remove_hosts', id=self.id, hosts=hosts)
mbligh67647152008-11-19 00:18:14 +0000797
798
mbligh5280e3b2008-12-22 14:39:28 +0000799class Acl(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000800 """
801 AFE acl object
802
803 Fields:
804 users, hosts, description, name, id
805 """
806 def __repr__(self):
807 return 'ACL: %s' % self.name
808
809
810 def add_hosts(self, hosts):
811 self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
812 return self.afe.run('acl_group_add_hosts', self.id, hosts)
813
814
815 def remove_hosts(self, hosts):
816 self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
817 return self.afe.run('acl_group_remove_hosts', self.id, hosts)
818
819
mbligh54459c72009-01-21 19:26:44 +0000820 def add_users(self, users):
821 self.afe.log('Adding users %s to ACL %s' % (users, self.name))
822 return self.afe.run('acl_group_add_users', id=self.name, users=users)
823
824
mbligh5280e3b2008-12-22 14:39:28 +0000825class Job(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000826 """
827 AFE job object
828
829 Fields:
830 name, control_file, control_type, synch_count, reboot_before,
831 run_verify, priority, email_list, created_on, dependencies,
832 timeout, owner, reboot_after, id
833 """
834 def __repr__(self):
835 return 'JOB: %s' % self.id
836
837
mbligh5280e3b2008-12-22 14:39:28 +0000838class JobStatus(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000839 """
840 AFE job_status object
841
842 Fields:
843 status, complete, deleted, meta_host, host, active, execution_subdir, id
844 """
845 def __init__(self, afe, hash):
846 # This should call super
847 self.afe = afe
848 self.hash = hash
849 self.__dict__.update(hash)
mbligh5280e3b2008-12-22 14:39:28 +0000850 self.job = Job(afe, self.job)
Dale Curtis8adf7892011-09-08 16:13:36 -0700851 if getattr(self, 'host'):
mbligh99b24f42009-06-08 16:45:55 +0000852 self.host = Host(afe, self.host)
mbligh67647152008-11-19 00:18:14 +0000853
854
855 def __repr__(self):
mbligh451ede12009-02-12 21:54:03 +0000856 if self.host and self.host.hostname:
857 hostname = self.host.hostname
858 else:
859 hostname = 'None'
860 return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
mbligh67647152008-11-19 00:18:14 +0000861
862
mbligh5280e3b2008-12-22 14:39:28 +0000863class Host(RpcObject):
mbligh67647152008-11-19 00:18:14 +0000864 """
865 AFE host object
866
867 Fields:
868 status, lock_time, locked_by, locked, hostname, invalid,
869 synch_id, labels, platform, protection, dirty, id
870 """
871 def __repr__(self):
872 return 'HOST OBJECT: %s' % self.hostname
873
874
875 def show(self):
876 labels = list(set(self.labels) - set([self.platform]))
877 print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
878 self.locked, self.platform,
879 ', '.join(labels))
880
881
mbligh54459c72009-01-21 19:26:44 +0000882 def delete(self):
883 return self.afe.run('delete_host', id=self.id)
884
885
mbligh6463c4b2009-01-30 00:33:37 +0000886 def modify(self, **dargs):
887 return self.afe.run('modify_host', id=self.id, **dargs)
888
889
mbligh67647152008-11-19 00:18:14 +0000890 def get_acls(self):
891 return self.afe.get_acls(hosts__hostname=self.hostname)
892
893
894 def add_acl(self, acl_name):
895 self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
896 return self.afe.run('acl_group_add_hosts', id=acl_name,
897 hosts=[self.hostname])
898
899
900 def remove_acl(self, acl_name):
901 self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
902 return self.afe.run('acl_group_remove_hosts', id=acl_name,
903 hosts=[self.hostname])
904
905
906 def get_labels(self):
907 return self.afe.get_labels(host__hostname__in=[self.hostname])
908
909
910 def add_labels(self, labels):
911 self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
912 return self.afe.run('host_add_labels', id=self.id, labels=labels)
913
914
915 def remove_labels(self, labels):
916 self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
917 return self.afe.run('host_remove_labels', id=self.id, labels=labels)
mbligh5b618382008-12-03 15:24:01 +0000918
919
mbligh54459c72009-01-21 19:26:44 +0000920class User(RpcObject):
921 def __repr__(self):
922 return 'USER: %s' % self.login
923
924
mbligh5280e3b2008-12-22 14:39:28 +0000925class TestStatus(RpcObject):
mblighc31e4022008-12-11 19:32:30 +0000926 """
927 TKO test status object
928
929 Fields:
930 test_idx, hostname, testname, id
931 complete_count, incomplete_count, group_count, pass_count
932 """
933 def __repr__(self):
934 return 'TEST STATUS: %s' % self.id
935
936
mbligh5b618382008-12-03 15:24:01 +0000937class MachineTestPairing(object):
938 """
939 Object representing the pairing of a machine label with a control file
mbligh1f23f362008-12-22 14:46:12 +0000940
941 machine_label: use machines from this label
942 control_file: use this control file (by name in the frontend)
943 platforms: list of rexeps to filter platforms by. [] => no filtering
mbligh282ce892010-01-06 18:40:17 +0000944 job_label: The label (name) to give to the autotest job launched
945 to run this pairing. '<kernel-version> : <config> : <date>'
mbligh5b618382008-12-03 15:24:01 +0000946 """
mbligh1354c9d2008-12-22 14:56:13 +0000947 def __init__(self, machine_label, control_file, platforms=[],
mbligh17c75e62009-06-08 16:18:21 +0000948 container=False, atomic_group_sched=False, synch_count=0,
mbligh282ce892010-01-06 18:40:17 +0000949 testname=None, job_label=None):
mbligh5b618382008-12-03 15:24:01 +0000950 self.machine_label = machine_label
951 self.control_file = control_file
mbligh1f23f362008-12-22 14:46:12 +0000952 self.platforms = platforms
mbligh1354c9d2008-12-22 14:56:13 +0000953 self.container = container
mblighb9db5162009-04-17 22:21:41 +0000954 self.atomic_group_sched = atomic_group_sched
955 self.synch_count = synch_count
mbligh17c75e62009-06-08 16:18:21 +0000956 self.testname = testname
mbligh282ce892010-01-06 18:40:17 +0000957 self.job_label = job_label
mbligh1354c9d2008-12-22 14:56:13 +0000958
959
960 def __repr__(self):
961 return '%s %s %s %s' % (self.machine_label, self.control_file,
962 self.platforms, self.container)