blob: d0c4a467e00f0a23862a7465e45a9614a298600a [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())
34 status = ['%s:%s(%.1f%%)' % (key, val, 100.0*float(val)/total)
35 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):
47 def __split_jobs_between_ids_names(self):
48 job_ids = []
49 job_names = []
50
51 # Sort between job IDs and names
52 for job_id in self.jobs:
53 if job_id.isdigit():
54 job_ids.append(job_id)
55 else:
56 job_names.append(job_id)
57 return (job_ids, job_names)
58
59
60 def execute_on_ids_and_names(self, op, filters={},
61 check_results={'id__in': 'id',
62 'name__in': 'id'},
63 tag_id='id__in', tag_name='name__in'):
64 if not self.jobs:
65 # Want everything
66 return super(job_list_stat, self).execute(op=op, filters=filters)
67
68 all_jobs = []
69 (job_ids, job_names) = self.__split_jobs_between_ids_names()
70
71 for items, tag in [(job_ids, tag_id),
72 (job_names, tag_name)]:
73 if items:
74 new_filters = filters.copy()
75 new_filters[tag] = items
76 jobs = super(job_list_stat,
77 self).execute(op=op,
78 filters=new_filters,
79 check_results=check_results)
80 all_jobs.extend(jobs)
81
82 return all_jobs
83
84
85class job_list(job_list_stat):
86 """atest job list [<jobs>] [--all] [--running] [--user <username>]"""
87 def __init__(self):
88 super(job_list, self).__init__()
89 self.parser.add_option('-a', '--all', help='List jobs for all '
90 'users.', action='store_true', default=False)
91 self.parser.add_option('-r', '--running', help='List only running '
92 'jobs', action='store_true')
93 self.parser.add_option('-u', '--user', help='List jobs for given '
94 'user', type='string')
95
96
97 def parse(self):
98 (options, leftover) = self.parse_with_flist([('jobs', '', '', True)],
99 None)
100 self.all = options.all
101 self.data['running'] = options.running
102 if options.user:
103 if options.all:
104 self.invalid_syntax('Only specify --all or --user, not both.')
105 else:
106 self.data['owner'] = options.user
107 elif not options.all and not self.jobs:
108 self.data['owner'] = getpass.getuser()
109
110 return (options, leftover)
111
112
113 def execute(self):
114 return self.execute_on_ids_and_names(op='get_jobs_summary',
115 filters=self.data)
116
117
118 def output(self, results):
119 keys = ['id', 'owner', 'name', 'status_counts']
120 if self.verbose:
121 keys.extend(['priority', 'control_type', 'created_on'])
122 self._convert_status(results)
123 super(job_list, self).output(results, keys)
124
125
126
127class job_stat(job_list_stat):
128 """atest job stat <job>"""
129 usage_action = 'stat'
130
131 def __init__(self):
132 super(job_stat, self).__init__()
133 self.parser.add_option('-f', '--control-file',
134 help='Display the control file',
135 action='store_true', default=False)
136
137
138 def parse(self):
139 (options, leftover) = self.parse_with_flist(flists=[('jobs', '', '',
140 True)],
141 req_items='jobs')
142 if not self.jobs:
143 self.invalid_syntax('Must specify at least one job.')
144
145 self.show_control_file = options.control_file
146
147 return (options, leftover)
148
149
150 def _merge_results(self, summary, qes):
151 hosts_status = {}
152 for qe in qes:
153 if qe['host']:
154 job_id = qe['job']['id']
155 hostname = qe['host']['hostname']
156 hosts_status.setdefault(job_id,
157 {}).setdefault(qe['status'],
158 []).append(hostname)
159
160 for job in summary:
161 job_id = job['id']
162 if hosts_status.has_key(job_id):
163 this_job = hosts_status[job_id]
164 host_per_status = ['%s:%s' %(status, ','.join(host))
165 for status, host in this_job.iteritems()]
166 job['hosts_status'] = ', '.join(host_per_status)
167 else:
168 job['hosts_status'] = ''
169 return summary
170
171
172 def execute(self):
173 summary = self.execute_on_ids_and_names(op='get_jobs_summary')
174
175 # Get the real hostnames
176 qes = self.execute_on_ids_and_names(op='get_host_queue_entries',
177 check_results={},
178 tag_id='job__in',
179 tag_name='job__name__in')
180
181 self._convert_status(summary)
182
183 return self._merge_results(summary, qes)
184
185
186 def output(self, results):
187 if not self.verbose:
188 keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status']
189 else:
190 keys = ['id', 'name', 'priority', 'status_counts', 'hosts_status',
showard2bab8f42008-11-12 18:15:22 +0000191 'owner', 'control_type', 'synch_count', 'created_on',
showard21baa452008-10-21 00:08:39 +0000192 'run_verify', 'reboot_before', 'reboot_after']
mblighbe630eb2008-08-01 16:41:48 +0000193
194 if self.show_control_file:
195 keys.append('control_file')
196
197 super(job_stat, self).output(results, keys)
198
199
200class job_create(action_common.atest_create, job):
201 """atest job create [--priority <Low|Medium|High|Urgent>]
showard2bab8f42008-11-12 18:15:22 +0000202 [--synch_count] [--container] [--control-file </path/to/cfile>]
mblighbe630eb2008-08-01 16:41:48 +0000203 [--on-server] [--test <test1,test2>] [--kernel <http://kernel>]
204 [--mlist </path/to/machinelist>] [--machine <host1 host2 host3>]
mblighb9a8b162008-10-29 16:47:29 +0000205 [--labels <labels this job is dependent on>]
showard21baa452008-10-21 00:08:39 +0000206 [--reboot_before <option>] [--reboot_after <option>]
mbligh5d0b4b32008-12-22 14:43:01 +0000207 [--noverify] [--timeout <timeout>]
mblighae64d3a2008-10-15 04:13:52 +0000208 job_name
209
210 Creating a job is rather different from the other create operations,
211 so it only uses the __init__() and output() from its superclass.
212 """
mblighbe630eb2008-08-01 16:41:48 +0000213 op_action = 'create'
214 msg_items = 'job_name'
mblighbe630eb2008-08-01 16:41:48 +0000215
216 def __init__(self):
217 super(job_create, self).__init__()
218 self.hosts = []
219 self.ctrl_file_data = {}
220 self.data_item_key = 'name'
221 self.parser.add_option('-p', '--priority', help='Job priority (low, '
222 'medium, high, urgent), default=medium',
223 type='choice', choices=('low', 'medium', 'high',
224 'urgent'), default='medium')
showard7bce1022008-11-14 22:51:05 +0000225 self.parser.add_option('-y', '--synch_count', type=int,
showard2bab8f42008-11-12 18:15:22 +0000226 help='Number of machines to use per autoserv '
mbligh7ffdb8b2009-01-21 19:01:51 +0000227 'execution')
mblighbe630eb2008-08-01 16:41:48 +0000228 self.parser.add_option('-c', '--container', help='Run this client job '
229 'in a container', action='store_true',
230 default=False)
231 self.parser.add_option('-f', '--control-file',
232 help='use this control file', metavar='FILE')
233 self.parser.add_option('-s', '--server',
234 help='This is server-side job',
235 action='store_true', default=False)
236 self.parser.add_option('-t', '--test',
mbligh51148c72008-08-11 20:23:58 +0000237 help='List of tests to run')
mblighbe630eb2008-08-01 16:41:48 +0000238 self.parser.add_option('-k', '--kernel', help='Install kernel from this'
239 ' URL before beginning job')
mblighb9a8b162008-10-29 16:47:29 +0000240 self.parser.add_option('-b', '--labels', help='Comma separated list of '
241 'labels this job is dependent on.', default='')
mblighbe630eb2008-08-01 16:41:48 +0000242 self.parser.add_option('-m', '--machine', help='List of machines to '
243 'run on')
244 self.parser.add_option('-M', '--mlist',
245 help='File listing machines to use',
246 type='string', metavar='MACHINE_FLIST')
mbligh6fee7fd2008-10-10 15:44:39 +0000247 self.parser.add_option('-e', '--email', help='A comma seperated list '
248 'of email addresses to notify of job completion',
249 default='')
mblighb9a8b162008-10-29 16:47:29 +0000250 self.parser.add_option('-B', '--reboot_before',
showard21baa452008-10-21 00:08:39 +0000251 help='Whether or not to reboot the machine '
252 'before the job (never/if dirty/always)',
253 type='choice',
254 choices=('never', 'if dirty', 'always'))
255 self.parser.add_option('-a', '--reboot_after',
256 help='Whether or not to reboot the machine '
257 'after the job (never/if all tests passed/'
258 'always)',
259 type='choice',
260 choices=('never', 'if all tests passed',
261 'always'))
mblighfb8f0ab2008-11-13 01:11:48 +0000262 self.parser.add_option('-l', '--clone', help='Clone an existing job. '
263 'This will discard all other options except '
264 '--reuse-hosts.', default=False,
265 metavar='JOB_ID')
266 self.parser.add_option('-r', '--reuse-hosts', help='Use the exact same '
267 'hosts as cloned job. Only for use with '
268 '--clone.', action='store_true', default=False)
mbligh5d0b4b32008-12-22 14:43:01 +0000269 self.parser.add_option('-n', '--noverify',
270 help='Do not run verify for job',
271 default=False, action='store_true')
272 self.parser.add_option('-o', '--timeout', help='Job timeout in hours.',
273 metavar='TIMEOUT')
mblighbe630eb2008-08-01 16:41:48 +0000274
275
276 def parse(self):
277 flists = [('hosts', 'mlist', 'machine', False),
278 ('jobname', '', '', True)]
279 (options, leftover) = self.parse_with_flist(flists,
280 req_items='jobname')
281 self.data = {}
mblighfb8f0ab2008-11-13 01:11:48 +0000282 if len(self.jobname) > 1:
283 self.invalid_syntax('Too many arguments specified, only expected '
284 'to receive job name: %s' % self.jobname)
285 self.jobname = self.jobname[0]
286
287 if options.reuse_hosts and not options.clone:
288 self.invalid_syntax('--reuse-hosts only to be used with --clone.')
289 # If cloning skip parse, parsing is done in execute
290 self.clone_id = options.clone
291 if options.clone:
292 self.op_action = 'clone'
293 self.msg_items = 'jobid'
294 self.reuse_hosts = options.reuse_hosts
295 return (options, leftover)
mblighbe630eb2008-08-01 16:41:48 +0000296
297 if len(self.hosts) == 0:
mbligh120351e2009-01-24 01:40:45 +0000298 self.invalid_syntax('Must specify at least one machine (-m or -M).')
mblighbe630eb2008-08-01 16:41:48 +0000299 if not options.control_file and not options.test:
300 self.invalid_syntax('Must specify either --test or --control-file'
301 ' to create a job.')
302 if options.control_file and options.test:
303 self.invalid_syntax('Can only specify one of --control-file or '
304 '--test, not both.')
305 if options.container and options.server:
306 self.invalid_syntax('Containers (--container) can only be added to'
307 ' client side jobs.')
mbligh120351e2009-01-24 01:40:45 +0000308 if options.container:
309 self.ctrl_file_data['use_container'] = options.container
310 if options.kernel:
311 self.ctrl_file_data['kernel'] = options.kernel
312 self.ctrl_file_data['do_push_packages'] = True
mblighbe630eb2008-08-01 16:41:48 +0000313 if options.control_file:
mblighbe630eb2008-08-01 16:41:48 +0000314 try:
mbligh120351e2009-01-24 01:40:45 +0000315 control_file_f = open(options.control_file)
316 try:
317 control_file_data = control_file_f.read()
318 finally:
319 control_file_f.close()
mblighbe630eb2008-08-01 16:41:48 +0000320 except IOError:
321 self.generic_error('Unable to read from specified '
322 'control-file: %s' % options.control_file)
mbligh120351e2009-01-24 01:40:45 +0000323 if options.kernel:
324 if options.server:
325 self.invalid_syntax(
326 'A control file and a kernel may only be specified'
327 ' together on client side jobs.')
328 # execute() will pass this to the AFE server to wrap this
329 # control file up to include the kernel installation steps.
330 self.ctrl_file_data['client_control_file'] = control_file_data
331 else:
332 self.data['control_file'] = control_file_data
mbligh4eae22a2008-10-10 16:09:46 +0000333 if options.test:
showard2bab8f42008-11-12 18:15:22 +0000334 if options.server:
mblighb9a8b162008-10-29 16:47:29 +0000335 self.invalid_syntax('If you specify tests, then the '
showard2bab8f42008-11-12 18:15:22 +0000336 'client/server setting is implicit and '
337 'cannot be overriden.')
mbligh4eae22a2008-10-10 16:09:46 +0000338 tests = [t.strip() for t in options.test.split(',') if t.strip()]
mbligh120351e2009-01-24 01:40:45 +0000339 self.ctrl_file_data['tests'] = tests
mbligh4eae22a2008-10-10 16:09:46 +0000340
mblighbe630eb2008-08-01 16:41:48 +0000341
342 if options.priority:
343 self.data['priority'] = options.priority.capitalize()
showard21baa452008-10-21 00:08:39 +0000344 if options.reboot_before:
345 self.data['reboot_before'] = options.reboot_before.capitalize()
346 if options.reboot_after:
347 self.data['reboot_after'] = options.reboot_after.capitalize()
mbligh5d0b4b32008-12-22 14:43:01 +0000348 if options.noverify:
349 self.data['run_verify'] = False
350 if options.timeout:
351 self.data['timeout'] = options.timeout
mblighbe630eb2008-08-01 16:41:48 +0000352
mblighbe630eb2008-08-01 16:41:48 +0000353 self.data['name'] = self.jobname
354
355 (self.data['hosts'],
356 self.data['meta_hosts']) = self.parse_hosts(self.hosts)
357
mblighb9a8b162008-10-29 16:47:29 +0000358 deps = options.labels.split(',')
359 deps = [dep.strip() for dep in deps if dep.strip()]
360 self.data['dependencies'] = deps
mblighbe630eb2008-08-01 16:41:48 +0000361
mbligh6fee7fd2008-10-10 15:44:39 +0000362 self.data['email_list'] = options.email
mbligh7ffdb8b2009-01-21 19:01:51 +0000363 if options.synch_count:
364 self.data['synch_count'] = options.synch_count
mblighbe630eb2008-08-01 16:41:48 +0000365 if options.server:
366 self.data['control_type'] = 'Server'
367 else:
368 self.data['control_type'] = 'Client'
369
mblighbe630eb2008-08-01 16:41:48 +0000370 return (options, leftover)
371
372
373 def execute(self):
374 if self.ctrl_file_data:
mbligh120351e2009-01-24 01:40:45 +0000375 uploading_kernel = 'kernel' in self.ctrl_file_data
376 if uploading_kernel:
mblighbe630eb2008-08-01 16:41:48 +0000377 socket.setdefaulttimeout(topic_common.UPLOAD_SOCKET_TIMEOUT)
378 print 'Uploading Kernel: this may take a while...',
mbligh120351e2009-01-24 01:40:45 +0000379 sys.stdout.flush()
380 try:
381 cf_info = self.execute_rpc(op='generate_control_file',
382 item=self.jobname,
383 **self.ctrl_file_data)
384 finally:
385 if uploading_kernel:
386 socket.setdefaulttimeout(
387 topic_common.DEFAULT_SOCKET_TIMEOUT)
388 if uploading_kernel:
mblighbe630eb2008-08-01 16:41:48 +0000389 print 'Done'
showard989f25d2008-10-01 11:38:11 +0000390 self.data['control_file'] = cf_info['control_file']
mbligh7ffdb8b2009-01-21 19:01:51 +0000391 if 'synch_count' not in self.data:
392 self.data['synch_count'] = cf_info['synch_count']
showard989f25d2008-10-01 11:38:11 +0000393 if cf_info['is_server']:
mblighbe630eb2008-08-01 16:41:48 +0000394 self.data['control_type'] = 'Server'
395 else:
396 self.data['control_type'] = 'Client'
mblighae64d3a2008-10-15 04:13:52 +0000397
mblighb9a8b162008-10-29 16:47:29 +0000398 # Get the union of the 2 sets of dependencies
399 deps = set(self.data['dependencies'])
showarda6fe9c62008-11-03 19:04:25 +0000400 deps = sorted(deps.union(cf_info['dependencies']))
mblighb9a8b162008-10-29 16:47:29 +0000401 self.data['dependencies'] = list(deps)
mblighae64d3a2008-10-15 04:13:52 +0000402
mbligh7ffdb8b2009-01-21 19:01:51 +0000403 if 'synch_count' not in self.data:
404 self.data['synch_count'] = 1
405
mblighfb8f0ab2008-11-13 01:11:48 +0000406 if self.clone_id:
407 clone_info = self.execute_rpc(op='get_info_for_clone',
408 id=self.clone_id,
409 preserve_metahosts=self.reuse_hosts)
410 self.data = clone_info['job']
411
412 # Remove fields from clone data that cannot be reused
413 unused_fields = ('name', 'created_on', 'id', 'owner')
414 for field in unused_fields:
415 del self.data[field]
416
417 # Keyword args cannot be unicode strings
418 for key, val in self.data.iteritems():
419 del self.data[key]
420 self.data[str(key)] = val
421
422 # Convert host list from clone info that can be used for job_create
423 host_list = []
424 if clone_info['meta_host_counts']:
425 # Creates a dictionary of meta_hosts, e.g.
426 # {u'label1': 3, u'label2': 2, u'label3': 5}
427 meta_hosts = clone_info['meta_host_counts']
428 # Create a list of formatted metahosts, e.g.
429 # [u'3*label1', u'2*label2', u'5*label3']
430 meta_host_list = ['%s*%s' % (str(val), key) for key,val in
431 meta_hosts.items()]
432 host_list.extend(meta_host_list)
433 if clone_info['hosts']:
434 # Creates a list of hosts, e.g. [u'host1', u'host2']
435 hosts = [host['hostname'] for host in clone_info['hosts']]
436 host_list.extend(hosts)
437
438 (self.data['hosts'],
439 self.data['meta_hosts']) = self.parse_hosts(host_list)
440 self.data['name'] = self.jobname
441
mbligh7b8d1f92008-10-21 16:24:08 +0000442 socket.setdefaulttimeout(topic_common.LIST_SOCKET_TIMEOUT)
443 # This RPC takes a while when there are lots of hosts.
444 # We don't set it back to default because it's the last RPC.
mblighfb8f0ab2008-11-13 01:11:48 +0000445
mbligh982170a2008-11-20 00:57:39 +0000446 socket.setdefaulttimeout(topic_common.LIST_SOCKET_TIMEOUT)
447 # This RPC takes a while when there are lots of hosts.
448 # We don't set it back to default because it's the last RPC.
mblighae64d3a2008-10-15 04:13:52 +0000449 job_id = self.execute_rpc(op='create_job', **self.data)
450 return ['%s (id %s)' % (self.jobname, job_id)]
mblighbe630eb2008-08-01 16:41:48 +0000451
452
453 def get_items(self):
454 return [self.jobname]
455
456
457class job_abort(job, action_common.atest_delete):
458 """atest job abort <job(s)>"""
459 usage_action = op_action = 'abort'
460 msg_done = 'Aborted'
461
462 def parse(self):
463 (options, leftover) = self.parse_with_flist([('jobids', '', '', True)],
464 req_items='jobids')
465
466
mbligh206d50a2008-11-13 01:19:25 +0000467 def execute(self):
468 data = {'job__id__in': self.jobids}
469 self.execute_rpc(op='abort_host_queue_entries', **data)
470 print 'Aborting jobs: %s' % ', '.join(self.jobids)
471
472
mblighbe630eb2008-08-01 16:41:48 +0000473 def get_items(self):
474 return self.jobids