blob: d0ef9b70f8c405346df940d32ef1be406d810e9c [file] [log] [blame]
mblighbe630eb2008-08-01 16:41:48 +00001#
2# Copyright 2008 Google Inc. All Rights Reserved.
3
4"""
5The job module contains the objects and methods used to
6manage jobs in Autotest.
7
8The valid actions are:
9list: lists job(s)
10create: create a job
11abort: abort job(s)
12stat: detailed listing of job(s)
13
14The common options are:
15
16See topic_common.py for a High Level Design and Algorithm.
17"""
18
19import getpass, os, pwd, re, socket, sys
20from autotest_lib.cli import topic_common, action_common
21
22
23class job(topic_common.atest):
24 """Job class
25 atest job [create|list|stat|abort] <options>"""
26 usage_action = '[create|list|stat|abort]'
27 topic = msg_topic = 'job'
28 msg_items = '<job_ids>'
29
30
31 def _convert_status(self, results):
32 for result in results:
mbligh10a47332008-08-11 19:37:46 +000033 total = sum(result['status_counts'].values())
mbligh47dc4d22009-02-12 21:48:34 +000034 status = ['%s=%s(%.1f%%)' % (key, val, 100.0*float(val)/total)
mbligh10a47332008-08-11 19:37:46 +000035 for key, val in result['status_counts'].iteritems()]
mblighbe630eb2008-08-01 16:41:48 +000036 status.sort()
37 result['status_counts'] = ', '.join(status)
38
39
40class job_help(job):
41 """Just here to get the atest logic working.
42 Usage is set by its parent"""
43 pass
44
45
46class job_list_stat(action_common.atest_list, job):
mbligh9deeefa2009-05-01 23:11:08 +000047 def __init__(self):
48 super(job_list_stat, self).__init__()
49
50 self.topic_parse_info = topic_common.item_parse_info(
51 attribute_name='jobs',
52 use_leftover=True)
53
54
mblighbe630eb2008-08-01 16:41:48 +000055 def __split_jobs_between_ids_names(self):
56 job_ids = []
57 job_names = []
58
59 # Sort between job IDs and names
60 for job_id in self.jobs:
61 if job_id.isdigit():
62 job_ids.append(job_id)
63 else:
64 job_names.append(job_id)
65 return (job_ids, job_names)
66
67
68 def execute_on_ids_and_names(self, op, filters={},
69 check_results={'id__in': 'id',
70 'name__in': 'id'},
71 tag_id='id__in', tag_name='name__in'):
72 if not self.jobs:
73 # Want everything
74 return super(job_list_stat, self).execute(op=op, filters=filters)
75
76 all_jobs = []
77 (job_ids, job_names) = self.__split_jobs_between_ids_names()
78
79 for items, tag in [(job_ids, tag_id),
80 (job_names, tag_name)]:
81 if items:
82 new_filters = filters.copy()
83 new_filters[tag] = items
84 jobs = super(job_list_stat,
85 self).execute(op=op,
86 filters=new_filters,
87 check_results=check_results)
88 all_jobs.extend(jobs)
89
90 return all_jobs
91
92
93class job_list(job_list_stat):
94 """atest job list [<jobs>] [--all] [--running] [--user <username>]"""
95 def __init__(self):
96 super(job_list, self).__init__()
97 self.parser.add_option('-a', '--all', help='List jobs for all '
98 'users.', action='store_true', default=False)
99 self.parser.add_option('-r', '--running', help='List only running '
100 'jobs', action='store_true')
101 self.parser.add_option('-u', '--user', help='List jobs for given '
102 'user', type='string')
103
104
105 def parse(self):
mbligh9deeefa2009-05-01 23:11:08 +0000106 options, leftover = super(job_list, self).parse()
mblighbe630eb2008-08-01 16:41:48 +0000107 self.all = options.all
108 self.data['running'] = options.running
109 if options.user:
110 if options.all:
111 self.invalid_syntax('Only specify --all or --user, not both.')
112 else:
113 self.data['owner'] = options.user
114 elif not options.all and not self.jobs:
115 self.data['owner'] = getpass.getuser()
116
mbligh9deeefa2009-05-01 23:11:08 +0000117 return options, leftover
mblighbe630eb2008-08-01 16:41:48 +0000118
119
120 def execute(self):
121 return self.execute_on_ids_and_names(op='get_jobs_summary',
122 filters=self.data)
123
124
125 def output(self, results):
126 keys = ['id', 'owner', 'name', 'status_counts']
127 if self.verbose:
128 keys.extend(['priority', 'control_type', 'created_on'])
129 self._convert_status(results)
130 super(job_list, self).output(results, keys)
131
132
133
134class job_stat(job_list_stat):
135 """atest job stat <job>"""
136 usage_action = 'stat'
137
138 def __init__(self):
139 super(job_stat, self).__init__()
140 self.parser.add_option('-f', '--control-file',
141 help='Display the control file',
142 action='store_true', default=False)
143
144
145 def parse(self):
mbligh9deeefa2009-05-01 23:11:08 +0000146 options, leftover = super(job_stat, self).parse(req_items='jobs')
mblighbe630eb2008-08-01 16:41:48 +0000147 if not self.jobs:
148 self.invalid_syntax('Must specify at least one job.')
149
150 self.show_control_file = options.control_file
151
mbligh9deeefa2009-05-01 23:11:08 +0000152 return options, leftover
mblighbe630eb2008-08-01 16:41:48 +0000153
154
155 def _merge_results(self, summary, qes):
156 hosts_status = {}
157 for qe in qes:
158 if qe['host']:
159 job_id = qe['job']['id']
160 hostname = qe['host']['hostname']
161 hosts_status.setdefault(job_id,
162 {}).setdefault(qe['status'],
163 []).append(hostname)
164
165 for job in summary:
166 job_id = job['id']
167 if hosts_status.has_key(job_id):
168 this_job = hosts_status[job_id]
mbligh47dc4d22009-02-12 21:48:34 +0000169 host_per_status = ['%s=%s' %(status, ','.join(host))
mblighbe630eb2008-08-01 16:41:48 +0000170 for status, host in this_job.iteritems()]
171 job['hosts_status'] = ', '.join(host_per_status)
172 else:
173 job['hosts_status'] = ''
174 return summary
175
176
177 def execute(self):
178 summary = self.execute_on_ids_and_names(op='get_jobs_summary')
179
180 # Get the real hostnames
181 qes = self.execute_on_ids_and_names(op='get_host_queue_entries',
182 check_results={},
183 tag_id='job__in',
184 tag_name='job__name__in')
185
186 self._convert_status(summary)
187
188 return self._merge_results(summary, qes)
189
190
191 def output(self, results):
192 if not self.verbose:
193 keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status']
194 else:
195 keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status',
showard2bab8f42008-11-12 18:15:22 +0000196 'owner', 'control_type', 'synch_count', 'created_on',
showarda1e74b32009-05-12 17:32:04 +0000197 'run_verify', 'reboot_before', 'reboot_after',
198 'parse_failed_repair']
mblighbe630eb2008-08-01 16:41:48 +0000199
200 if self.show_control_file:
201 keys.append('control_file')
202
203 super(job_stat, self).output(results, keys)
204
205
206class job_create(action_common.atest_create, job):
207 """atest job create [--priority <Low|Medium|High|Urgent>]
mbligha212d712009-02-11 01:22:36 +0000208 [--synch_count] [--control-file </path/to/cfile>]
mblighbe630eb2008-08-01 16:41:48 +0000209 [--on-server] [--test <test1,test2>] [--kernel <http://kernel>]
210 [--mlist </path/to/machinelist>] [--machine <host1 host2 host3>]
showardb27f4ad2009-05-01 00:08:26 +0000211 [--labels <list of labels of machines to run on>]
showard21baa452008-10-21 00:08:39 +0000212 [--reboot_before <option>] [--reboot_after <option>]
mblighce348642009-02-12 21:50:39 +0000213 [--noverify] [--timeout <timeout>] [--one-time-hosts <hosts>]
showardb27f4ad2009-05-01 00:08:26 +0000214 [--email <email>] [--dependencies <labels this job is dependent on>]
showarda1e74b32009-05-12 17:32:04 +0000215 [--atomic_group <atomic group name>] [--parse-failed-repair <option>]
mblighae64d3a2008-10-15 04:13:52 +0000216 job_name
217
218 Creating a job is rather different from the other create operations,
219 so it only uses the __init__() and output() from its superclass.
220 """
mblighbe630eb2008-08-01 16:41:48 +0000221 op_action = 'create'
222 msg_items = 'job_name'
mblighbe630eb2008-08-01 16:41:48 +0000223
224 def __init__(self):
225 super(job_create, self).__init__()
226 self.hosts = []
227 self.ctrl_file_data = {}
228 self.data_item_key = 'name'
229 self.parser.add_option('-p', '--priority', help='Job priority (low, '
230 'medium, high, urgent), default=medium',
231 type='choice', choices=('low', 'medium', 'high',
232 'urgent'), default='medium')
showard7bce1022008-11-14 22:51:05 +0000233 self.parser.add_option('-y', '--synch_count', type=int,
showard2bab8f42008-11-12 18:15:22 +0000234 help='Number of machines to use per autoserv '
mbligh7ffdb8b2009-01-21 19:01:51 +0000235 'execution')
mblighbe630eb2008-08-01 16:41:48 +0000236 self.parser.add_option('-f', '--control-file',
237 help='use this control file', metavar='FILE')
238 self.parser.add_option('-s', '--server',
239 help='This is server-side job',
240 action='store_true', default=False)
241 self.parser.add_option('-t', '--test',
mbligh51148c72008-08-11 20:23:58 +0000242 help='List of tests to run')
mblighbe630eb2008-08-01 16:41:48 +0000243 self.parser.add_option('-k', '--kernel', help='Install kernel from this'
244 ' URL before beginning job')
showardb27f4ad2009-05-01 00:08:26 +0000245 self.parser.add_option('-d', '--dependencies', help='Comma separated '
246 'list of labels this job is dependent on.',
247 default='')
mblighb9a8b162008-10-29 16:47:29 +0000248 self.parser.add_option('-b', '--labels', help='Comma separated list of '
showardb27f4ad2009-05-01 00:08:26 +0000249 'labels to get machine list from.', default='')
showard648a35c2009-05-01 00:08:42 +0000250 self.parser.add_option('-G', '--atomic_group', help='Name of an Atomic '
251 'Group to schedule this job on.',
252 default='')
mblighbe630eb2008-08-01 16:41:48 +0000253 self.parser.add_option('-m', '--machine', help='List of machines to '
254 'run on')
255 self.parser.add_option('-M', '--mlist',
256 help='File listing machines to use',
257 type='string', metavar='MACHINE_FLIST')
mblighce348642009-02-12 21:50:39 +0000258 self.parser.add_option('--one-time-hosts',
259 help='List of one time hosts')
mbligh6fee7fd2008-10-10 15:44:39 +0000260 self.parser.add_option('-e', '--email', help='A comma seperated list '
261 'of email addresses to notify of job completion',
262 default='')
mblighb9a8b162008-10-29 16:47:29 +0000263 self.parser.add_option('-B', '--reboot_before',
showard21baa452008-10-21 00:08:39 +0000264 help='Whether or not to reboot the machine '
265 'before the job (never/if dirty/always)',
266 type='choice',
267 choices=('never', 'if dirty', 'always'))
268 self.parser.add_option('-a', '--reboot_after',
269 help='Whether or not to reboot the machine '
270 'after the job (never/if all tests passed/'
271 'always)',
272 type='choice',
273 choices=('never', 'if all tests passed',
274 'always'))
showarda1e74b32009-05-12 17:32:04 +0000275 self.parser.add_option('--parse-failed-repair',
276 help='Whether or not to parse failed repair '
277 'results as part of the job',
278 type='choice',
279 choices=('true', 'false'))
mblighfb8f0ab2008-11-13 01:11:48 +0000280 self.parser.add_option('-l', '--clone', help='Clone an existing job. '
281 'This will discard all other options except '
282 '--reuse-hosts.', default=False,
283 metavar='JOB_ID')
284 self.parser.add_option('-r', '--reuse-hosts', help='Use the exact same '
285 'hosts as cloned job. Only for use with '
286 '--clone.', action='store_true', default=False)
mbligh5d0b4b32008-12-22 14:43:01 +0000287 self.parser.add_option('-n', '--noverify',
288 help='Do not run verify for job',
289 default=False, action='store_true')
290 self.parser.add_option('-o', '--timeout', help='Job timeout in hours.',
291 metavar='TIMEOUT')
mblighbe630eb2008-08-01 16:41:48 +0000292
293
294 def parse(self):
mbligh9deeefa2009-05-01 23:11:08 +0000295 host_info = topic_common.item_parse_info(attribute_name='hosts',
296 inline_option='machine',
297 filename_option='mlist')
298 job_info = topic_common.item_parse_info(attribute_name='jobname',
299 use_leftover=True)
300 oth_info = topic_common.item_parse_info(attribute_name='one_time_hosts',
301 inline_option='one_time_hosts')
302
303 options, leftover = super(job_create,
304 self).parse([host_info, job_info, oth_info],
305 req_items='jobname')
mblighbe630eb2008-08-01 16:41:48 +0000306 self.data = {}
mblighfb8f0ab2008-11-13 01:11:48 +0000307 if len(self.jobname) > 1:
308 self.invalid_syntax('Too many arguments specified, only expected '
309 'to receive job name: %s' % self.jobname)
310 self.jobname = self.jobname[0]
311
312 if options.reuse_hosts and not options.clone:
313 self.invalid_syntax('--reuse-hosts only to be used with --clone.')
314 # If cloning skip parse, parsing is done in execute
315 self.clone_id = options.clone
316 if options.clone:
317 self.op_action = 'clone'
318 self.msg_items = 'jobid'
319 self.reuse_hosts = options.reuse_hosts
mbligh9deeefa2009-05-01 23:11:08 +0000320 return options, leftover
mblighbe630eb2008-08-01 16:41:48 +0000321
mbligh9deeefa2009-05-01 23:11:08 +0000322 if (len(self.hosts) == 0 and not self.one_time_hosts
showard648a35c2009-05-01 00:08:42 +0000323 and not options.labels and not options.atomic_group):
mblighce348642009-02-12 21:50:39 +0000324 self.invalid_syntax('Must specify at least one machine '
showard648a35c2009-05-01 00:08:42 +0000325 'or an atomic group '
326 '(-m, -M, -b, -G or --one-time-hosts).')
mblighbe630eb2008-08-01 16:41:48 +0000327 if not options.control_file and not options.test:
328 self.invalid_syntax('Must specify either --test or --control-file'
329 ' to create a job.')
330 if options.control_file and options.test:
331 self.invalid_syntax('Can only specify one of --control-file or '
332 '--test, not both.')
mbligh120351e2009-01-24 01:40:45 +0000333 if options.kernel:
334 self.ctrl_file_data['kernel'] = options.kernel
335 self.ctrl_file_data['do_push_packages'] = True
mblighbe630eb2008-08-01 16:41:48 +0000336 if options.control_file:
mblighbe630eb2008-08-01 16:41:48 +0000337 try:
mbligh120351e2009-01-24 01:40:45 +0000338 control_file_f = open(options.control_file)
339 try:
340 control_file_data = control_file_f.read()
341 finally:
342 control_file_f.close()
mblighbe630eb2008-08-01 16:41:48 +0000343 except IOError:
344 self.generic_error('Unable to read from specified '
345 'control-file: %s' % options.control_file)
mbligh120351e2009-01-24 01:40:45 +0000346 if options.kernel:
347 if options.server:
348 self.invalid_syntax(
349 'A control file and a kernel may only be specified'
350 ' together on client side jobs.')
351 # execute() will pass this to the AFE server to wrap this
352 # control file up to include the kernel installation steps.
353 self.ctrl_file_data['client_control_file'] = control_file_data
354 else:
355 self.data['control_file'] = control_file_data
mbligh4eae22a2008-10-10 16:09:46 +0000356 if options.test:
showard2bab8f42008-11-12 18:15:22 +0000357 if options.server:
mblighb9a8b162008-10-29 16:47:29 +0000358 self.invalid_syntax('If you specify tests, then the '
showard2bab8f42008-11-12 18:15:22 +0000359 'client/server setting is implicit and '
360 'cannot be overriden.')
mbligh4eae22a2008-10-10 16:09:46 +0000361 tests = [t.strip() for t in options.test.split(',') if t.strip()]
mbligh120351e2009-01-24 01:40:45 +0000362 self.ctrl_file_data['tests'] = tests
mbligh4eae22a2008-10-10 16:09:46 +0000363
mblighbe630eb2008-08-01 16:41:48 +0000364
365 if options.priority:
366 self.data['priority'] = options.priority.capitalize()
showard21baa452008-10-21 00:08:39 +0000367 if options.reboot_before:
368 self.data['reboot_before'] = options.reboot_before.capitalize()
369 if options.reboot_after:
370 self.data['reboot_after'] = options.reboot_after.capitalize()
showarda1e74b32009-05-12 17:32:04 +0000371 if options.parse_failed_repair:
372 self.data['parse_failed_repair'] = (
373 options.parse_failed_repair == 'true')
mbligh5d0b4b32008-12-22 14:43:01 +0000374 if options.noverify:
375 self.data['run_verify'] = False
376 if options.timeout:
377 self.data['timeout'] = options.timeout
mblighbe630eb2008-08-01 16:41:48 +0000378
mbligh9deeefa2009-05-01 23:11:08 +0000379 if self.one_time_hosts:
380 self.data['one_time_hosts'] = self.one_time_hosts
showardb27f4ad2009-05-01 00:08:26 +0000381 if options.labels:
382 labels = options.labels.split(',')
383 labels = [label.strip() for label in labels if label.strip()]
384 label_hosts = self.execute_rpc(op='get_hosts',
385 multiple_labels=labels)
386 for host in label_hosts:
387 self.hosts.append(host['hostname'])
mblighce348642009-02-12 21:50:39 +0000388
mblighbe630eb2008-08-01 16:41:48 +0000389 self.data['name'] = self.jobname
390
391 (self.data['hosts'],
392 self.data['meta_hosts']) = self.parse_hosts(self.hosts)
393
showard648a35c2009-05-01 00:08:42 +0000394 if options.atomic_group:
395 self.data['atomic_group_name'] = options.atomic_group
396
showardb27f4ad2009-05-01 00:08:26 +0000397 deps = options.dependencies.split(',')
mblighb9a8b162008-10-29 16:47:29 +0000398 deps = [dep.strip() for dep in deps if dep.strip()]
399 self.data['dependencies'] = deps
mblighbe630eb2008-08-01 16:41:48 +0000400
mbligh6fee7fd2008-10-10 15:44:39 +0000401 self.data['email_list'] = options.email
mbligh7ffdb8b2009-01-21 19:01:51 +0000402 if options.synch_count:
403 self.data['synch_count'] = options.synch_count
mblighbe630eb2008-08-01 16:41:48 +0000404 if options.server:
405 self.data['control_type'] = 'Server'
406 else:
407 self.data['control_type'] = 'Client'
408
mbligh9deeefa2009-05-01 23:11:08 +0000409 return options, leftover
mblighbe630eb2008-08-01 16:41:48 +0000410
411
412 def execute(self):
413 if self.ctrl_file_data:
mbligh120351e2009-01-24 01:40:45 +0000414 uploading_kernel = 'kernel' in self.ctrl_file_data
415 if uploading_kernel:
mbligh8c7b04c2009-03-25 18:01:56 +0000416 default_timeout = socket.getdefaulttimeout()
mblighbe630eb2008-08-01 16:41:48 +0000417 socket.setdefaulttimeout(topic_common.UPLOAD_SOCKET_TIMEOUT)
418 print 'Uploading Kernel: this may take a while...',
mbligh120351e2009-01-24 01:40:45 +0000419 sys.stdout.flush()
420 try:
421 cf_info = self.execute_rpc(op='generate_control_file',
422 item=self.jobname,
423 **self.ctrl_file_data)
424 finally:
425 if uploading_kernel:
mbligh8c7b04c2009-03-25 18:01:56 +0000426 socket.setdefaulttimeout(default_timeout)
427
mbligh120351e2009-01-24 01:40:45 +0000428 if uploading_kernel:
mblighbe630eb2008-08-01 16:41:48 +0000429 print 'Done'
showard989f25d2008-10-01 11:38:11 +0000430 self.data['control_file'] = cf_info['control_file']
mbligh7ffdb8b2009-01-21 19:01:51 +0000431 if 'synch_count' not in self.data:
432 self.data['synch_count'] = cf_info['synch_count']
showard989f25d2008-10-01 11:38:11 +0000433 if cf_info['is_server']:
mblighbe630eb2008-08-01 16:41:48 +0000434 self.data['control_type'] = 'Server'
435 else:
436 self.data['control_type'] = 'Client'
mblighae64d3a2008-10-15 04:13:52 +0000437
mblighb9a8b162008-10-29 16:47:29 +0000438 # Get the union of the 2 sets of dependencies
439 deps = set(self.data['dependencies'])
showarda6fe9c62008-11-03 19:04:25 +0000440 deps = sorted(deps.union(cf_info['dependencies']))
mblighb9a8b162008-10-29 16:47:29 +0000441 self.data['dependencies'] = list(deps)
mblighae64d3a2008-10-15 04:13:52 +0000442
mbligh7ffdb8b2009-01-21 19:01:51 +0000443 if 'synch_count' not in self.data:
444 self.data['synch_count'] = 1
445
mblighfb8f0ab2008-11-13 01:11:48 +0000446 if self.clone_id:
447 clone_info = self.execute_rpc(op='get_info_for_clone',
448 id=self.clone_id,
449 preserve_metahosts=self.reuse_hosts)
450 self.data = clone_info['job']
451
452 # Remove fields from clone data that cannot be reused
453 unused_fields = ('name', 'created_on', 'id', 'owner')
454 for field in unused_fields:
455 del self.data[field]
456
457 # Keyword args cannot be unicode strings
458 for key, val in self.data.iteritems():
459 del self.data[key]
460 self.data[str(key)] = val
461
462 # Convert host list from clone info that can be used for job_create
463 host_list = []
464 if clone_info['meta_host_counts']:
465 # Creates a dictionary of meta_hosts, e.g.
466 # {u'label1': 3, u'label2': 2, u'label3': 5}
467 meta_hosts = clone_info['meta_host_counts']
468 # Create a list of formatted metahosts, e.g.
469 # [u'3*label1', u'2*label2', u'5*label3']
470 meta_host_list = ['%s*%s' % (str(val), key) for key,val in
471 meta_hosts.items()]
472 host_list.extend(meta_host_list)
473 if clone_info['hosts']:
474 # Creates a list of hosts, e.g. [u'host1', u'host2']
475 hosts = [host['hostname'] for host in clone_info['hosts']]
476 host_list.extend(hosts)
477
478 (self.data['hosts'],
479 self.data['meta_hosts']) = self.parse_hosts(host_list)
480 self.data['name'] = self.jobname
481
mblighae64d3a2008-10-15 04:13:52 +0000482 job_id = self.execute_rpc(op='create_job', **self.data)
483 return ['%s (id %s)' % (self.jobname, job_id)]
mblighbe630eb2008-08-01 16:41:48 +0000484
485
486 def get_items(self):
487 return [self.jobname]
488
489
490class job_abort(job, action_common.atest_delete):
491 """atest job abort <job(s)>"""
492 usage_action = op_action = 'abort'
493 msg_done = 'Aborted'
494
495 def parse(self):
mbligh9deeefa2009-05-01 23:11:08 +0000496 job_info = topic_common.item_parse_info(attribute_name='jobids',
497 use_leftover=True)
498 options, leftover = super(job_abort, self).parse([job_info],
499 req_items='jobids')
mblighbe630eb2008-08-01 16:41:48 +0000500
501
mbligh206d50a2008-11-13 01:19:25 +0000502 def execute(self):
503 data = {'job__id__in': self.jobids}
504 self.execute_rpc(op='abort_host_queue_entries', **data)
505 print 'Aborting jobs: %s' % ', '.join(self.jobids)
506
507
mblighbe630eb2008-08-01 16:41:48 +0000508 def get_items(self):
509 return self.jobids