blob: 26d4d76d339e322747ea9293b4e459f012f40fa8 [file] [log] [blame]
mblighbe630eb2008-08-01 16:41:48 +00001#
2# Copyright 2008 Google Inc. All Rights Reserved.
3
4"""
5The host module contains the objects and method used to
6manage a host in Autotest.
7
8The valid actions are:
9create: adds host(s)
10delete: deletes host(s)
11list: lists host(s)
12stat: displays host(s) information
13mod: modifies host(s)
14jobs: lists all jobs that ran on host(s)
15
16The common options are:
17-M|--mlist: file containing a list of machines
18
19
mblighbe630eb2008-08-01 16:41:48 +000020See topic_common.py for a High Level Design and Algorithm.
21
22"""
Simran Basi0739d682015-02-25 16:22:56 -080023import re
mblighbe630eb2008-08-01 16:41:48 +000024
Jiaxi Luoc342f9f2014-05-19 16:22:03 -070025from autotest_lib.cli import action_common, topic_common
mblighaed47e82009-03-17 19:06:18 +000026from autotest_lib.client.common_lib import host_protections
mblighbe630eb2008-08-01 16:41:48 +000027
28
29class host(topic_common.atest):
30 """Host class
31 atest host [create|delete|list|stat|mod|jobs] <options>"""
32 usage_action = '[create|delete|list|stat|mod|jobs]'
33 topic = msg_topic = 'host'
34 msg_items = '<hosts>'
35
mblighaed47e82009-03-17 19:06:18 +000036 protections = host_protections.Protection.names
37
mblighbe630eb2008-08-01 16:41:48 +000038
39 def __init__(self):
40 """Add to the parser the options common to all the
41 host actions"""
42 super(host, self).__init__()
43
44 self.parser.add_option('-M', '--mlist',
45 help='File listing the machines',
46 type='string',
47 default=None,
48 metavar='MACHINE_FLIST')
49
mbligh9deeefa2009-05-01 23:11:08 +000050 self.topic_parse_info = topic_common.item_parse_info(
51 attribute_name='hosts',
52 filename_option='mlist',
53 use_leftover=True)
mblighbe630eb2008-08-01 16:41:48 +000054
55
56 def _parse_lock_options(self, options):
57 if options.lock and options.unlock:
58 self.invalid_syntax('Only specify one of '
59 '--lock and --unlock.')
60
61 if options.lock:
62 self.data['locked'] = True
63 self.messages.append('Locked host')
64 elif options.unlock:
65 self.data['locked'] = False
Matthew Sartori68186332015-04-27 17:19:53 -070066 self.data['lock_reason'] = ''
mblighbe630eb2008-08-01 16:41:48 +000067 self.messages.append('Unlocked host')
68
Matthew Sartori68186332015-04-27 17:19:53 -070069 if options.lock and options.lock_reason:
70 self.data['lock_reason'] = options.lock_reason
71
mblighbe630eb2008-08-01 16:41:48 +000072
73 def _cleanup_labels(self, labels, platform=None):
74 """Removes the platform label from the overall labels"""
75 if platform:
76 return [label for label in labels
77 if label != platform]
78 else:
79 try:
80 return [label for label in labels
81 if not label['platform']]
82 except TypeError:
83 # This is a hack - the server will soon
84 # do this, so all this code should be removed.
85 return labels
86
87
88 def get_items(self):
89 return self.hosts
90
91
92class host_help(host):
93 """Just here to get the atest logic working.
94 Usage is set by its parent"""
95 pass
96
97
98class host_list(action_common.atest_list, host):
99 """atest host list [--mlist <file>|<hosts>] [--label <label>]
mbligh536a5242008-10-18 14:35:54 +0000100 [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
mblighbe630eb2008-08-01 16:41:48 +0000101
102 def __init__(self):
103 super(host_list, self).__init__()
104
105 self.parser.add_option('-b', '--label',
mbligh6444c6b2008-10-27 20:55:13 +0000106 default='',
107 help='Only list hosts with all these labels '
108 '(comma separated)')
mblighbe630eb2008-08-01 16:41:48 +0000109 self.parser.add_option('-s', '--status',
mbligh6444c6b2008-10-27 20:55:13 +0000110 default='',
111 help='Only list hosts with any of these '
112 'statuses (comma separated)')
mbligh536a5242008-10-18 14:35:54 +0000113 self.parser.add_option('-a', '--acl',
mbligh6444c6b2008-10-27 20:55:13 +0000114 default='',
mbligh536a5242008-10-18 14:35:54 +0000115 help='Only list hosts within this ACL')
116 self.parser.add_option('-u', '--user',
mbligh6444c6b2008-10-27 20:55:13 +0000117 default='',
mbligh536a5242008-10-18 14:35:54 +0000118 help='Only list hosts available to this user')
mbligh70b9bf42008-11-18 15:00:46 +0000119 self.parser.add_option('-N', '--hostnames-only', help='Only return '
120 'hostnames for the machines queried.',
121 action='store_true')
mbligh91e0efd2009-02-26 01:02:16 +0000122 self.parser.add_option('--locked',
123 default=False,
124 help='Only list locked hosts',
125 action='store_true')
126 self.parser.add_option('--unlocked',
127 default=False,
128 help='Only list unlocked hosts',
129 action='store_true')
130
mblighbe630eb2008-08-01 16:41:48 +0000131
132
133 def parse(self):
134 """Consume the specific options"""
jamesrenc2863162010-07-12 21:20:51 +0000135 label_info = topic_common.item_parse_info(attribute_name='labels',
136 inline_option='label')
137
138 (options, leftover) = super(host_list, self).parse([label_info])
139
mblighbe630eb2008-08-01 16:41:48 +0000140 self.status = options.status
mbligh536a5242008-10-18 14:35:54 +0000141 self.acl = options.acl
142 self.user = options.user
mbligh70b9bf42008-11-18 15:00:46 +0000143 self.hostnames_only = options.hostnames_only
mbligh91e0efd2009-02-26 01:02:16 +0000144
145 if options.locked and options.unlocked:
146 self.invalid_syntax('--locked and --unlocked are '
147 'mutually exclusive')
148 self.locked = options.locked
149 self.unlocked = options.unlocked
mblighbe630eb2008-08-01 16:41:48 +0000150 return (options, leftover)
151
152
153 def execute(self):
154 filters = {}
155 check_results = {}
156 if self.hosts:
157 filters['hostname__in'] = self.hosts
158 check_results['hostname__in'] = 'hostname'
mbligh6444c6b2008-10-27 20:55:13 +0000159
160 if self.labels:
jamesrenc2863162010-07-12 21:20:51 +0000161 if len(self.labels) == 1:
mblighf703fb42009-01-30 00:35:05 +0000162 # This is needed for labels with wildcards (x86*)
jamesrenc2863162010-07-12 21:20:51 +0000163 filters['labels__name__in'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000164 check_results['labels__name__in'] = None
165 else:
jamesrenc2863162010-07-12 21:20:51 +0000166 filters['multiple_labels'] = self.labels
mblighf703fb42009-01-30 00:35:05 +0000167 check_results['multiple_labels'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000168
mblighbe630eb2008-08-01 16:41:48 +0000169 if self.status:
mbligh6444c6b2008-10-27 20:55:13 +0000170 statuses = self.status.split(',')
171 statuses = [status.strip() for status in statuses
172 if status.strip()]
173
174 filters['status__in'] = statuses
mblighcd8eb972008-08-25 19:20:39 +0000175 check_results['status__in'] = None
mbligh6444c6b2008-10-27 20:55:13 +0000176
mbligh536a5242008-10-18 14:35:54 +0000177 if self.acl:
showardd9ac4452009-02-07 02:04:37 +0000178 filters['aclgroup__name'] = self.acl
179 check_results['aclgroup__name'] = None
mbligh536a5242008-10-18 14:35:54 +0000180 if self.user:
showardd9ac4452009-02-07 02:04:37 +0000181 filters['aclgroup__users__login'] = self.user
182 check_results['aclgroup__users__login'] = None
mbligh91e0efd2009-02-26 01:02:16 +0000183
184 if self.locked or self.unlocked:
185 filters['locked'] = self.locked
186 check_results['locked'] = None
187
mblighbe630eb2008-08-01 16:41:48 +0000188 return super(host_list, self).execute(op='get_hosts',
189 filters=filters,
190 check_results=check_results)
191
192
193 def output(self, results):
194 if results:
195 # Remove the platform from the labels.
196 for result in results:
197 result['labels'] = self._cleanup_labels(result['labels'],
198 result['platform'])
mbligh70b9bf42008-11-18 15:00:46 +0000199 if self.hostnames_only:
mblighdf75f8b2008-11-18 19:07:42 +0000200 self.print_list(results, key='hostname')
mbligh70b9bf42008-11-18 15:00:46 +0000201 else:
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800202 keys = ['hostname', 'status',
Aviv Keshet619901b2016-03-28 12:09:06 -0700203 'shard', 'locked', 'lock_reason', 'locked_by', 'platform',
204 'labels']
Prashanth Balasubramaniana5048562014-12-12 10:14:11 -0800205 super(host_list, self).output(results, keys=keys)
mblighbe630eb2008-08-01 16:41:48 +0000206
207
208class host_stat(host):
209 """atest host stat --mlist <file>|<hosts>"""
210 usage_action = 'stat'
211
212 def execute(self):
213 results = []
214 # Convert wildcards into real host stats.
215 existing_hosts = []
216 for host in self.hosts:
217 if host.endswith('*'):
218 stats = self.execute_rpc('get_hosts',
219 hostname__startswith=host.rstrip('*'))
220 if len(stats) == 0:
221 self.failure('No hosts matching %s' % host, item=host,
222 what_failed='Failed to stat')
223 continue
224 else:
225 stats = self.execute_rpc('get_hosts', hostname=host)
226 if len(stats) == 0:
227 self.failure('Unknown host %s' % host, item=host,
228 what_failed='Failed to stat')
229 continue
230 existing_hosts.extend(stats)
231
232 for stat in existing_hosts:
233 host = stat['hostname']
234 # The host exists, these should succeed
235 acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
236
237 labels = self.execute_rpc('get_labels', host__hostname=host)
Simran Basi0739d682015-02-25 16:22:56 -0800238 results.append([[stat], acls, labels, stat['attributes']])
mblighbe630eb2008-08-01 16:41:48 +0000239 return results
240
241
242 def output(self, results):
Simran Basi0739d682015-02-25 16:22:56 -0800243 for stats, acls, labels, attributes in results:
mblighbe630eb2008-08-01 16:41:48 +0000244 print '-'*5
245 self.print_fields(stats,
246 keys=['hostname', 'platform',
mblighe163b032008-10-18 14:30:27 +0000247 'status', 'locked', 'locked_by',
Matthew Sartori68186332015-04-27 17:19:53 -0700248 'lock_time', 'lock_reason', 'protection',])
mblighbe630eb2008-08-01 16:41:48 +0000249 self.print_by_ids(acls, 'ACLs', line_before=True)
250 labels = self._cleanup_labels(labels)
251 self.print_by_ids(labels, 'Labels', line_before=True)
Simran Basi0739d682015-02-25 16:22:56 -0800252 self.print_dict(attributes, 'Host Attributes', line_before=True)
mblighbe630eb2008-08-01 16:41:48 +0000253
254
255class host_jobs(host):
mbligh1494eca2008-08-13 21:24:22 +0000256 """atest host jobs [--max-query] --mlist <file>|<hosts>"""
mblighbe630eb2008-08-01 16:41:48 +0000257 usage_action = 'jobs'
258
mbligh6996fe82008-08-13 00:32:27 +0000259 def __init__(self):
260 super(host_jobs, self).__init__()
261 self.parser.add_option('-q', '--max-query',
262 help='Limits the number of results '
263 '(20 by default)',
264 type='int', default=20)
265
266
267 def parse(self):
268 """Consume the specific options"""
mbligh9deeefa2009-05-01 23:11:08 +0000269 (options, leftover) = super(host_jobs, self).parse()
mbligh6996fe82008-08-13 00:32:27 +0000270 self.max_queries = options.max_query
271 return (options, leftover)
272
273
mblighbe630eb2008-08-01 16:41:48 +0000274 def execute(self):
275 results = []
276 real_hosts = []
277 for host in self.hosts:
278 if host.endswith('*'):
279 stats = self.execute_rpc('get_hosts',
280 hostname__startswith=host.rstrip('*'))
281 if len(stats) == 0:
282 self.failure('No host matching %s' % host, item=host,
283 what_failed='Failed to stat')
284 [real_hosts.append(stat['hostname']) for stat in stats]
285 else:
286 real_hosts.append(host)
287
288 for host in real_hosts:
289 queue_entries = self.execute_rpc('get_host_queue_entries',
mbligh6996fe82008-08-13 00:32:27 +0000290 host__hostname=host,
mbligh1494eca2008-08-13 21:24:22 +0000291 query_limit=self.max_queries,
showard6958c722009-09-23 20:03:16 +0000292 sort_by=['-job__id'])
mblighbe630eb2008-08-01 16:41:48 +0000293 jobs = []
294 for entry in queue_entries:
295 job = {'job_id': entry['job']['id'],
296 'job_owner': entry['job']['owner'],
297 'job_name': entry['job']['name'],
298 'status': entry['status']}
299 jobs.append(job)
300 results.append((host, jobs))
301 return results
302
303
304 def output(self, results):
305 for host, jobs in results:
306 print '-'*5
307 print 'Hostname: %s' % host
308 self.print_table(jobs, keys_header=['job_id',
showardbe0d8692009-08-20 23:42:44 +0000309 'job_owner',
310 'job_name',
311 'status'])
mblighbe630eb2008-08-01 16:41:48 +0000312
313
314class host_mod(host):
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800315 """atest host mod --lock|--unlock|--force_modify_locking|--protection
mblighbe630eb2008-08-01 16:41:48 +0000316 --mlist <file>|<hosts>"""
317 usage_action = 'mod'
Simran Basi0739d682015-02-25 16:22:56 -0800318 attribute_regex = r'^(?P<attribute>\w+)=(?P<value>.+)?'
mblighbe630eb2008-08-01 16:41:48 +0000319
320 def __init__(self):
321 """Add the options specific to the mod action"""
322 self.data = {}
323 self.messages = []
Simran Basi0739d682015-02-25 16:22:56 -0800324 self.attribute = None
325 self.value = None
mblighbe630eb2008-08-01 16:41:48 +0000326 super(host_mod, self).__init__()
mblighbe630eb2008-08-01 16:41:48 +0000327 self.parser.add_option('-l', '--lock',
328 help='Lock hosts',
329 action='store_true')
330 self.parser.add_option('-u', '--unlock',
331 help='Unlock hosts',
332 action='store_true')
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800333 self.parser.add_option('-f', '--force_modify_locking',
334 help='Forcefully lock\unlock a host',
335 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700336 self.parser.add_option('-r', '--lock_reason',
337 help='Reason for locking hosts',
338 default='')
mblighe163b032008-10-18 14:30:27 +0000339 self.parser.add_option('-p', '--protection', type='choice',
mblighaed47e82009-03-17 19:06:18 +0000340 help=('Set the protection level on a host. '
341 'Must be one of: %s' %
342 ', '.join('"%s"' % p
343 for p in self.protections)),
344 choices=self.protections)
Simran Basi0739d682015-02-25 16:22:56 -0800345 self.parser.add_option('--attribute', '-a', default='',
346 help=('Host attribute to add or change. Format '
347 'is <attribute>=<value>. Value can be '
348 'blank to delete attribute.'))
mblighbe630eb2008-08-01 16:41:48 +0000349
350
351 def parse(self):
352 """Consume the specific options"""
353 (options, leftover) = super(host_mod, self).parse()
354
355 self._parse_lock_options(options)
Shuqian Zhao4c0d2902016-01-12 17:03:15 -0800356 if options.force_modify_locking:
357 self.data['force_modify_locking'] = True
mblighbe630eb2008-08-01 16:41:48 +0000358
mblighe163b032008-10-18 14:30:27 +0000359 if options.protection:
360 self.data['protection'] = options.protection
showardbe0d8692009-08-20 23:42:44 +0000361 self.messages.append('Protection set to "%s"' % options.protection)
mblighe163b032008-10-18 14:30:27 +0000362
Simran Basi0739d682015-02-25 16:22:56 -0800363 if len(self.data) == 0 and not options.attribute:
mblighbe630eb2008-08-01 16:41:48 +0000364 self.invalid_syntax('No modification requested')
Simran Basi0739d682015-02-25 16:22:56 -0800365
366 if options.attribute:
367 match = re.match(self.attribute_regex, options.attribute)
368 if not match:
369 self.invalid_syntax('Attributes must be in <attribute>=<value>'
370 ' syntax!')
371
372 self.attribute = match.group('attribute')
373 self.value = match.group('value')
374
mblighbe630eb2008-08-01 16:41:48 +0000375 return (options, leftover)
376
377
378 def execute(self):
379 successes = []
380 for host in self.hosts:
mbligh6eca4602009-01-07 16:48:50 +0000381 try:
382 res = self.execute_rpc('modify_host', item=host,
383 id=host, **self.data)
Simran Basi0739d682015-02-25 16:22:56 -0800384 if self.attribute:
385 self.execute_rpc('set_host_attribute',
386 attribute=self.attribute,
387 value=self.value, hostname=host)
mbligh6eca4602009-01-07 16:48:50 +0000388 # TODO: Make the AFE return True or False,
389 # especially for lock
mblighbe630eb2008-08-01 16:41:48 +0000390 successes.append(host)
mbligh6eca4602009-01-07 16:48:50 +0000391 except topic_common.CliError, full_error:
392 # Already logged by execute_rpc()
393 pass
394
mblighbe630eb2008-08-01 16:41:48 +0000395 return successes
396
397
398 def output(self, hosts):
399 for msg in self.messages:
400 self.print_wrapped(msg, hosts)
401
402
mblighbe630eb2008-08-01 16:41:48 +0000403class host_create(host):
404 """atest host create [--lock|--unlock --platform <arch>
405 --labels <labels>|--blist <label_file>
406 --acls <acls>|--alist <acl_file>
mblighaed47e82009-03-17 19:06:18 +0000407 --protection <protection_type>
mblighbe630eb2008-08-01 16:41:48 +0000408 --mlist <mach_file>] <hosts>"""
409 usage_action = 'create'
410
411 def __init__(self):
412 self.messages = []
413 super(host_create, self).__init__()
414 self.parser.add_option('-l', '--lock',
415 help='Create the hosts as locked',
mbligh719e14a2008-12-04 01:17:08 +0000416 action='store_true', default=False)
mblighbe630eb2008-08-01 16:41:48 +0000417 self.parser.add_option('-u', '--unlock',
418 help='Create the hosts as '
419 'unlocked (default)',
420 action='store_true')
Matthew Sartori68186332015-04-27 17:19:53 -0700421 self.parser.add_option('-r', '--lock_reason',
422 help='Reason for locking hosts',
423 default='')
mblighbe630eb2008-08-01 16:41:48 +0000424 self.parser.add_option('-t', '--platform',
425 help='Sets the platform label')
426 self.parser.add_option('-b', '--labels',
427 help='Comma separated list of labels')
428 self.parser.add_option('-B', '--blist',
429 help='File listing the labels',
430 type='string',
431 metavar='LABEL_FLIST')
432 self.parser.add_option('-a', '--acls',
433 help='Comma separated list of ACLs')
434 self.parser.add_option('-A', '--alist',
435 help='File listing the acls',
436 type='string',
437 metavar='ACL_FLIST')
mblighaed47e82009-03-17 19:06:18 +0000438 self.parser.add_option('-p', '--protection', type='choice',
439 help=('Set the protection level on a host. '
440 'Must be one of: %s' %
441 ', '.join('"%s"' % p
442 for p in self.protections)),
443 choices=self.protections)
Kevin Cheng3a4a57a2015-09-30 12:09:50 -0700444 self.parser.add_option('-s', '--serials',
445 help=('Comma separated list of adb-based device '
446 'serials'))
mblighbe630eb2008-08-01 16:41:48 +0000447
448
449 def parse(self):
mbligh9deeefa2009-05-01 23:11:08 +0000450 label_info = topic_common.item_parse_info(attribute_name='labels',
451 inline_option='labels',
452 filename_option='blist')
453 acl_info = topic_common.item_parse_info(attribute_name='acls',
454 inline_option='acls',
455 filename_option='alist')
456
457 (options, leftover) = super(host_create, self).parse([label_info,
458 acl_info],
459 req_items='hosts')
mblighbe630eb2008-08-01 16:41:48 +0000460
461 self._parse_lock_options(options)
mbligh719e14a2008-12-04 01:17:08 +0000462 self.locked = options.lock
mblighbe630eb2008-08-01 16:41:48 +0000463 self.platform = getattr(options, 'platform', None)
Kevin Cheng3a4a57a2015-09-30 12:09:50 -0700464 self.serials = getattr(options, 'serials', None)
465 if self.serials:
466 if len(self.hosts) > 1:
467 raise topic_common.CliError('Can not specify serials with '
468 'multiple hosts')
469 self.serials = self.serials.split(',')
mblighaed47e82009-03-17 19:06:18 +0000470 if options.protection:
471 self.data['protection'] = options.protection
mblighbe630eb2008-08-01 16:41:48 +0000472 return (options, leftover)
473
474
475 def _execute_add_one_host(self, host):
mbligh719e14a2008-12-04 01:17:08 +0000476 # Always add the hosts as locked to avoid the host
Matthew Sartori68186332015-04-27 17:19:53 -0700477 # being picked up by the scheduler before it's ACL'ed.
478 # We enforce lock reasons for each lock, so we
479 # provide a 'dummy' if we are intending to unlock after.
mbligh719e14a2008-12-04 01:17:08 +0000480 self.data['locked'] = True
Matthew Sartori68186332015-04-27 17:19:53 -0700481 if not self.locked:
482 self.data['lock_reason'] = 'Forced lock on device creation'
mblighbe630eb2008-08-01 16:41:48 +0000483 self.execute_rpc('add_host', hostname=host,
484 status="Ready", **self.data)
485
486 # Now add the platform label
487 labels = self.labels[:]
488 if self.platform:
489 labels.append(self.platform)
490 if len (labels):
491 self.execute_rpc('host_add_labels', id=host, labels=labels)
492
493
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700494 def _execute_add_hosts(self):
495 successful_hosts = self.site_create_hosts_hook()
496
497 if successful_hosts:
498 for acl in self.acls:
499 self.execute_rpc('acl_group_add_hosts',
500 id=acl,
501 hosts=successful_hosts)
502
503 if not self.locked:
504 for host in successful_hosts:
Matthew Sartori68186332015-04-27 17:19:53 -0700505 self.execute_rpc('modify_host', id=host, locked=False,
506 lock_reason='')
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700507 return successful_hosts
508
509
mblighbe630eb2008-08-01 16:41:48 +0000510 def execute(self):
511 # We need to check if these labels & ACLs exist,
512 # and create them if not.
513 if self.platform:
514 self.check_and_create_items('get_labels', 'add_label',
515 [self.platform],
516 platform=True)
517
518 if self.labels:
519 self.check_and_create_items('get_labels', 'add_label',
520 self.labels,
521 platform=False)
522
523 if self.acls:
524 self.check_and_create_items('get_acl_groups',
525 'add_acl_group',
526 self.acls)
527
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700528 return self._execute_add_hosts()
mblighbe630eb2008-08-01 16:41:48 +0000529
530
531 def site_create_hosts_hook(self):
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700532 successful_hosts = []
mblighbe630eb2008-08-01 16:41:48 +0000533 for host in self.hosts:
534 try:
535 self._execute_add_one_host(host)
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700536 successful_hosts.append(host)
mblighbe630eb2008-08-01 16:41:48 +0000537 except topic_common.CliError:
538 pass
539
Jiaxi Luoc342f9f2014-05-19 16:22:03 -0700540 return successful_hosts
mblighbe630eb2008-08-01 16:41:48 +0000541
542
543 def output(self, hosts):
544 self.print_wrapped('Added host', hosts)
545
546
547class host_delete(action_common.atest_delete, host):
548 """atest host delete [--mlist <mach_file>] <hosts>"""
549 pass